pax_global_header00006660000000000000000000000064146072404010014510gustar00rootroot0000000000000052 comment=75d946e4be493316a98a638adedf537d1000889e python-telegram-bot-21.1.1/000077500000000000000000000000001460724040100154735ustar00rootroot00000000000000python-telegram-bot-21.1.1/.git-blame-ignore-revs000066400000000000000000000005531460724040100215760ustar00rootroot00000000000000# .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 python-telegram-bot-21.1.1/.github/000077500000000000000000000000001460724040100170335ustar00rootroot00000000000000python-telegram-bot-21.1.1/.github/CONTRIBUTING.rst000066400000000000000000000347671460724040100215150ustar00rootroot00000000000000================= How To Contribute ================= Every open source project lives from the generous help by contributors that sacrifice their time and ``python-telegram-bot`` is no different. To make participation as pleasant as possible, this project adheres to the `Code of Conduct`_ by the Python Software Foundation. Setting things up ================= 1. Fork the ``python-telegram-bot`` repository to your GitHub account. 2. Clone your forked repository of ``python-telegram-bot`` to your computer: .. code-block:: bash $ git clone https://github.com//python-telegram-bot $ cd python-telegram-bot 3. Add a track to the original repository: .. code-block:: bash $ git remote add upstream https://github.com/python-telegram-bot/python-telegram-bot 4. Install dependencies: .. code-block:: bash $ pip install -r requirements-all.txt 5. Install pre-commit hooks: .. code-block:: bash $ pre-commit install Finding something to do ======================= If you already know what you'd like to work on, you can skip this section. If you have an idea for something to do, first check if it's already been filed on the `issue tracker`_. If so, add a comment to the issue saying you'd like to work on it, and we'll help you get started! Otherwise, please file a new issue and assign yourself to it. Another great way to start contributing is by writing tests. Tests are really important because they help prevent developers from accidentally breaking existing code, allowing them to build cool things faster. If you're interested in helping out, let the development team know by posting to the `Telegram group`_, and we'll help you get started. That being said, we want to mention that we are very hesitant about adding new requirements to our projects. If you intend to do this, please state this in an issue and get a verification from one of the maintainers. Instructions for making a code change ===================================== The central development branch is ``master``, which should be clean and ready for release at any time. In general, all changes should be done as feature branches based off of ``master``. If you want to do solely documentation changes, base them and PR to the branch ``doc-fixes``. This branch also has its own `RTD build`_. Here's how to make a one-off code change. 1. **Choose a descriptive branch name.** It should be lowercase, hyphen-separated, and a noun describing the change (so, ``fuzzy-rules``, but not ``implement-fuzzy-rules``). Also, it shouldn't start with ``hotfix`` or ``release``. 2. **Create a new branch with this name, starting from** ``master``. In other words, run: .. code-block:: bash $ git fetch upstream $ git checkout master $ git merge upstream/master $ git checkout -b your-branch-name 3. **Make a commit to your feature branch**. Each commit should be self-contained and have a descriptive commit message that helps other developers understand why the changes were made. We also have a check-list for PRs `below`_. - You can refer to relevant issues in the commit message by writing, e.g., "#105". - Your code should adhere to the `PEP 8 Style Guide`_, with the exception that we have a maximum line length of 99. - Provide static typing with signature annotations. The documentation of `MyPy`_ will be a good start, the cheat sheet is `here`_. We also have some custom type aliases in ``telegram._utils.types``. - Document your code. This step is pretty important to us, so it has its own `section`_. - For consistency, please conform to `Google Python Style Guide`_ and `Google Python Style Docstrings`_. - The following exceptions to the above (Google's) style guides applies: - Documenting types of global variables and complex types of class members can be done using the Sphinx docstring convention. - In addition, PTB uses some formatting/styling and linting tools in the pre-commit setup. Some of those tools also have command line tools that can help to run these tools outside of the pre-commit step. If you'd like to leverage that, please have a look at the `pre-commit config file`_ for an overview of which tools (and which versions of them) are used. For example, we use `Black`_ for code formatting. Plugins for Black exist for some `popular editors`_. You can use those instead of manually formatting everything. - Please ensure that the code you write is well-tested and that all automated tests still pass. We have dedicated an `testing page`_ to help you with that. - Don't break backward compatibility. - Add yourself to the AUTHORS.rst_ file in an alphabetical fashion. - If you want run style & type checks before committing run .. code-block:: bash $ pre-commit run -a - To actually make the commit (this will trigger tests style & type checks automatically): .. code-block:: bash $ git add your-file-changed.py - Finally, push it to your GitHub fork, run: .. code-block:: bash $ git push origin your-branch-name 4. **When your feature is ready to merge, create a pull request.** - Go to your fork on GitHub, select your branch from the dropdown menu, and click "New pull request". - Add a descriptive comment explaining the purpose of the branch (e.g. "Add the new API feature to create inline bot queries."). This will tell the reviewer what the purpose of the branch is. - Click "Create pull request". An admin will assign a reviewer to your commit. 5. **Address review comments until all reviewers give LGTM ('looks good to me').** - When your reviewer has reviewed the code, you'll get a notification. You'll need to respond in two ways: - Make a new commit addressing the comments you agree with, and push it to the same branch. Ideally, the commit message would explain what the commit does (e.g. "Fix lint error"), but if there are lots of disparate review comments, it's fine to refer to the original commit message and add something like "(address review comments)". - In order to keep the commit history intact, please avoid squashing or amending history and then force-pushing to the PR. Reviewers often want to look at individual commits. - In addition, please reply to each comment. Each reply should be either "Done" or a response explaining why the corresponding suggestion wasn't implemented. All comments must be resolved before LGTM can be given. - Resolve any merge conflicts that arise. To resolve conflicts between 'your-branch-name' (in your fork) and 'master' (in the ``python-telegram-bot`` repository), run: .. code-block:: bash $ git checkout your-branch-name $ git fetch upstream $ git merge upstream/master $ ...[fix the conflicts]... $ ...[make sure the tests pass before committing]... $ git commit -a $ git push origin your-branch-name - At the end, the reviewer will merge the pull request. 6. **Tidy up!** Delete the feature branch from both your local clone and the GitHub repository: .. code-block:: bash $ git branch -D your-branch-name $ git push origin --delete your-branch-name 7. **Celebrate.** Congratulations, you have contributed to ``python-telegram-bot``! Check-list for PRs ------------------ This checklist is a non-exhaustive reminder of things that should be done before a PR is merged, both for you as contributor and for the maintainers. Feel free to copy (parts of) the checklist to the PR description to remind you or the maintainers of open points or if you have questions on anything. - Added ``.. versionadded:: NEXT.VERSION``, ``.. versionchanged:: NEXT.VERSION`` or ``.. deprecated:: NEXT.VERSION`` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes) - Created new or adapted existing unit tests - Documented code changes according to the `CSI standard `__ - Added myself alphabetically to ``AUTHORS.rst`` (optional) - Added new classes & modules to the docs and all suitable ``__all__`` s - Checked the `Stability Policy `_ in case of deprecations or changes to documented behavior **If the PR contains API changes (otherwise, you can ignore this passage)** - Checked the Bot API specific sections of the `Stability Policy `_ - Created a PR to remove functionality deprecated in the previous Bot API release (`see here `_) - New classes: - Added ``self._id_attrs`` and corresponding documentation - ``__init__`` accepts ``api_kwargs`` as kw-only - Added new shortcuts: - In :class:`~telegram.Chat` & :class:`~telegram.User` for all methods that accept ``chat/user_id`` - In :class:`~telegram.Message` for all methods that accept ``chat_id`` and ``message_id`` - For new :class:`~telegram.Message` shortcuts: Added ``quote`` argument if methods accepts ``reply_to_message_id`` - In :class:`~telegram.CallbackQuery` for all methods that accept either ``chat_id`` and ``message_id`` or ``inline_message_id`` - If relevant: - Added new constants at :mod:`telegram.constants` and shortcuts to them as class variables - Link new and existing constants in docstrings instead of hard-coded numbers and strings - Add new message types to :attr:`telegram.Message.effective_attachment` - Added new handlers for new update types - Add the handlers to the warning loop in the :class:`~telegram.ext.ConversationHandler` - Added new filters for new message (sub)types - Added or updated documentation for the changed class(es) and/or method(s) - Added the new method(s) to ``_extbot.py`` - Added or updated ``bot_methods.rst`` - Updated the Bot API version number in all places: ``README.rst`` and ``README_RAW.rst`` (including the badge), as well as ``telegram.constants.BOT_API_VERSION_INFO`` - Added logic for arbitrary callback data in :class:`telegram.ext.ExtBot` for new methods that either accept a ``reply_markup`` in some form or have a return type that is/contains :class:`~telegram.Message` Documenting =========== The documentation of this project is separated in two sections: User facing and dev facing. User facing docs are hosted at `RTD`_. They are the main way the users of our library are supposed to get information about the objects. They don't care about the internals, they just want to know what they have to pass to make it work, what it actually does. You can/should provide examples for non obvious cases (like the Filter module), and notes/warnings. Dev facing, on the other side, is for the devs/maintainers of this project. These doc strings don't have a separate documentation site they generate, instead, they document the actual code. User facing documentation ------------------------- We use `sphinx`_ to generate static HTML docs. To build them, first make sure you're running Python 3.9 or above and have the required dependencies: .. code-block:: bash $ pip install -r docs/requirements-docs.txt then run the following from the PTB root directory: .. code-block:: bash $ make -C docs html or, if you don't have ``make`` available (e.g. on Windows): .. code-block:: bash $ sphinx-build docs/source docs/build/html Once the process terminates, you can view the built documentation by opening ``docs/build/html/index.html`` with a browser. - Add ``.. versionadded:: NEXT.VERSION``, ``.. versionchanged:: NEXT.VERSION`` or ``.. deprecated:: NEXT.VERSION`` to the associated documentation of your changes, depending on what kind of change you made. This only applies if the change you made is visible to an end user. The directives should be added to class/method descriptions if their general behaviour changed and to the description of all arguments & attributes that changed. Dev facing documentation ------------------------ We adhere to the `CSI`_ standard. This documentation is not fully implemented in the project, yet, but new code changes should comply with the `CSI` standard. The idea behind this is to make it very easy for you/a random maintainer or even a totally foreign person to drop anywhere into the code and more or less immediately understand what a particular line does. This will make it easier for new to make relevant changes if said lines don't do what they are supposed to. Style commandments ================== Assert comparison order ----------------------- Assert statements should compare in **actual** == **expected** order. For example (assuming ``test_call`` is the thing being tested): .. code-block:: python # GOOD assert test_call() == 5 # BAD assert 5 == test_call() Properly calling callables -------------------------- Methods, functions and classes can specify optional parameters (with default values) using Python's keyword arg syntax. When providing a value to such a callable we prefer that the call also uses keyword arg syntax. For example: .. code-block:: python # GOOD f(0, optional=True) # BAD f(0, True) This gives us the flexibility to re-order arguments and more importantly to add new required arguments. It's also more explicit and easier to read. .. _`Code of Conduct`: https://www.python.org/psf/conduct/ .. _`issue tracker`: https://github.com/python-telegram-bot/python-telegram-bot/issues .. _`Telegram group`: https://telegram.me/pythontelegrambotgroup .. _`PEP 8 Style Guide`: https://peps.python.org/pep-0008/ .. _`sphinx`: https://www.sphinx-doc.org/en/master .. _`Google Python Style Guide`: https://google.github.io/styleguide/pyguide.html .. _`Google Python Style Docstrings`: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html .. _AUTHORS.rst: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/AUTHORS.rst .. _`MyPy`: https://mypy.readthedocs.io/en/stable/index.html .. _`here`: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html .. _`pre-commit config file`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/.pre-commit-config.yaml .. _`Black`: https://black.readthedocs.io/en/stable/index.html .. _`popular editors`: https://black.readthedocs.io/en/stable/integrations/editors.html .. _`RTD`: https://docs.python-telegram-bot.org/ .. _`RTD build`: https://docs.python-telegram-bot.org/en/doc-fixes .. _`CSI`: https://standards.mousepawmedia.com/en/stable/csi.html .. _`section`: #documenting .. _`testing page`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/tests/README.rst .. _`below`: #check-list-for-prs python-telegram-bot-21.1.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001460724040100212165ustar00rootroot00000000000000python-telegram-bot-21.1.1/.github/ISSUE_TEMPLATE/bug-report.yml000066400000000000000000000040071460724040100240300ustar00rootroot00000000000000name: Bug Report description: Create a report to help us improve title: "[BUG]" labels: ["bug :bug:"] body: - type: markdown attributes: value: | Thanks for reporting issues of python-telegram-bot! Use this template to notify us if you found a bug. To make it easier for us to help you please enter detailed information below. Please note, we only support the latest version of python-telegram-bot and master branch. Please make sure to upgrade & recreate the issue on the latest version prior to opening an issue. - type: textarea id: steps-to-reproduce attributes: label: Steps to Reproduce value: | 1. 2. 3. validations: required: true - type: textarea id: expected-behaviour attributes: label: Expected behaviour description: Tell us what should happen validations: required: true - type: textarea id: actual-behaviour attributes: label: Actual behaviour description: Tell us what happens instead validations: required: true - type: markdown attributes: value: "### Configuration" - type: input id: operating-system attributes: label: Operating System validations: required: true - type: textarea id: versions attributes: label: Version of Python, python-telegram-bot & dependencies description: Paste the output of `$ python -m telegram` here. This will be automatically formatted into code, so no need for backticks. render: shell validations: required: true - type: textarea id: logs attributes: label: Relevant log output description: Insert logs here (if necessary). This will be automatically formatted into code, so no need for backticks. render: python - type: textarea id: additional-context attributes: label: Additional Context description: You may provide any other additional context to the bug here. python-telegram-bot-21.1.1/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000006211460724040100232050ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Telegram Group url: https://telegram.me/pythontelegrambotgroup about: Questions asked on the group usually get answered faster. - name: GitHub Discussions url: https://github.com/python-telegram-bot/python-telegram-bot/discussions about: For getting answers to usage on GitHub, Discussions is even better than this bug tracker :) python-telegram-bot-21.1.1/.github/ISSUE_TEMPLATE/feature-request.yml000066400000000000000000000025251460724040100250660ustar00rootroot00000000000000name: Feature Request description: Suggest an idea for this project title: "[FEATURE]" labels: ["enhancement"] body: - type: textarea id: related-problem attributes: label: "What kind of feature are you missing? Where do you notice a shortcoming of PTB?" description: "A clear and concise description of what the problem is." placeholder: "Example: I want to do X, but there is no way to do it." validations: required: true - type: textarea id: solution attributes: label: "Describe the solution you'd like" description: "A clear and concise description of what you want to happen." placeholder: "Example: I think it would be nice if you would add feature Y so I can do X." validations: required: true - type: textarea id: alternatives attributes: label: "Describe alternatives you've considered" description: "A clear and concise description of any alternative solutions or features you've considered." placeholder: "Example: I considered Z to be able to do X, but that didn't work because..." - type: textarea id: additional-context attributes: label: "Additional context" description: "Add any other context or screenshots about the feature request here." placeholder: "Example: Here's a photo of my cat!" python-telegram-bot-21.1.1/.github/ISSUE_TEMPLATE/question.yml000066400000000000000000000046351460724040100236200ustar00rootroot00000000000000name: Question description: Get help with errors or general questions title: "[QUESTION]" labels: ["question"] body: - type: markdown attributes: value: | Hey there, you have a question? We are happy to answer. Please make sure no similar question was opened already. To make it easier for us to help you, please read this [article](https://github.com/python-telegram-bot/python-telegram-bot/wiki/Ask-Right). Please mind that there is also a users' [Telegram group](https://t.me/pythontelegrambotgroup) for questions about the library. Questions asked there might be answered quicker than here. Moreover, [GitHub Discussions](https://github.com/python-telegram-bot/python-telegram-bot/discussions) offer a slightly better format to discuss usage questions. - type: textarea id: issue-faced attributes: label: "Issue I am facing" description: "Please describe the issue here in as much detail as possible" validations: required: true - type: textarea id: traceback attributes: label: "Traceback to the issue" description: "If you are facing a specific error message, please paste the traceback here. This will be automatically formatted into python code, so no need for backticks." placeholder: | Traceback (most recent call last): File "/home/bot.py", line 1, in main foo = bar() ... telegram.error.BadRequest: Traceback not found render: python - type: textarea id: related-code attributes: label: "Related part of your code" description: "This will be automatically formatted into code (python), so no need for backticks." placeholder: | logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) logger = logging.getLogger(__name__) render: python - type: markdown attributes: value: "### Configuration" - type: input id: operating-system attributes: label: Operating System validations: required: true - type: textarea id: versions attributes: label: Version of Python, python-telegram-bot & dependencies description: Paste the output of `$ python -m telegram` here. This will be automatically formatted into code, so no need for backticks. render: shell validations: required: true python-telegram-bot-21.1.1/.github/dependabot.yml000066400000000000000000000004641460724040100216670ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" day: "friday" # Updates the dependencies of the GitHub Actions workflows - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" day: "friday" python-telegram-bot-21.1.1/.github/labeler.yml000066400000000000000000000003121460724040100211600ustar00rootroot00000000000000# Config file for workflows/labelling.yml version: 1 labels: - label: "dependencies" authors: ["dependabot[bot]", "pre-commit-ci[bot]"] - label: "code quality ✨" authors: ["pre-commit-ci[bot]"] python-telegram-bot-21.1.1/.github/pull_request_template.md000066400000000000000000000007771460724040100240070ustar00rootroot00000000000000 python-telegram-bot-21.1.1/.github/workflows/000077500000000000000000000000001460724040100210705ustar00rootroot00000000000000python-telegram-bot-21.1.1/.github/workflows/dependabot-prs.yml000066400000000000000000000020611460724040100245210ustar00rootroot00000000000000name: Process Dependabot PRs on: pull_request: types: [opened, reopened] jobs: process-dependabot-prs: permissions: pull-requests: read contents: write runs-on: ubuntu-latest if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} steps: - name: Fetch Dependabot metadata id: dependabot-metadata uses: dependabot/fetch-metadata@v2.0.0 - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} - name: Update Version Number in Other Files uses: jacobtomlinson/gha-find-replace@v3 with: find: ${{ steps.dependabot-metadata.outputs.previous-version }} replace: ${{ steps.dependabot-metadata.outputs.new-version }} regex: false exclude: CHANGES.rst - name: Commit & Push Changes to PR uses: EndBug/add-and-commit@v9.1.4 with: message: 'Update version number in other files' committer_name: GitHub Actions committer_email: 41898282+github-actions[bot]@users.noreply.github.com python-telegram-bot-21.1.1/.github/workflows/docs-linkcheck.yml000066400000000000000000000014721460724040100245000ustar00rootroot00000000000000name: Check Links in Documentation on: schedule: # First day of month at 05:46 in every 2nd month - cron: '46 5 1 */2 *' jobs: test-sphinx-build: name: test-sphinx-linkcheck runs-on: ${{matrix.os}} strategy: matrix: python-version: [3.9] os: [ubuntu-latest] fail-fast: False steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip python -W ignore -m pip install -r requirements-all.txt - name: Check Links run: sphinx-build docs/source docs/build/html -W --keep-going -j auto -b linkcheck python-telegram-bot-21.1.1/.github/workflows/docs.yml000066400000000000000000000026031460724040100225440ustar00rootroot00000000000000name: Test Documentation Build on: pull_request: paths: - telegram/** - docs/** push: branches: - master jobs: test-sphinx-build: name: test-sphinx-build runs-on: ${{matrix.os}} strategy: matrix: python-version: [3.9] os: [ubuntu-latest] fail-fast: False steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' cache-dependency-path: '**/requirements*.txt' - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip python -W ignore -m pip install -r requirements-all.txt - name: Test autogeneration of admonitions run: pytest -v --tb=short tests/docs/admonition_inserter.py - name: Build docs run: sphinx-build docs/source docs/build/html -W --keep-going -j auto - name: Upload docs uses: actions/upload-artifact@v4 with: name: HTML Docs retention-days: 7 path: | # Exclude the .doctrees folder and .buildinfo file from the artifact # since they are not needed and add to the size docs/build/html/* !docs/build/html/.doctrees !docs/build/html/.buildinfo python-telegram-bot-21.1.1/.github/workflows/labelling.yml000066400000000000000000000006331460724040100235460ustar00rootroot00000000000000name: PR Labeler on: pull_request: types: [opened] jobs: pre-commit-ci: permissions: contents: read # for srvaroa/labeler to read config file pull-requests: write # for srvaroa/labeler to add labels in PR runs-on: ubuntu-latest steps: - uses: srvaroa/labeler@v1.10.0 # Config file at .github/labeler.yml env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" python-telegram-bot-21.1.1/.github/workflows/lock.yml000066400000000000000000000006471460724040100225520ustar00rootroot00000000000000name: 'Lock Closed Threads' on: schedule: - cron: '8 4 * * *' jobs: lock: runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v5.0.1 with: github-token: ${{ github.token }} issue-inactive-days: '7' issue-lock-reason: '' pr-inactive-days: '7' pr-lock-reason: '' # Don't lock Discussions process-only: 'issues, prs' python-telegram-bot-21.1.1/.github/workflows/pre-commit_dependencies_notifier.yml000066400000000000000000000012071460724040100302740ustar00rootroot00000000000000name: Warning maintainers on: pull_request_target: paths: - requirements.txt - requirements-opts.txt - .pre-commit-config.yaml permissions: pull-requests: write jobs: job: runs-on: ubuntu-latest name: about pre-commit and dependency change steps: - name: running the check uses: Poolitzer/notifier-action@master with: notify-message: Hey! Looks like you edited the (optional) requirements or the pre-commit hooks. I'm just a friendly reminder to keep the additional dependencies for the hooks in sync with the requirements :) repo-token: ${{ secrets.GITHUB_TOKEN }} python-telegram-bot-21.1.1/.github/workflows/readme_notifier.yml000066400000000000000000000010211460724040100247410ustar00rootroot00000000000000name: Warning maintainers on: pull_request_target: paths: - README.rst - README_RAW.rst permissions: pull-requests: write jobs: job: runs-on: ubuntu-latest name: about readme change steps: - name: running the check uses: Poolitzer/notifier-action@master with: notify-message: Hey! Looks like you edited README.rst or README_RAW.rst. I'm just a friendly reminder to apply relevant changes to both of those files :) repo-token: ${{ secrets.GITHUB_TOKEN }} python-telegram-bot-21.1.1/.github/workflows/stale.yml000066400000000000000000000011631460724040100227240ustar00rootroot00000000000000name: 'Mark & close stale questions' on: schedule: - cron: '42 2 * * *' jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: # PRs never get stale days-before-stale: 3 days-before-close: 2 days-before-pr-stale: -1 stale-issue-label: 'stale' only-labels: 'question' stale-issue-message: '' close-issue-message: 'This issue has been automatically closed due to inactivity. Feel free to comment in order to reopen or ask again in our Telegram support group at https://t.me/pythontelegrambotgroup.' python-telegram-bot-21.1.1/.github/workflows/test_official.yml000066400000000000000000000026041460724040100244300ustar00rootroot00000000000000name: Bot API Tests on: pull_request: paths: - telegram/** - tests/** push: branches: - master schedule: # Run monday and friday morning at 03:07 - odd time to spread load on GitHub Actions - cron: '7 3 * * 1,5' jobs: check-conformity: name: check-conformity runs-on: ${{matrix.os}} strategy: matrix: python-version: [3.11] os: [ubuntu-latest] fail-fast: False steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip python -W ignore -m pip install -r requirements.txt python -W ignore -m pip install -r requirements-opts.txt python -W ignore -m pip install -r requirements-dev.txt - name: Compare to official api run: | pytest -v tests/test_official/test_official.py --junit-xml=.test_report_official.xml exit $? env: TEST_OFFICIAL: "true" shell: bash --noprofile --norc {0} - name: Test Summary id: test_summary uses: test-summary/action@v2.3 if: always() # always run, even if tests fail with: paths: .test_report_official.xml python-telegram-bot-21.1.1/.github/workflows/type_completeness.yml000066400000000000000000000056421460724040100253640ustar00rootroot00000000000000name: Check Type Completeness on: pull_request: paths: - telegram/** - requirements.txt - requirements-opts.txt push: branches: - master jobs: test-type-completeness: name: test-type-completeness runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: git fetch --depth=1 # https://github.com/actions/checkout/issues/329#issuecomment-674881489 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: 3.9 cache: 'pip' cache-dependency-path: '**/requirements*.txt' - name: Install Pyright run: | python -W ignore -m pip install pyright~=1.1.316 - name: Get PR Completeness # Must run before base completeness, as base completeness will checkout the base branch # And we can't go back to the PR branch after that in case the PR is coming from a fork run: | pip install . -U pyright --verifytypes telegram --ignoreexternal --outputjson > pr.json || true pyright --verifytypes telegram --ignoreexternal > pr.readable || true - name: Get Base Completeness run: | git checkout ${{ github.base_ref }} pip install . -U pyright --verifytypes telegram --ignoreexternal --outputjson > base.json || true - name: Compare Completeness uses: jannekem/run-python-script-action@v1 with: script: | import json import os from pathlib import Path base = float( json.load(open("base.json", "rb"))["typeCompleteness"]["completenessScore"] ) pr = float( json.load(open("pr.json", "rb"))["typeCompleteness"]["completenessScore"] ) base_text = f"This PR changes type completeness from {round(base, 3)} to {round(pr, 3)}." if base == 0: text = f"Something is broken in the workflow. Reported type completeness is 0. 💥" set_summary(text) print(Path("pr.readable").read_text(encoding="utf-8")) error(text) exit(1) if pr < (base - 0.001): text = f"{base_text} ❌" set_summary(text) print(Path("pr.readable").read_text(encoding="utf-8")) error(text) exit(1) elif pr > (base + 0.001): text = f"{base_text} ✨" set_summary(text) if pr < 1: print(Path("pr.readable").read_text(encoding="utf-8")) print(text) else: text = f"{base_text} This is less than 0.1 percentage points. ✅" set_summary(text) print(Path("pr.readable").read_text(encoding="utf-8")) print(text) python-telegram-bot-21.1.1/.github/workflows/unit_tests.yml000066400000000000000000000223551460724040100240230ustar00rootroot00000000000000name: Unit Tests on: pull_request: paths: - telegram/** - tests/** - requirements.txt - requirements-opts.txt push: branches: - master schedule: # Run monday and friday morning at 03:07 - odd time to spread load on GitHub Actions - cron: '7 3 * * 1,5' jobs: pytest: name: pytest runs-on: ${{matrix.os}} strategy: matrix: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: False steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' cache-dependency-path: '**/requirements*.txt' - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip python -W ignore -m pip install -U pytest-cov python -W ignore -m pip install -r requirements.txt python -W ignore -m pip install -r requirements-dev.txt python -W ignore -m pip install pytest-xdist[psutil] - name: Test with pytest # We run 4 different suites here # 1. Test just utils.datetime.py without pytz being installed # 2. Test just test_no_passport.py without passport dependencies being installed # 3. Test just test_rate_limiter.py without passport dependencies being installed # 4. Test everything else # The first & second one are achieved by mocking the corresponding import # See test_helpers.py & test_no_passport.py for details run: | # We test without optional dependencies first. This includes: # - without pytz # - without jobqueue # - without ratelimiter # - without webhooks # - without arbitrary callback data # - without socks support # - without http2 support TO_TEST="test_no_passport.py or test_datetime.py or test_defaults.py or test_jobqueue.py or test_applicationbuilder.py or test_ratelimiter.py or test_updater.py or test_callbackdatacache.py or test_request.py" pytest -v --cov -k "${TO_TEST}" # Rerun only failed tests (--lf), and don't run any tests if none failed (--lfnf=none) pytest -v --cov --cov-append -k "${TO_TEST}" --lf --lfnf=none --junit-xml=.test_report_no_optionals.xml # No tests were selected, convert returned status code to 0 opt_dep_status=$(( $? == 5 ? 0 : $? )) # Test the rest export TEST_WITH_OPT_DEPS='true' pip install -r requirements-opts.txt # `-n auto --dist loadfile` uses pytest-xdist to run each test file on a different CPU # worker. Increasing number of workers has little effect on test duration, but it seems # to increase flakyness, specially on python 3.7 with --dist=loadgroup. pytest -v --cov --cov-append -n auto --dist loadfile pytest -v --cov --cov-append -n auto --dist loadfile --lf --lfnf=none --junit-xml=.test_report_optionals.xml main_status=$(( $? == 5 ? 0 : $? )) # exit with non-zero status if any of the two pytest runs failed exit $(( ${opt_dep_status} || ${main_status} )) env: JOB_INDEX: ${{ strategy.job-index }} BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJuYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEzOTA5ODM5OTciLCAidXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8yN19ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE3MTA4NTA4MjIifSwgeyJ0b2tlbiI6ICI2NzE0Njg4ODY6QUFHUEdmY2lSSUJVTkZlODI0dUlWZHE3SmUzX1luQVROR3ciLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpaR1l3T1Rsa016TXhOMlkyIiwgIm5hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ0NjAyMjUyMiIsICJ1c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM0X2JvdCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTg5MTQ0MTc5MSJ9LCB7InRva2VuIjogIjYyOTMyNjUzODpBQUZSclpKckI3b0IzbXV6R3NHSlhVdkdFNUNRek01Q1U0byIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk1tTTVZV0poWXpreE0yVTEiLCAibmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy41IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDk2OTE3NzUwIiwgInVzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90IiwgImZvcnVtX2dyb3VwX2lkIjogIi0xMDAxNTc3NTA0Nzg3In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJuYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjYiLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEzMzM4NzE0NjEiLCAidXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNl9ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE4Njc5MDExNzIifSwgeyJ0b2tlbiI6ICI2OTUxMDQwODg6QUFIZnp5bElPalNJSVMtZU9uSTIweTJFMjBIb2RIc2Z6LTAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpPR1ExTURnd1pqSXdaakZsIiwgIm5hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ3ODI5MzcxNCIsICJ1c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM3X2JvdCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTg2NDA1NDg3OSJ9LCB7InRva2VuIjogIjY5MTQyMzU1NDpBQUY4V2tqQ1pibkhxUF9pNkdoVFlpckZFbFpyR2FZT2hYMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllqYzVOVGhpTW1ReU1XVmgiLCAibmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMi43IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzYzOTMyNTczIiwgInVzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90IiwgImZvcnVtX2dyb3VwX2lkIjogIi0xMDAxODY3ODU1OTM2In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJuYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAzLjUiLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDE0MDc4MzY2MDUiLCAidXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfcHlweV8zNV9ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE1NTg5OTAyODIifSwgeyJ0b2tlbiI6ICI2OTAwOTEzNDc6QUFGTG1SNXBBQjVZY3BlX21PaDd6TTRKRkJPaDB6M1QwVG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpaRGhsTnpFNU1Ea3dZV0ppIiwgIm5hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjc5NjAwMDI2IiwgInVzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8zNF9ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE3MjU2OTEzODcifSwgeyJ0b2tlbiI6ICI2OTQzMDgwNTI6QUFFQjJfc29uQ2s1NUxZOUJHOUFPLUg4anhpUFM1NW9vQkEiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZbVppWVdabU1qSmhaR015IiwgIm5hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMi43IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjkzMDc5MTY1IiwgInVzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE1NjU4NTU5ODcifSwgeyJ0b2tlbiI6ICIxMDU1Mzk3NDcxOkFBRzE4bkJfUzJXQXd1SjNnN29oS0JWZ1hYY2VNbklPeVNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpBd056QXpZalZpTkdOayIsICJuYW1lIjogIlBUQiB0ZXN0cyBbMF0iLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDExODU1MDk2MzYiLCAidXNlcm5hbWUiOiAicHRiXzBfYm90IiwgImZvcnVtX2dyb3VwX2lkIjogIi0xMDAxODE5MDM3MzExIn0sIHsidG9rZW4iOiAiMTA0NzMyNjc3MTpBQUY4bk90ODFGcFg4bGJidno4VWV3UVF2UmZUYkZmQnZ1SSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllUVTFOVEk0WkdSallqbGkiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzFdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDg0Nzk3NjEyIiwgInVzZXJuYW1lIjogInB0Yl8xX2JvdCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTc5NzMwODQ0NCJ9LCB7InRva2VuIjogIjk3MTk5Mjc0NTpBQUdPa09hVzBOSGpnSXY1LTlqUWJPajR2R3FkaFNGLVV1cyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk5XWmtNV1ZoWWpsallqVTUiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzJdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDAyMjU1MDcwIiwgInVzZXJuYW1lIjogInB0Yl8yX2JvdCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTUyMzU3NTA3MiJ9LCB7InRva2VuIjogIjU1MTg2NDU0MTE6QUFHdzBxaEs3ZTRHbmoxWjJjc1BBQzdaYWtvTWs1NkVKZmsiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNRE0wT1RCbE9UUXpNVEU1IiwgIm5hbWUiOiAiUFRCIFRlc3QgQm90IFszXSIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTgwMzgxMDE5NiIsICJ1c2VybmFtZSI6ICJwdGJfdGVzdF8wM19ib3QiLCAiZm9ydW1fZ3JvdXBfaWQiOiAiLTEwMDE2MTk2NzMyNjEifSwgeyJ0b2tlbiI6ICI1NzM3MDE4MzU2OkFBSDEzOFN1aUtRRjBMRENXc2ZnV2VYZmpKNWQ2M2tDV0xBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TjJWaVpqUmxaak01TlRNdyIsICJuYW1lIjogIlBUQiBUZXN0IEJvdCBbNF0iLCAidXNlcm5hbWUiOiAicHRiX3Rlc3RfMDRfYm90IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxODQyNDM5NjQxIiwgImZvcnVtX2dyb3VwX2lkIjogIi0xMDAxODQyOTk2MTk5In0sIHsidG9rZW4iOiAiNTc0NDY0NDUyMjpBQUVBZHNyRjBoQzZwNkhVTzBQMDFROGJfakNoVTUyWEctTSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpqSmtZVGd5TmpnMlpHRTAiLCAibmFtZSI6ICJQVEIgVGVzdCBCb3QgWzVdIiwgInVzZXJuYW1lIjogInB0Yl90ZXN0XzA1X2JvdCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTg1NTM2MDk4NiIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTY0NDM2NjkwMiJ9XQ== TEST_WITH_OPT_DEPS : "false" TEST_BUILD: "true" shell: bash --noprofile --norc {0} - name: Test Summary id: test_summary uses: test-summary/action@v2.3 if: always() # always run, even if tests fail with: paths: | .test_report_no_optionals.xml .test_report_optionals.xml - name: Submit coverage uses: codecov/codecov-action@v4 with: env_vars: OS,PYTHON name: ${{ matrix.os }}-${{ matrix.python-version }} fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} python-telegram-bot-21.1.1/.gitignore000066400000000000000000000021651460724040100174670ustar00rootroot00000000000000# 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* python-telegram-bot-21.1.1/.pre-commit-config.yaml000066400000000000000000000041141460724040100217540ustar00rootroot00000000000000# Make sure that the additional_dependencies here match requirements(-opts).txt ci: autofix_prs: false autoupdate_schedule: monthly repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: 'v0.3.5' hooks: - id: ruff name: ruff additional_dependencies: - httpx~=0.27 - tornado~=6.4 - APScheduler~=3.10.4 - cachetools~=5.3.3 - aiolimiter~=1.1.0 - repo: https://github.com/psf/black-pre-commit-mirror rev: 24.3.0 hooks: - id: black args: - --diff - --check - repo: https://github.com/PyCQA/flake8 rev: 7.0.0 hooks: - id: flake8 - repo: https://github.com/PyCQA/pylint rev: v3.1.0 hooks: - id: pylint files: ^(?!(tests|docs)).*\.py$ additional_dependencies: - httpx~=0.27 - tornado~=6.4 - APScheduler~=3.10.4 - cachetools~=5.3.3 - aiolimiter~=1.1.0 - . # this basically does `pip install -e .` - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.9.0 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 - 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 - . # this basically does `pip install -e .` - repo: https://github.com/asottile/pyupgrade rev: v3.15.2 hooks: - id: pyupgrade args: - --py38-plus - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort name: isort args: - --diff - --check python-telegram-bot-21.1.1/.readthedocs.yml000066400000000000000000000040361460724040100205640ustar00rootroot00000000000000# .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' python-telegram-bot-21.1.1/AUTHORS.rst000066400000000000000000000136071460724040100173610ustar00rootroot00000000000000Credits ======= ``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 `_ - `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 `_ - `naveenvhegde `_ - `neurrone `_ - `NikitaPirate `_ - `Nikolai Krivenko `_ - `njittam `_ - `Noam Meltzer `_ - `Oleg Shlyazhko `_ - `Oleg Sushchenko `_ - `Or Bin `_ - `overquota `_ - `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. python-telegram-bot-21.1.1/CHANGES.rst000066400000000000000000002476761460724040100173230ustar00rootroot00000000000000.. _ptb-changelog: ========= Changelog ========= 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 python-telegram-bot-21.1.1/CODE_OF_CONDUCT.rst000066400000000000000000000065721460724040100205140ustar00rootroot00000000000000==================================== 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 `_. python-telegram-bot-21.1.1/LICENSE000066400000000000000000000772461460724040100165200ustar00rootroot00000000000000 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. python-telegram-bot-21.1.1/LICENSE.dual000066400000000000000000001164001460724040100174260ustar00rootroot00000000000000 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. python-telegram-bot-21.1.1/LICENSE.lesser000066400000000000000000000167431460724040100200070ustar00rootroot00000000000000 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. python-telegram-bot-21.1.1/MANIFEST.in000066400000000000000000000001471460724040100172330ustar00rootroot00000000000000include LICENSE LICENSE.lesser requirements.txt requirements-opts.txt README_RAW.rst telegram/py.typed python-telegram-bot-21.1.1/README.rst000066400000000000000000000306521460724040100171700ustar00rootroot00000000000000.. Make sure to apply any changes to this file to README_RAW.rst as well! .. image:: https://raw.githubusercontent.com/python-telegram-bot/logos/master/logo-text/png/ptb-logo-text_768.png :align: center :target: https://python-telegram-bot.org :alt: python-telegram-bot Logo .. image:: https://img.shields.io/pypi/v/python-telegram-bot.svg :target: https://pypi.org/project/python-telegram-bot/ :alt: PyPi Package Version .. image:: https://img.shields.io/pypi/pyversions/python-telegram-bot.svg :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions .. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions .. image:: https://img.shields.io/pypi/dm/python-telegram-bot :target: https://pypistats.org/packages/python-telegram-bot :alt: PyPi Package Monthly Download .. image:: https://readthedocs.org/projects/python-telegram-bot/badge/?version=stable :target: https://docs.python-telegram-bot.org/en/stable/ :alt: Documentation Status .. image:: https://img.shields.io/pypi/l/python-telegram-bot.svg :target: https://www.gnu.org/licenses/lgpl-3.0.html :alt: LGPLv3 License .. image:: https://github.com/python-telegram-bot/python-telegram-bot/actions/workflows/unit_tests.yml/badge.svg?branch=master :target: https://github.com/python-telegram-bot/python-telegram-bot/ :alt: Github Actions workflow .. image:: https://codecov.io/gh/python-telegram-bot/python-telegram-bot/branch/master/graph/badge.svg :target: https://app.codecov.io/gh/python-telegram-bot/python-telegram-bot :alt: Code coverage .. image:: https://isitmaintained.com/badge/resolution/python-telegram-bot/python-telegram-bot.svg :target: https://isitmaintained.com/project/python-telegram-bot/python-telegram-bot :alt: Median time to resolve an issue .. image:: https://api.codacy.com/project/badge/Grade/99d901eaa09b44b4819aec05c330c968 :target: https://app.codacy.com/gh/python-telegram-bot/python-telegram-bot/dashboard :alt: Code quality: Codacy .. image:: https://results.pre-commit.ci/badge/github/python-telegram-bot/python-telegram-bot/master.svg :target: https://results.pre-commit.ci/latest/github/python-telegram-bot/python-telegram-bot/master :alt: pre-commit.ci status .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code Style: Black .. image:: https://img.shields.io/badge/Telegram-Channel-blue.svg?logo=telegram :target: https://t.me/pythontelegrambotchannel :alt: Telegram Channel .. image:: https://img.shields.io/badge/Telegram-Group-blue.svg?logo=telegram :target: https://telegram.me/pythontelegrambotgroup :alt: Telegram Group We have made you a wrapper you can't refuse We have a vibrant community of developers helping each other in our `Telegram group `_. Join us! *Stay tuned for library updates and new releases on our* `Telegram Channel `_. Introduction ============ This library provides a pure Python, asynchronous interface for the `Telegram Bot API `_. It's compatible with Python versions **3.8+**. In addition to the pure API implementation, this library features a number of high-level classes to make the development of bots easy and straightforward. These classes are contained in the ``telegram.ext`` submodule. A pure API implementation *without* ``telegram.ext`` is available as the standalone package ``python-telegram-bot-raw``. `See here for details. `_ Note ---- Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conjunction will result in undesired side-effects, so only install *one* of both. Telegram API support ==================== All types and methods of the Telegram Bot API **7.2** are supported. Installing ========== You can install or upgrade ``python-telegram-bot`` via .. code:: shell $ pip install python-telegram-bot --upgrade To install a pre-release, use the ``--pre`` `flag `_ in addition. You can also install ``python-telegram-bot`` from source, though this is usually not necessary. .. code:: shell $ git clone https://github.com/python-telegram-bot/python-telegram-bot $ cd python-telegram-bot $ python setup.py install Verifying Releases ------------------ We sign all the releases with a GPG key. The signatures are uploaded to both the `GitHub releases page `_ and the `PyPI project `_ and end with a suffix ``.asc``. Please find the public keys `here `_. The keys are named in the format ``-.gpg`` or ``-current.gpg`` if the key is currently being used for new releases. In addition, the GitHub release page also contains the sha1 hashes of the release files in the files with the suffix ``.sha1``. This allows you to verify that a release file that you downloaded was indeed provided by the ``python-telegram-bot`` team. Dependencies & Their Versions ----------------------------- ``python-telegram-bot`` tries to use as few 3rd party dependencies as possible. However, for some features using a 3rd party library is more sane than implementing the functionality again. As these features are *optional*, the corresponding 3rd party dependencies are not installed by default. Instead, they are listed as optional dependencies. This allows to avoid unnecessary dependency conflicts for users who don't need the optional features. The only required dependency is `httpx ~= 0.27 `_ for ``telegram.request.HTTPXRequest``, the default networking backend. ``python-telegram-bot`` is most useful when used along with additional libraries. To minimize dependency conflicts, we try to be liberal in terms of version requirements on the (optional) dependencies. On the other hand, we have to ensure stability of ``python-telegram-bot``, which is why we do apply version bounds. If you encounter dependency conflicts due to these bounds, feel free to reach out. Optional Dependencies ##################### PTB can be installed with optional dependencies: * ``pip install "python-telegram-bot[passport]"`` installs the `cryptography>=39.0.1 `_ library. Use this, if you want to use Telegram Passport related functionality. * ``pip install "python-telegram-bot[socks]"`` installs `httpx[socks] `_. Use this, if you want to work behind a Socks5 server. * ``pip install "python-telegram-bot[http2]"`` installs `httpx[http2] `_. Use this, if you want to use HTTP/2. * ``pip install "python-telegram-bot[rate-limiter]"`` installs `aiolimiter~=1.1.0 `_. Use this, if you want to use ``telegram.ext.AIORateLimiter``. * ``pip install "python-telegram-bot[webhooks]"`` installs the `tornado~=6.4 `_ library. Use this, if you want to use ``telegram.ext.Updater.start_webhook``/``telegram.ext.Application.run_webhook``. * ``pip install "python-telegram-bot[callback-data]"`` installs the `cachetools~=5.3.3 `_ library. Use this, if you want to use `arbitrary callback_data `_. * ``pip install "python-telegram-bot[job-queue]"`` installs the `APScheduler~=3.10.4 `_ library and enforces `pytz>=2018.6 `_, where ``pytz`` is a dependency of ``APScheduler``. Use this, if you want to use the ``telegram.ext.JobQueue``. To install multiple optional dependencies, separate them by commas, e.g. ``pip install "python-telegram-bot[socks,webhooks]"``. Additionally, two shortcuts are provided: * ``pip install "python-telegram-bot[all]"`` installs all optional dependencies. * ``pip install "python-telegram-bot[ext]"`` installs all optional dependencies that are related to ``telegram.ext``, i.e. ``[rate-limiter, webhooks, callback-data, job-queue]``. Quick Start =========== Our Wiki contains an `Introduction to the API `_ explaining how the pure Bot API can be accessed via ``python-telegram-bot``. Moreover, the `Tutorial: Your first Bot `_ gives an introduction on how chatbots can be easily programmed with the help of the ``telegram.ext`` module. Resources ========= - The `package documentation `_ is the technical reference for ``python-telegram-bot``. It contains descriptions of all available classes, modules, methods and arguments as well as the `changelog `_. - The `wiki `_ is home to number of more elaborate introductions of the different features of ``python-telegram-bot`` and other useful resources that go beyond the technical documentation. - Our `examples section `_ contains several examples that showcase the different features of both the Bot API and ``python-telegram-bot``. Even if it is not your approach for learning, please take a look at ``echobot.py``. It is the de facto base for most of the bots out there. The code for these examples is released to the public domain, so you can start by grabbing the code and building on top of it. - The `official Telegram Bot API documentation `_ is of course always worth a read. Getting help ============ If the resources mentioned above don't answer your questions or simply overwhelm you, there are several ways of getting help. 1. We have a vibrant community of developers helping each other in our `Telegram group `_. Join us! Asking a question here is often the quickest way to get a pointer in the right direction. 2. Ask questions by opening `a discussion `_. 3. You can even ask for help on Stack Overflow using the `python-telegram-bot tag `_. Concurrency =========== Since v20.0, ``python-telegram-bot`` is built on top of Pythons ``asyncio`` module. Because ``asyncio`` is in general single-threaded, ``python-telegram-bot`` does currently not aim to be thread-safe. Noteworthy parts of ``python-telegram-bots`` API that are likely to cause issues (e.g. race conditions) when used in a multi-threaded setting include: * ``telegram.ext.Application/Updater.update_queue`` * ``telegram.ext.ConversationHandler.check/handle_update`` * ``telegram.ext.CallbackDataCache`` * ``telegram.ext.BasePersistence`` * all classes in the ``telegram.ext.filters`` module that allow to add/remove allowed users/chats at runtime Contributing ============ Contributions of all sizes are welcome. Please review our `contribution guidelines `_ to get started. You can also help by `reporting bugs or feature requests `_. Donating ======== Occasionally we are asked if we accept donations to support the development. While we appreciate the thought, maintaining PTB is our hobby, and we have almost no running costs for it. We therefore have nothing set up to accept donations. If you still want to donate, we kindly ask you to donate to another open source project/initiative of your choice instead. License ======= You may copy, distribute and modify the software provided that modifications are described and licensed for free under `LGPL-3 `_. Derivatives works (including modifications or anything statically linked to the library) can only be redistributed under LGPL-3, but applications that use the library don't have to be. python-telegram-bot-21.1.1/README_RAW.rst000066400000000000000000000251441460724040100177010ustar00rootroot00000000000000.. Make sure to apply any changes to this file to README.rst as well! .. image:: https://github.com/python-telegram-bot/logos/blob/master/logo-text/png/ptb-raw-logo-text_768.png?raw=true :align: center :target: https://python-telegram-bot.org :alt: python-telegram-bot-raw Logo .. image:: https://img.shields.io/pypi/v/python-telegram-bot-raw.svg :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: PyPi Package Version .. image:: https://img.shields.io/pypi/pyversions/python-telegram-bot-raw.svg :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions .. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions .. image:: https://img.shields.io/pypi/dm/python-telegram-bot-raw :target: https://pypistats.org/packages/python-telegram-bot-raw :alt: PyPi Package Monthly Download .. image:: https://readthedocs.org/projects/python-telegram-bot/badge/?version=stable :target: https://docs.python-telegram-bot.org/ :alt: Documentation Status .. image:: https://img.shields.io/pypi/l/python-telegram-bot-raw.svg :target: https://www.gnu.org/licenses/lgpl-3.0.html :alt: LGPLv3 License .. image:: https://github.com/python-telegram-bot/python-telegram-bot/actions/workflows/unit_tests.yml/badge.svg?branch=master :target: https://github.com/python-telegram-bot/python-telegram-bot/ :alt: Github Actions workflow .. image:: https://codecov.io/gh/python-telegram-bot/python-telegram-bot/branch/master/graph/badge.svg :target: https://app.codecov.io/gh/python-telegram-bot/python-telegram-bot :alt: Code coverage .. image:: https://isitmaintained.com/badge/resolution/python-telegram-bot/python-telegram-bot.svg :target: https://isitmaintained.com/project/python-telegram-bot/python-telegram-bot :alt: Median time to resolve an issue .. image:: https://api.codacy.com/project/badge/Grade/99d901eaa09b44b4819aec05c330c968 :target: https://app.codacy.com/gh/python-telegram-bot/python-telegram-bot/dashboard :alt: Code quality: Codacy .. image:: https://results.pre-commit.ci/badge/github/python-telegram-bot/python-telegram-bot/master.svg :target: https://results.pre-commit.ci/latest/github/python-telegram-bot/python-telegram-bot/master :alt: pre-commit.ci status .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code Style: Black .. image:: https://img.shields.io/badge/Telegram-Channel-blue.svg?logo=telegram :target: https://t.me/pythontelegrambotchannel :alt: Telegram Channel .. image:: https://img.shields.io/badge/Telegram-Group-blue.svg?logo=telegram :target: https://telegram.me/pythontelegrambotgroup :alt: Telegram Group We have made you a wrapper you can't refuse We have a vibrant community of developers helping each other in our `Telegram group `_. Join us! *Stay tuned for library updates and new releases on our* `Telegram Channel `_. Introduction ============ This library provides a pure Python, asynchronous interface for the `Telegram Bot API `_. It's compatible with Python versions **3.8+**. ``python-telegram-bot-raw`` is part of the `python-telegram-bot `_ ecosystem and provides the pure API functionality extracted from PTB. It therefore does not have independent release schedules, changelogs or documentation. Note ---- Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conjunction will result in undesired side-effects, so only install *one* of both. Telegram API support ==================== All types and methods of the Telegram Bot API **7.2** are supported. Installing ========== You can install or upgrade ``python-telegram-bot`` via .. code:: shell $ pip install python-telegram-bot-raw --upgrade To install a pre-release, use the ``--pre`` `flag `_ in addition. You can also install ``python-telegram-bot-raw`` from source, though this is usually not necessary. .. code:: shell $ git clone https://github.com/python-telegram-bot/python-telegram-bot $ cd python-telegram-bot $ python setup_raw.py install Note ---- Installing the ``.tar.gz`` archive available on PyPi directly via ``pip`` will *not* work as expected, as ``pip`` does not recognize that it should use ``setup_raw.py`` instead of ``setup.py``. Verifying Releases ------------------ We sign all the releases with a GPG key. The signatures are uploaded to both the `GitHub releases page `_ and the `PyPI project `_ and end with a suffix ``.asc``. Please find the public keys `here `_. The keys are named in the format ``-.gpg`` or ``-current.gpg`` if the key is currently being used for new releases. In addition, the GitHub release page also contains the sha1 hashes of the release files in the files with the suffix ``.sha1``. This allows you to verify that a release file that you downloaded was indeed provided by the ``python-telegram-bot`` team. Dependencies & Their Versions ----------------------------- ``python-telegram-bot`` tries to use as few 3rd party dependencies as possible. However, for some features using a 3rd party library is more sane than implementing the functionality again. As these features are *optional*, the corresponding 3rd party dependencies are not installed by default. Instead, they are listed as optional dependencies. This allows to avoid unnecessary dependency conflicts for users who don't need the optional features. The only required dependency is `httpx ~= 0.27 `_ for ``telegram.request.HTTPXRequest``, the default networking backend. ``python-telegram-bot`` is most useful when used along with additional libraries. To minimize dependency conflicts, we try to be liberal in terms of version requirements on the (optional) dependencies. On the other hand, we have to ensure stability of ``python-telegram-bot``, which is why we do apply version bounds. If you encounter dependency conflicts due to these bounds, feel free to reach out. Optional Dependencies ##################### PTB can be installed with optional dependencies: * ``pip install "python-telegram-bot-raw[passport]"`` installs the `cryptography>=39.0.1 `_ library. Use this, if you want to use Telegram Passport related functionality. * ``pip install "python-telegram-bot-raw[socks]"`` installs `httpx[socks] `_. Use this, if you want to work behind a Socks5 server. * ``pip install "python-telegram-bot-raw[http2]"`` installs `httpx[http2] `_. Use this, if you want to use HTTP/2. To install multiple optional dependencies, separate them by commas, e.g. ``pip install "python-telegram-bot-raw[passport,socks]"``. Additionally, the shortcut ``pip install "python-telegram-bot-raw[all]"`` installs all optional dependencies. Quick Start =========== Our Wiki contains an `Introduction to the API `_ explaining how the pure Bot API can be accessed via ``python-telegram-bot``. Resources ========= - The `package documentation `_ is the technical reference for ``python-telegram-bot``. It contains descriptions of all available classes, modules, methods and arguments as well as the `changelog `_. - The `wiki `_ is home to number of more elaborate introductions of the different features of ``python-telegram-bot`` and other useful resources that go beyond the technical documentation. - Our `examples section `_ contains several examples that showcase the different features of both the Bot API and ``python-telegram-bot``. Even if it is not your approach for learning, please take a look at ``echobot.py``. It is the de facto base for most of the bots out there. The code for these examples is released to the public domain, so you can start by grabbing the code and building on top of it. - The `official Telegram Bot API documentation `_ is of course always worth a read. Getting help ============ If the resources mentioned above don't answer your questions or simply overwhelm you, there are several ways of getting help. 1. We have a vibrant community of developers helping each other in our `Telegram group `_. Join us! Asking a question here is often the quickest way to get a pointer in the right direction. 2. Ask questions by opening `a discussion `_. 3. You can even ask for help on Stack Overflow using the `python-telegram-bot tag `_. Concurrency =========== Since v20.0, ``python-telegram-bot`` is built on top of Pythons ``asyncio`` module. Because ``asyncio`` is in general single-threaded, ``python-telegram-bot`` does currently not aim to be thread-safe. Contributing ============ Contributions of all sizes are welcome. Please review our `contribution guidelines `_ to get started. You can also help by `reporting bugs or feature requests `_. Donating ======== Occasionally we are asked if we accept donations to support the development. While we appreciate the thought, maintaining PTB is our hobby, and we have almost no running costs for it. We therefore have nothing set up to accept donations. If you still want to donate, we kindly ask you to donate to another open source project/initiative of your choice instead. License ======= You may copy, distribute and modify the software provided that modifications are described and licensed for free under `LGPL-3 `_. Derivatives works (including modifications or anything statically linked to the library) can only be redistributed under LGPL-3, but applications that use the library don't have to be. python-telegram-bot-21.1.1/codecov.yml000066400000000000000000000004031460724040100176350ustar00rootroot00000000000000comment: 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% python-telegram-bot-21.1.1/contrib/000077500000000000000000000000001460724040100171335ustar00rootroot00000000000000python-telegram-bot-21.1.1/contrib/build-debian.sh000077500000000000000000000001261460724040100220100ustar00rootroot00000000000000#!/bin/bash cp -R contrib/debian . debuild -us -uc debian/rules clean rm -rf debian python-telegram-bot-21.1.1/contrib/debian/000077500000000000000000000000001460724040100203555ustar00rootroot00000000000000python-telegram-bot-21.1.1/contrib/debian/changelog000066400000000000000000000002461460724040100222310ustar00rootroot00000000000000telegram (12.0.0b1) unstable; urgency=medium * Debian packaging; * Initial Release. -- Marco Marinello Thu, 22 Aug 2019 20:36:47 +0200 python-telegram-bot-21.1.1/contrib/debian/compat000066400000000000000000000000031460724040100215540ustar00rootroot0000000000000011 python-telegram-bot-21.1.1/contrib/debian/control000066400000000000000000000017521460724040100217650ustar00rootroot00000000000000Source: telegram Section: utils Priority: optional Maintainer: Marco Marinello Build-Depends: debhelper (>= 11), dh-python, python3-all, python3-setuptools Standards-Version: 4.1.3 Homepage: https://python-telegram-bot.org X-Python-Version: >= 3.2 Vcs-Browser: https://github.com/python-telegram-bot/python-telegram-bot Vcs-Git: https://github.com/python-telegram-bot/python-telegram-bot.git Package: python3-telegram-bot Architecture: any Depends: ${python3:Depends}, ${misc:Depends} Description: We have made you a wrapper you can't refuse! The Python Telegram bot (Python 3) This library provides a pure Python interface for the Telegram Bot API. It's compatible with Python versions 3.5+ and PyPy. . In addition to the pure API implementation, this library features a number of high-level classes to make the development of bots easy and straightforward. These classes are contained in the telegram.ext submodule. . This package installs the library for Python 3. python-telegram-bot-21.1.1/contrib/debian/copyright000066400000000000000000000021521460724040100223100ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: telegram Source: https://github.com/python-telegram-bot/python-telegram-bot Files: * Copyright: 2019 Leandro Toledo 2019 see AUTHORS file License: LGPLv3 Files: debian/* Copyright: 2019 Marco Marinello License: GPL-3.0+ License: GPL-3.0+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. . This package 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 General Public License for more details. . You should have received a copy of the GNU General Public License along with this program. If not, see . . On Debian systems, the complete text of the GNU General Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". python-telegram-bot-21.1.1/contrib/debian/install000066400000000000000000000000601460724040100217420ustar00rootroot00000000000000AUTHORS.rst /usr/share/doc/python3-telegram-bot python-telegram-bot-21.1.1/contrib/debian/rules000077500000000000000000000011171460724040100214350ustar00rootroot00000000000000#!/usr/bin/make -f # See debhelper(7) (uncomment to enable) # output every command that modifies files on the build system. #export DH_VERBOSE = 1 export PYBUILD_NAME=telegram %: DEB_BUILD_OPTIONS=nocheck dh $@ --with python3 --buildsystem=pybuild # If you need to rebuild the Sphinx documentation # Add spinxdoc to the dh --with line #override_dh_auto_build: # dh_auto_build # PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bhtml docs/ build/html # HTML generator # PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bman docs/ build/man # Manpage generator python-telegram-bot-21.1.1/contrib/debian/source/000077500000000000000000000000001460724040100216555ustar00rootroot00000000000000python-telegram-bot-21.1.1/contrib/debian/source/format000066400000000000000000000000151460724040100230640ustar00rootroot000000000000003.0 (native) python-telegram-bot-21.1.1/contrib/debian/source/options000066400000000000000000000000521460724040100232700ustar00rootroot00000000000000extend-diff-ignore = "^[^/]*[.]egg-info/" python-telegram-bot-21.1.1/docs/000077500000000000000000000000001460724040100164235ustar00rootroot00000000000000python-telegram-bot-21.1.1/docs/Makefile000066400000000000000000000165021460724040100200670ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -j auto # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." rebuild: clean html dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonTelegramBot.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonTelegramBot.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonTelegramBot" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonTelegramBot" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."python-telegram-bot-21.1.1/docs/__init__.py000066400000000000000000000000001460724040100205220ustar00rootroot00000000000000python-telegram-bot-21.1.1/docs/auxil/000077500000000000000000000000001460724040100175455ustar00rootroot00000000000000python-telegram-bot-21.1.1/docs/auxil/__init__.py000066400000000000000000000000001460724040100216440ustar00rootroot00000000000000python-telegram-bot-21.1.1/docs/auxil/admonition_inserter.py000066400000000000000000000656201460724040100242040ustar00rootroot00000000000000# # 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 collections.abc import inspect import re import typing from collections import defaultdict from typing import Any, Iterator, Union import telegram import telegram.ext def _iter_own_public_methods(cls: type) -> Iterator[tuple[str, type]]: """Iterates over methods of a class that are not protected/private, not camelCase and not inherited from the parent class. Returns pairs of method names and methods. This function is defined outside the class because it is used to create class constants. """ return ( m for m in inspect.getmembers(cls, predicate=inspect.isfunction) # not .ismethod if not m[0].startswith("_") and m[0].islower() # to avoid camelCase methods and m[0] in cls.__dict__ # method is not inherited from parent class ) class AdmonitionInserter: """Class for inserting admonitions into docs of Telegram classes.""" CLASS_ADMONITION_TYPES = ("use_in", "available_in", "returned_in") METHOD_ADMONITION_TYPES = ("shortcuts",) ALL_ADMONITION_TYPES = CLASS_ADMONITION_TYPES + METHOD_ADMONITION_TYPES FORWARD_REF_PATTERN = re.compile(r"^ForwardRef\('(?P\w+)'\)$") """ A pattern to find a class name in a ForwardRef typing annotation. Class name (in a named group) is surrounded by parentheses and single quotes. Note that since we're analyzing argument by argument, the pattern can be strict, with start and end markers. """ FORWARD_REF_SKIP_PATTERN = re.compile(r"^ForwardRef\('DefaultValue\[\w+]'\)$") """A pattern that will be used to skip known ForwardRef's that need not be resolved to a Telegram class, e.g.: ForwardRef('DefaultValue[None]') ForwardRef('DefaultValue[DVValueType]') """ METHOD_NAMES_FOR_BOT_AND_APPBUILDER: typing.ClassVar[dict[type, str]] = { cls: tuple(m[0] for m in _iter_own_public_methods(cls)) # m[0] means we take only names for cls in (telegram.Bot, telegram.ext.ApplicationBuilder) } """A dictionary mapping Bot and ApplicationBuilder classes to their relevant methods that will be mentioned in 'Returned in' and 'Use in' admonitions in other classes' docstrings. Methods must be public, not aliases, not inherited from TelegramObject. """ def __init__(self): self.admonitions: dict[str, dict[Union[type, collections.abc.Callable], str]] = { # dynamically determine which method to use to create a sub-dictionary admonition_type: getattr(self, f"_create_{admonition_type}")() for admonition_type in self.ALL_ADMONITION_TYPES } """Dictionary with admonitions. Contains sub-dictionaries, one per admonition type. Each sub-dictionary matches bot methods (for "Shortcuts") or telegram classes (for other admonition types) to texts of admonitions, e.g.: ``` { "use_in": {: <"Use in" admonition for ChatInviteLink>, ...}, "available_in": {: <"Available in" admonition">, ...}, "returned_in": {...} } ``` """ def insert_admonitions( self, obj: Union[type, collections.abc.Callable], docstring_lines: list[str], ): """Inserts admonitions into docstring lines for a given class or method. **Modifies lines in place**. """ # A better way would be to copy the lines and return them, but that will not work with # docs.auxil.sphinx_hooks.autodoc_process_docstring() for admonition_type in self.ALL_ADMONITION_TYPES: # If there is no admonition of the given type for the given class or method, # continue to the next admonition type, maybe the class/method is listed there. if obj not in self.admonitions[admonition_type]: continue insert_idx = self._find_insert_pos_for_admonition(docstring_lines) admonition_lines = self.admonitions[admonition_type][obj].splitlines() for idx in range(insert_idx, insert_idx + len(admonition_lines)): docstring_lines.insert(idx, admonition_lines[idx - insert_idx]) def _create_available_in(self) -> dict[type, str]: """Creates a dictionary with 'Available in' admonitions for classes that are available in attributes of other classes. """ # Generate a mapping of classes to ReST links to attributes in other classes that # correspond to instances of a given class # i.e. {telegram._files.sticker.Sticker: {":attr:`telegram.Message.sticker`", ...}} attrs_for_class = defaultdict(set) # The following regex is supposed to capture a class name in a line like this: # media (:obj:`str` | :class:`telegram.InputFile`): Audio file to send. # # Note that even if such typing description spans over multiple lines but each line ends # with a backslash (otherwise Sphinx will throw an error) # (e.g. EncryptedPassportElement.data), then Sphinx will combine these lines into a single # line automatically, and it will contain no backslash (only some extra many whitespaces # from the indentation). attr_docstr_pattern = re.compile( r"^\s*(?P[a-z_]+)" # Any number of spaces, named group for attribute r"\s?\(" # Optional whitespace, opening parenthesis r".*" # Any number of characters (that could denote a built-in type) r":class:`.+`" # Marker of a classref, class name in backticks r".*\):" # Any number of characters, closing parenthesis, colon. # The ^ colon above along with parenthesis is important because it makes sure that # the class is mentioned in the attribute description, not in free text. r".*$", # Any number of characters, end of string (end of line) re.VERBOSE, ) # for properties: there is no attr name in docstring. Just check if there's a class name. prop_docstring_pattern = re.compile(r":class:`.+`.*:") # pattern for iterating over potentially many class names in docstring for one attribute. # Tilde is optional (sometimes it is in the docstring, sometimes not). single_class_name_pattern = re.compile(r":class:`~?(?P[\w.]*)`") classes_to_inspect = inspect.getmembers(telegram, inspect.isclass) + inspect.getmembers( telegram.ext, inspect.isclass ) for _class_name, inspected_class in classes_to_inspect: # We need to make "" into # "telegram.StickerSet" because that's the way the classes are mentioned in # docstrings. name_of_inspected_class_in_docstr = self._generate_class_name_for_link(inspected_class) # Parsing part of the docstring with attributes (parsing of properties follows later) docstring_lines = inspect.getdoc(inspected_class).splitlines() lines_with_attrs = [] for idx, line in enumerate(docstring_lines): if line.strip() == "Attributes:": lines_with_attrs = docstring_lines[idx + 1 :] break for line in lines_with_attrs: if not (line_match := attr_docstr_pattern.match(line)): continue target_attr = line_match.group("attr_name") # a typing description of one attribute can contain multiple classes for match in single_class_name_pattern.finditer(line): name_of_class_in_attr = match.group("class_name") # Writing to dictionary: matching the class found in the docstring # and its subclasses to the attribute of the class being inspected. # The class in the attribute docstring (or its subclass) is the key, # ReST link to attribute of the class currently being inspected is the value. try: self._resolve_arg_and_add_link( arg=name_of_class_in_attr, dict_of_methods_for_class=attrs_for_class, link=f":attr:`{name_of_inspected_class_in_docstr}.{target_attr}`", ) except NotImplementedError as e: raise NotImplementedError( "Error generating Sphinx 'Available in' admonition " f"(admonition_inserter.py). Class {name_of_class_in_attr} present in " f"attribute {target_attr} of class {name_of_inspected_class_in_docstr}" f" could not be resolved. {e!s}" ) from e # Properties need to be parsed separately because they act like attributes but not # listed as attributes. properties = inspect.getmembers(inspected_class, lambda o: isinstance(o, property)) for prop_name, _ in properties: # Make sure this property is really defined in the class being inspected. # A property can be inherited from a parent class, then a link to it will not work. if prop_name not in inspected_class.__dict__: continue # 1. Can't use typing.get_type_hints because double-quoted type hints # (like "Application") will throw a NameError # 2. Can't use inspect.signature because return annotations of properties can be # hard to parse (like "(self) -> BD"). # 3. fget is used to access the actual function under the property wrapper docstring = inspect.getdoc(getattr(inspected_class, prop_name).fget) if docstring is None: continue first_line = docstring.splitlines()[0] if not prop_docstring_pattern.match(first_line): continue for match in single_class_name_pattern.finditer(first_line): name_of_class_in_prop = match.group("class_name") # Writing to dictionary: matching the class found in the docstring and its # subclasses to the property of the class being inspected. # The class in the property docstring (or its subclass) is the key, # ReST link to property of the class currently being inspected is the value. try: self._resolve_arg_and_add_link( arg=name_of_class_in_prop, dict_of_methods_for_class=attrs_for_class, link=f":attr:`{name_of_inspected_class_in_docstr}.{prop_name}`", ) except NotImplementedError as e: raise NotImplementedError( "Error generating Sphinx 'Available in' admonition " f"(admonition_inserter.py). Class {name_of_class_in_prop} present in " f"property {prop_name} of class {name_of_inspected_class_in_docstr}" f" could not be resolved. {e!s}" ) from e return self._generate_admonitions(attrs_for_class, admonition_type="available_in") def _create_returned_in(self) -> dict[type, str]: """Creates a dictionary with 'Returned in' admonitions for classes that are returned in Bot's and ApplicationBuilder's methods. """ # Generate a mapping of classes to ReST links to Bot methods which return it, # i.e. {: {:meth:`telegram.Bot.send_message`, ...}} methods_for_class = defaultdict(set) for cls, method_names in self.METHOD_NAMES_FOR_BOT_AND_APPBUILDER.items(): for method_name in method_names: sig = inspect.signature(getattr(cls, method_name)) ret_annot = sig.return_annotation method_link = self._generate_link_to_method(method_name, cls) try: self._resolve_arg_and_add_link( arg=ret_annot, dict_of_methods_for_class=methods_for_class, link=method_link, ) except NotImplementedError as e: raise NotImplementedError( "Error generating Sphinx 'Returned in' admonition " f"(admonition_inserter.py). {cls}, method {method_name}. " f"Couldn't resolve type hint in return annotation {ret_annot}. {e!s}" ) from e return self._generate_admonitions(methods_for_class, admonition_type="returned_in") def _create_shortcuts(self) -> dict[collections.abc.Callable, str]: """Creates a dictionary with 'Shortcuts' admonitions for Bot methods that have shortcuts in other classes. """ # pattern for looking for calls to Bot methods only bot_method_pattern = re.compile( r"""\s* # any number of whitespaces (?<=return\sawait\sself\.get_bot\(\)\.) # lookbehind \w+ # the method name we are looking for, letters/underscores (?=\() # lookahead: opening bracket before the args of the method start """, re.VERBOSE, ) # Generate a mapping of methods of classes to links to Bot methods which they are shortcuts # for, i.e. {: {:meth:`telegram.User.send_voice`, ...} shortcuts_for_bot_method = defaultdict(set) # inspect methods of all telegram classes for return statements that indicate # that this given method is a shortcut for a Bot method for _class_name, cls in inspect.getmembers(telegram, predicate=inspect.isclass): # no need to inspect Bot's own methods, as Bot can't have shortcuts in Bot if cls is telegram.Bot: continue for method_name, method in _iter_own_public_methods(cls): # .getsourcelines() returns a tuple. Item [1] is an int for line in inspect.getsourcelines(method)[0]: if not (bot_method_match := bot_method_pattern.search(line)): continue bot_method = getattr(telegram.Bot, bot_method_match.group()) link_to_shortcut_method = self._generate_link_to_method(method_name, cls) shortcuts_for_bot_method[bot_method].add(link_to_shortcut_method) return self._generate_admonitions(shortcuts_for_bot_method, admonition_type="shortcuts") def _create_use_in(self) -> dict[type, str]: """Creates a dictionary with 'Use in' admonitions for classes whose instances are accepted as arguments for Bot's and ApplicationBuilder's methods. """ # Generate a mapping of classes to links to Bot methods which accept them as arguments, # i.e. {: # {:meth:`telegram.Bot.answer_inline_query`, ...}} methods_for_class = defaultdict(set) for cls, method_names in self.METHOD_NAMES_FOR_BOT_AND_APPBUILDER.items(): for method_name in method_names: method_link = self._generate_link_to_method(method_name, cls) sig = inspect.signature(getattr(cls, method_name)) parameters = sig.parameters for param in parameters.values(): try: self._resolve_arg_and_add_link( arg=param.annotation, dict_of_methods_for_class=methods_for_class, link=method_link, ) except NotImplementedError as e: raise NotImplementedError( "Error generating Sphinx 'Use in' admonition " f"(admonition_inserter.py). {cls}, method {method_name}, parameter " f"{param}: Couldn't resolve type hint {param.annotation}. {e!s}" ) from e return self._generate_admonitions(methods_for_class, admonition_type="use_in") @staticmethod def _find_insert_pos_for_admonition(lines: list[str]) -> int: """Finds the correct position to insert the class admonition and returns the index. The admonition will be insert above "See also", "Examples:", version added/changed notes and args, whatever comes first. If no key phrases are found, the admonition will be inserted at the very end. """ for idx, value in list(enumerate(lines)): if value.startswith( ( ".. seealso:", # The docstring contains heading "Examples:", but Sphinx will have it converted # to ".. admonition: Examples": ".. admonition:: Examples", ".. version", # The space after ":param" is important because docstring can contain # ":paramref:" in its plain text in the beginning of a line (e.g. ExtBot): ":param ", # some classes (like "Credentials") have no params, so insert before attrs: ".. attribute::", ) ): return idx return len(lines) - 1 def _generate_admonitions( self, attrs_or_methods_for_class: dict[type, set[str]], admonition_type: str, ) -> dict[type, str]: """Generates admonitions of a given type. Takes a dictionary of classes matched to ReST links to methods or attributes, e.g.: ``` {: [":meth: `telegram.Bot.get_sticker_set`", ...]}. ``` Returns a dictionary of classes matched to full admonitions, e.g. for `admonition_type` "returned_in" (note that title and CSS class are generated automatically): ``` {: ".. admonition:: Returned in: :class: returned-in :meth: `telegram.Bot.get_sticker_set`"}. ``` """ if admonition_type not in self.ALL_ADMONITION_TYPES: raise TypeError(f"Admonition type {admonition_type} not supported.") admonition_for_class = {} for cls, attrs in attrs_or_methods_for_class.items(): if cls is telegram.ext.ApplicationBuilder: # ApplicationBuilder is only used in and returned from its own methods, # so its page needs no admonitions. continue sorted_attrs = sorted(attrs) # e.g. for admonition type "use_in" the title will be "Use in" and CSS class "use-in". admonition = f""" .. admonition:: {admonition_type.title().replace("_", " ")} :class: {admonition_type.replace("_", "-")} """ if len(sorted_attrs) > 1: for target_attr in sorted_attrs: admonition += "\n * " + target_attr else: admonition += f"\n {sorted_attrs[0]}" admonition += "\n " # otherwise an unexpected unindent warning will be issued admonition_for_class[cls] = admonition return admonition_for_class @staticmethod def _generate_class_name_for_link(cls: type) -> str: """Generates class name that can be used in a ReST link.""" # Check for potential presence of ".ext.", we will need to keep it. ext = ".ext" if ".ext." in str(cls) else "" return f"telegram{ext}.{cls.__name__}" def _generate_link_to_method(self, method_name: str, cls: type) -> str: """Generates a ReST link to a method of a telegram class.""" return f":meth:`{self._generate_class_name_for_link(cls)}.{method_name}`" @staticmethod def _iter_subclasses(cls: type) -> Iterator: return ( # exclude private classes c for c in cls.__subclasses__() if not str(c).split(".")[-1].startswith("_") ) def _resolve_arg_and_add_link( self, arg: Any, dict_of_methods_for_class: defaultdict, link: str, ) -> None: """A helper method. Tries to resolve the arg into a valid class. In case of success, adds the link (to a method, attribute, or property) for that class' and its subclasses' sets of links in the dictionary of admonitions. **Modifies dictionary in place.** """ for cls in self._resolve_arg(arg): # When trying to resolve an argument from args or return annotation, # the method _resolve_arg returns None if nothing could be resolved. # Also, if class was resolved correctly, "telegram" will definitely be in its str(). if cls is None or "telegram" not in str(cls): continue dict_of_methods_for_class[cls].add(link) for subclass in self._iter_subclasses(cls): dict_of_methods_for_class[subclass].add(link) def _resolve_arg(self, arg: Any) -> Iterator[Union[type, None]]: """Analyzes an argument of a method and recursively yields classes that the argument or its sub-arguments (in cases like Union[...]) belong to, if they can be resolved to telegram or telegram.ext classes. Raises `NotImplementedError`. """ origin = typing.get_origin(arg) if ( origin in (collections.abc.Callable, typing.IO) or arg is None # no other check available (by type or origin) for these: or str(type(arg)) in ("", "") ): pass # RECURSIVE CALLS # for cases like Union[Sequence.... elif origin in ( Union, collections.abc.Coroutine, collections.abc.Sequence, ): for sub_arg in typing.get_args(arg): yield from self._resolve_arg(sub_arg) elif isinstance(arg, typing.TypeVar): # gets access to the "bound=..." parameter yield from self._resolve_arg(arg.__bound__) # END RECURSIVE CALLS elif isinstance(arg, typing.ForwardRef): m = self.FORWARD_REF_PATTERN.match(str(arg)) # We're sure it's a ForwardRef, so, unless it belongs to known exceptions, # the class must be resolved. # If it isn't resolved, we'll have the program throw an exception to be sure. try: cls = self._resolve_class(m.group("class_name")) except AttributeError as exc: # skip known ForwardRef's that need not be resolved to a Telegram class if self.FORWARD_REF_SKIP_PATTERN.match(str(arg)): pass else: raise NotImplementedError(f"Could not process ForwardRef: {arg}") from exc else: yield cls # For custom generics like telegram.ext._application.Application[~BT, ~CCT, ~UD...]. # This must come before the check for isinstance(type) because GenericAlias can also be # recognized as type if it belongs to . elif str(type(arg)) in ( "", "", "", ): if "telegram" in str(arg): # get_origin() of telegram.ext._application.Application[~BT, ~CCT, ~UD...] # will produce yield origin elif isinstance(arg, type): if "telegram" in str(arg): yield arg # For some reason "InlineQueryResult", "InputMedia" & some others are currently not # recognized as ForwardRefs and are identified as plain strings. elif isinstance(arg, str): # args like "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]" can be recognized as strings. # Remove whatever is in the square brackets because it doesn't need to be parsed. arg = re.sub(r"\[.+]", "", arg) cls = self._resolve_class(arg) # Here we don't want an exception to be thrown since we're not sure it's ForwardRef if cls is not None: yield cls else: raise NotImplementedError( f"Cannot process argument {arg} of type {type(arg)} (origin {origin})" ) @staticmethod def _resolve_class(name: str) -> Union[type, None]: """The keys in the admonitions dictionary are not strings like "telegram.StickerSet" but classes like . This method attempts to resolve a PTB class from a name that does or does not contain the word 'telegram', e.g. from "telegram.StickerSet" or "StickerSet". Returns a class on success, :obj:`None` if nothing could be resolved. """ for option in ( name, f"telegram.{name}", f"telegram.ext.{name}", f"telegram.ext.filters.{name}", ): try: return eval(option) # NameError will be raised if trying to eval just name and it doesn't work, e.g. # "Name 'ApplicationBuilder' is not defined". # AttributeError will be raised if trying to e.g. eval f"telegram.{name}" when the # class denoted by `name` actually belongs to `telegram.ext`: # "module 'telegram' has no attribute 'ApplicationBuilder'". # If neither option works, this is not a PTB class. except (NameError, AttributeError): continue return None if __name__ == "__main__": # just try instantiating for debugging purposes AdmonitionInserter() python-telegram-bot-21.1.1/docs/auxil/kwargs_insertion.py000066400000000000000000000077371460724040100235250ustar00rootroot00000000000000# # 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 typing import List keyword_args = [ "Keyword Arguments:", ( " read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to " " :paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to " " :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. " ), ( " write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to " " :paramref:`telegram.request.BaseRequest.post.write_timeout`. Defaults to " " :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`." ), ( " connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to " " :paramref:`telegram.request.BaseRequest.post.connect_timeout`. Defaults to " " :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`." ), ( " pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to " " :paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to " " :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`." ), ( " api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments" " to be passed to the Telegram API. See :meth:`~telegram.Bot.do_api_request` for" " limitations." ), "", ] media_write_timeout_deprecation_methods = [ "send_photo", "send_audio", "send_document", "send_sticker", "send_video", "send_video_note", "send_animation", "send_voice", "send_media_group", "set_chat_photo", "upload_sticker_file", "add_sticker_to_set", "create_new_sticker_set", ] media_write_timeout_deprecation = [ " write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to " " :paramref:`telegram.request.BaseRequest.post.write_timeout`. By default, ``20`` " " seconds are used as write timeout." "", "", " .. deprecated:: 20.7", " In future versions, the default value will be changed to " " :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.", "", "", ] get_updates_read_timeout_addition = [ " :paramref:`timeout` will be added to this value.", "", "", " .. versionchanged:: 20.7", " Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE` instead of ", " ``2``.", ] def find_insert_pos_for_kwargs(lines: List[str]) -> int: """Finds the correct position to insert the keyword arguments and returns the index.""" for idx, value in reversed(list(enumerate(lines))): # reversed since :returns: is at the end if value.startswith("Returns"): return idx return False def check_timeout_and_api_kwargs_presence(obj: object) -> int: """Checks if the method has timeout and api_kwargs keyword only parameters.""" sig = inspect.signature(obj) params_to_check = ( "read_timeout", "write_timeout", "connect_timeout", "pool_timeout", "api_kwargs", ) return all( param in sig.parameters and sig.parameters[param].kind == inspect.Parameter.KEYWORD_ONLY for param in params_to_check ) python-telegram-bot-21.1.1/docs/auxil/link_code.py000066400000000000000000000056101460724040100220500ustar00rootroot00000000000000# # 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/]. """Functionality in this file is used for getting the [source] links on the classes, methods etc to link to the correct files & lines on github. Can be simplified once https://github.com/sphinx-doc/sphinx/issues/1556 is closed """ import subprocess from pathlib import Path from typing import Dict, Tuple from sphinx.util import logging # get the sphinx(!) logger # Makes sure logs render in red and also plays nicely with e.g. the `nitpicky` option. sphinx_logger = logging.getLogger(__name__) # must be a module-level variable so that it can be written to by the `autodoc-process-docstring` # event handler in `sphinx_hooks.py` LINE_NUMBERS: Dict[str, Tuple[Path, int, int]] = {} def _git_branch() -> str: """Get's the current git sha if available or fall back to `master`""" try: output = subprocess.check_output( ["git", "describe", "--tags", "--always"], stderr=subprocess.STDOUT ) return output.decode().strip() except Exception as exc: sphinx_logger.exception( "Failed to get a description of the current commit. Falling back to `master`.", exc_info=exc, ) return "master" git_branch = _git_branch() base_url = "https://github.com/python-telegram-bot/python-telegram-bot/blob/" def linkcode_resolve(_, info) -> str: """See www.sphinx-doc.org/en/master/usage/extensions/linkcode.html""" combined = ".".join((info["module"], info["fullname"])) # special casing for ExtBot which is due to the special structure of extbot.rst combined = combined.replace("ExtBot.ExtBot", "ExtBot") line_info = LINE_NUMBERS.get(combined) if not line_info: # Try the __init__ line_info = LINE_NUMBERS.get(f"{combined.rsplit('.', 1)[0]}.__init__") if not line_info: # Try the class line_info = LINE_NUMBERS.get(f"{combined.rsplit('.', 1)[0]}") if not line_info: # Try the module line_info = LINE_NUMBERS.get(info["module"]) if not line_info: return None file, start_line, end_line = line_info return f"{base_url}{git_branch}/{file}#L{start_line}-L{end_line}" python-telegram-bot-21.1.1/docs/auxil/sphinx_hooks.py000066400000000000000000000200431460724040100226320ustar00rootroot00000000000000# # 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 collections.abc import inspect import re import typing from pathlib import Path from sphinx.application import Sphinx import telegram import telegram.ext from docs.auxil.admonition_inserter import AdmonitionInserter from docs.auxil.kwargs_insertion import ( check_timeout_and_api_kwargs_presence, find_insert_pos_for_kwargs, get_updates_read_timeout_addition, keyword_args, media_write_timeout_deprecation, media_write_timeout_deprecation_methods, ) from docs.auxil.link_code import LINE_NUMBERS ADMONITION_INSERTER = AdmonitionInserter() # Some base classes are implementation detail # We want to instead show *their* base class PRIVATE_BASE_CLASSES = { "_ChatUserBaseFilter": "MessageFilter", "_Dice": "MessageFilter", "_BaseThumbedMedium": "TelegramObject", "_BaseMedium": "TelegramObject", "_CredentialsBase": "TelegramObject", } FILE_ROOT = Path(inspect.getsourcefile(telegram)).parent.parent.resolve() def autodoc_skip_member(app, what, name, obj, skip, options): """We use this to not document certain members like filter() or check_update() for filters. See https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#skipping-members""" included = {"MessageFilter", "UpdateFilter"} # filter() and check_update() only for these. included_in_obj = any(inc in repr(obj) for inc in included) if included_in_obj: # it's difficult to see if check_update is from an inherited-member or not for frame in inspect.stack(): # From https://github.com/sphinx-doc/sphinx/issues/9533 if frame.function == "filter_members": docobj = frame.frame.f_locals["self"].object if not any(inc in str(docobj) for inc in included) and name == "check_update": return True break if name == "filter" and obj.__module__ == "telegram.ext.filters" and not included_in_obj: return True # return True to exclude from docs. return None def autodoc_process_docstring( app: Sphinx, what, name: str, obj: object, options, lines: list[str] ): """We do the following things: 1) Use this method to automatically insert the Keyword Args and "Shortcuts" admonitions for the Bot methods. 2) Use this method to automatically insert "Returned in" admonition into classes that are returned from the Bot methods 3) Use this method to automatically insert "Available in" admonition into classes whose instances are available as attributes of other classes 4) Use this method to automatically insert "Use in" admonition into classes whose instances can be used as arguments of the Bot methods 5) Misuse this autodoc hook to get the file names & line numbers because we have access to the actual object here. """ # 1) Insert the Keyword Args and "Shortcuts" admonitions for the Bot methods method_name = name.split(".")[-1] if ( name.startswith("telegram.Bot.") and what == "method" and method_name.islower() and check_timeout_and_api_kwargs_presence(obj) ): insert_index = find_insert_pos_for_kwargs(lines) if not insert_index: raise ValueError( f"Couldn't find the correct position to insert the keyword args for {obj}." ) get_updates: bool = method_name == "get_updates" # The below can be done in 1 line with itertools.chain, but this must be modified in-place insert_idx = insert_index for i in range(insert_index, insert_index + len(keyword_args)): to_insert = keyword_args[i - insert_index] if ( "post.write_timeout`. Defaults to" in to_insert and method_name in media_write_timeout_deprecation_methods ): effective_insert: list[str] = media_write_timeout_deprecation elif get_updates and to_insert.lstrip().startswith("read_timeout"): effective_insert = [to_insert, *get_updates_read_timeout_addition] else: effective_insert = [to_insert] lines[insert_idx:insert_idx] = effective_insert insert_idx += len(effective_insert) ADMONITION_INSERTER.insert_admonitions( obj=typing.cast(collections.abc.Callable, obj), docstring_lines=lines, ) # 2-4) Insert "Returned in", "Available in", "Use in" admonitions into classes # (where applicable) if what == "class": ADMONITION_INSERTER.insert_admonitions( obj=typing.cast(type, obj), # since "what" == class, we know it's not just object docstring_lines=lines, ) # 5) Get the file names & line numbers # We can't properly handle ordinary attributes. # In linkcode_resolve we'll resolve to the `__init__` or module instead if what == "attribute": return # Special casing for properties if hasattr(obj, "fget"): obj = obj.fget # Special casing for filters if isinstance(obj, telegram.ext.filters.BaseFilter): obj = obj.__class__ try: source_lines, start_line = inspect.getsourcelines(obj) end_line = start_line + len(source_lines) file = Path(inspect.getsourcefile(obj)).relative_to(FILE_ROOT) LINE_NUMBERS[name] = (file, start_line, end_line) except Exception: pass # Since we don't document the `__init__`, we call this manually to have it available for # attributes -- see the note above if what == "class": autodoc_process_docstring(app, "method", f"{name}.__init__", obj.__init__, options, lines) def autodoc_process_bases(app, name, obj, option, bases: list) -> None: """Here we fine tune how the base class's classes are displayed.""" for idx, raw_base in enumerate(bases): # let's use a string representation of the object base = str(raw_base) # Special case for abstract context managers which are wrongly resoled for some reason if base.startswith("typing.AbstractAsyncContextManager"): bases[idx] = ":class:`contextlib.AbstractAsyncContextManager`" continue # Special case because base classes are in std lib: if "StringEnum" in base == "": bases[idx] = ":class:`enum.Enum`" bases.insert(0, ":class:`str`") continue if "IntEnum" in base: bases[idx] = ":class:`enum.IntEnum`" continue # Drop generics (at least for now) if base.endswith("]"): base = base.split("[", maxsplit=1)[0] bases[idx] = f":class:`{base}`" # Now convert `telegram._message.Message` to `telegram.Message` etc if ( not (match := re.search(pattern=r"(telegram(\.ext|))\.[_\w\.]+", string=base)) or "_utils" in base ): continue parts = match.group(0).split(".") # Remove private paths for index, part in enumerate(parts): if part.startswith("_"): parts = parts[:index] + parts[-1:] break # Replace private base classes with their respective parent parts = [PRIVATE_BASE_CLASSES.get(part, part) for part in parts] base = ".".join(parts) bases[idx] = f":class:`{base}`" python-telegram-bot-21.1.1/docs/auxil/tg_const_role.py000066400000000000000000000076141460724040100227700ustar00rootroot00000000000000# # 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 enum import Enum from docutils.nodes import Element from sphinx.domains.python import PyXRefRole from sphinx.environment import BuildEnvironment from sphinx.util import logging import telegram # get the sphinx(!) logger # Makes sure logs render in red and also plays nicely with e.g. the `nitpicky` option. sphinx_logger = logging.getLogger(__name__) CONSTANTS_ROLE = "tg-const" class TGConstXRefRole(PyXRefRole): """This is a bit of Sphinx magic. We add a new role type called tg-const that allows us to reference values from the `telegram.constants.module` while using the actual value as title of the link. Example: :tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` renders as `4096` but links to the constant. """ def process_link( self, env: BuildEnvironment, refnode: Element, has_explicit_title: bool, title: str, target: str, ) -> tuple[str, str]: title, target = super().process_link(env, refnode, has_explicit_title, title, target) try: # We use `eval` to get the value of the expression. Maybe there are better ways to # do this via importlib or so, but it does the job for now value = eval(target) # Maybe we need a better check if the target is actually from tg.constants # for now checking if it's an Enum suffices since those are used nowhere else in PTB if isinstance(value, Enum): # Special casing for file size limits if isinstance(value, telegram.constants.FileSizeLimit): return f"{int(value.value / 1e6)} MB", target return repr(value.value), target # Just for (Bot API) versions number auto add in constants: if isinstance(value, str) and target in ( "telegram.constants.BOT_API_VERSION", "telegram.__version__", ): return value, target if isinstance(value, tuple) and target in ( "telegram.constants.BOT_API_VERSION_INFO", "telegram.__version_info__", ): return str(value), target if ( isinstance(value, datetime.datetime) and value == telegram.constants.ZERO_DATE and target in ("telegram.constants.ZERO_DATE",) ): return repr(value), target sphinx_logger.warning( "%s:%d: WARNING: Did not convert reference %s. :%s: is not supposed" " to be used with this type of target.", refnode.source, refnode.line, refnode.rawsource, CONSTANTS_ROLE, ) return title, target except Exception as exc: sphinx_logger.exception( "%s:%d: WARNING: Did not convert reference %s due to an exception.", refnode.source, refnode.line, refnode.rawsource, exc_info=exc, ) return title, target python-telegram-bot-21.1.1/docs/requirements-docs.txt000066400000000000000000000003351460724040100226360ustar00rootroot00000000000000sphinx==7.2.6 furo==2024.1.29 furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0.1 sphinx-paramlinks==0.6.0 sphinxcontrib-mermaid==0.9.2 sphinx-copybutton==0.5.2 sphinx-inline-tabs==2023.4.21 python-telegram-bot-21.1.1/docs/source/000077500000000000000000000000001460724040100177235ustar00rootroot00000000000000python-telegram-bot-21.1.1/docs/source/_static/000077500000000000000000000000001460724040100213515ustar00rootroot00000000000000python-telegram-bot-21.1.1/docs/source/_static/.placeholder000066400000000000000000000000001460724040100236220ustar00rootroot00000000000000python-telegram-bot-21.1.1/docs/source/_static/style_admonitions.css000066400000000000000000000077661460724040100256470ustar00rootroot00000000000000:root { --icon--shortcuts: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-signpost' viewBox='0 0 16 16'%3E%3Cpath d='M7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414a1 1 0 0 0-2 0zM12.532 5l1.666 2-1.666 2H2V5h10.532z'/%3E%3C/svg%3E"); --icon--returned-in: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-arrow-return-right' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z'/%3E%3C/svg%3E"); --icon--available-in: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-geo-fill' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4 4a4 4 0 1 1 4.5 3.969V13.5a.5.5 0 0 1-1 0V7.97A4 4 0 0 1 4 3.999zm2.493 8.574a.5.5 0 0 1-.411.575c-.712.118-1.28.295-1.655.493a1.319 1.319 0 0 0-.37.265.301.301 0 0 0-.057.09V14l.002.008a.147.147 0 0 0 .016.033.617.617 0 0 0 .145.15c.165.13.435.27.813.395.751.25 1.82.414 3.024.414s2.273-.163 3.024-.414c.378-.126.648-.265.813-.395a.619.619 0 0 0 .146-.15.148.148 0 0 0 .015-.033L12 14v-.004a.301.301 0 0 0-.057-.09 1.318 1.318 0 0 0-.37-.264c-.376-.198-.943-.375-1.655-.493a.5.5 0 1 1 .164-.986c.77.127 1.452.328 1.957.594C12.5 13 13 13.4 13 14c0 .426-.26.752-.544.977-.29.228-.68.413-1.116.558-.878.293-2.059.465-3.34.465-1.281 0-2.462-.172-3.34-.465-.436-.145-.826-.33-1.116-.558C3.26 14.752 3 14.426 3 14c0-.599.5-1 .961-1.243.505-.266 1.187-.467 1.957-.594a.5.5 0 0 1 .575.411z'/%3E%3C/svg%3E"); --icon--use-in:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-funnel' viewBox='0 0 16 16'%3E%3Cpath d='M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z'/%3E%3C/svg%3E"); } .admonition.shortcuts { border-color: rgb(43, 155, 70); } .admonition.shortcuts > .admonition-title { background-color: rgba(43, 155, 70, 0.1); border-color: rgb(43, 155, 70); } .admonition.shortcuts > .admonition-title::before { background-color: rgb(43, 155, 70); -webkit-mask-image: var(--icon--shortcuts); mask-image: var(--icon--shortcuts); } .admonition.returned-in { border-color: rgb(230, 109, 15); } .admonition.returned-in > .admonition-title { background-color: rgba(177, 108, 51, 0.1); border-color: rgb(230, 109, 15); } .admonition.returned-in > .admonition-title::before { background-color: rgb(230, 109, 15); -webkit-mask-image: var(--icon--returned-in); mask-image: var(--icon--returned-in); } .admonition.available-in { border-color: rgb(183, 4, 215); } .admonition.available-in > .admonition-title { background-color: rgba(165, 99, 177, 0.1); border-color: rgb(183, 4, 215); } .admonition.available-in > .admonition-title::before { background-color: rgb(183, 4, 215); -webkit-mask-image: var(--icon--available-in); mask-image: var(--icon--available-in); } .admonition.use-in { border-color: rgb(203, 147, 1); } .admonition.use-in > .admonition-title { background-color: rgba(176, 144, 60, 0.1); border-color: rgb(203, 147, 1); } .admonition.use-in > .admonition-title::before { background-color: rgb(203, 147, 1); -webkit-mask-image: var(--icon--use-in); mask-image: var(--icon--use-in); } .admonition.returned-in > ul:hover, .admonition.available-in > ul:hover, .admonition.use-in > ul:hover, .admonition.shortcuts > ul:hover { cursor: move; } .admonition.returned-in > ul, .admonition.available-in > ul, .admonition.use-in > ul, .admonition.shortcuts > ul { max-height: 200px; overflow-y: scroll; } python-telegram-bot-21.1.1/docs/source/_static/style_external_link.css000066400000000000000000000010471460724040100261440ustar00rootroot00000000000000article a.reference.external:not([href*="/python-telegram-bot/blob/"])::after { content: url('data:image/svg+xml,'); margin: 0 0.25rem; vertical-align: middle; }python-telegram-bot-21.1.1/docs/source/_static/style_general.css000066400000000000000000000000701460724040100247150ustar00rootroot00000000000000.article-container h1 { overflow-wrap: anywhere; }python-telegram-bot-21.1.1/docs/source/_static/style_images.css000066400000000000000000000006151460724040100245520ustar00rootroot00000000000000figure > img { height: 300px; /* resize figures so they aren't too big */ } @media (prefers-color-scheme: dark) { body:not([data-theme="light"]) figure > img { /* auto and dark is dark mode */ filter: invert(92%); } } @media (prefers-color-scheme: light) { body[data-theme="dark"] figure > img { /* auto and light is light mode */ filter: invert(92%); } } python-telegram-bot-21.1.1/docs/source/_static/style_mermaid_diagrams.css000066400000000000000000000000411460724040100265630ustar00rootroot00000000000000.mermaid svg { height: auto; } python-telegram-bot-21.1.1/docs/source/_static/style_sidebar_brand.css000066400000000000000000000003451460724040100260640ustar00rootroot00000000000000.sidebar-sticky .sidebar-brand { flex-direction: row; } .sidebar-sticky .sidebar-brand .sidebar-logo-container { align-self: center; } .sidebar-sticky .sidebar-brand .sidebar-brand-text { align-self: center; }python-telegram-bot-21.1.1/docs/source/changelog.rst000066400000000000000000000000361460724040100224030ustar00rootroot00000000000000.. include:: ../../CHANGES.rstpython-telegram-bot-21.1.1/docs/source/coc.rst000066400000000000000000000000461460724040100212210ustar00rootroot00000000000000.. include:: ../../CODE_OF_CONDUCT.rstpython-telegram-bot-21.1.1/docs/source/conf.py000066400000000000000000000337651460724040100212400ustar00rootroot00000000000000import re import sys from pathlib import Path # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. from sphinx.application import Sphinx sys.path.insert(0, str(Path("../..").resolve().absolute())) # -- General configuration ------------------------------------------------ # General information about the project. project = "python-telegram-bot" copyright = "2015-2024, Leandro Toledo" author = "Leandro Toledo" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = "21.1.1" # telegram.__version__[:3] # The full version, including alpha/beta/rc tags. release = "21.1.1" # telegram.__version__ # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = "6.1.3" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx.ext.linkcode", "sphinx.ext.extlinks", "sphinx_paramlinks", "sphinx_copybutton", "sphinx_inline_tabs", "sphinxcontrib.mermaid", "sphinx_search.extension", ] # For shorter links to Wiki in docstrings extlinks = { "wiki": ("https://github.com/python-telegram-bot/python-telegram-bot/wiki/%s", "%s"), "pr": ("https://github.com/python-telegram-bot/python-telegram-bot/pull/%s", "#%s"), "issue": ("https://github.com/python-telegram-bot/python-telegram-bot/issues/%s", "#%s"), } # Use intersphinx to reference the python builtin library docs intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "APScheduler": ("https://apscheduler.readthedocs.io/en/3.x/", None), } # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: source_suffix = ".rst" # The master toctree document. master_doc = "index" # Global substitutions rst_prolog = (Path.cwd() / "../substitutions/global.rst").read_text(encoding="utf-8") # -- Extension settings ------------------------------------------------ napoleon_use_admonition_for_examples = True # Don't show type hints in the signature - that just makes it hardly readable # and we document the types anyway autodoc_typehints = "none" # Show docstring for special members autodoc_default_options = { "special-members": True, # For some reason, __weakref__ can not be ignored by using "inherited-members" in all cases # so we list it here. "exclude-members": "__init__, __weakref__", } # Fail on warnings & unresolved references etc nitpicky = True # Paramlink style paramlinks_hyperlink_param = "name" # Linkcheck settings linkcheck_ignore = [ # Let's not check issue/PR links - that's wasted resources r"http(s)://github\.com/python-telegram-bot/python-telegram-bot/(issues|pull)/\d+/?", # For some reason linkcheck has a problem with these two: re.escape("https://github.com/python-telegram-bot/python-telegram-bot/discussions/new"), re.escape("https://github.com/python-telegram-bot/python-telegram-bot/issues/new"), # Anchors are apparently inserted by GitHub dynamically, so let's skip checking them "https://github.com/python-telegram-bot/python-telegram-bot/tree/master/examples#", r"https://github\.com/python-telegram-bot/python-telegram-bot/wiki/[\w\-_,]+\#", ] linkcheck_allowed_redirects = { # Redirects to the default version are okay r"https://docs\.python-telegram-bot\.org/.*": ( r"https://docs\.python-telegram-bot\.org/en/[\w\d\.]+/.*" ), # pre-commit.ci always redirects to the latest run re.escape( "https://results.pre-commit.ci/latest/github/python-telegram-bot/python-telegram-bot/master" ): r"https://results\.pre-commit\.ci/run/github/.*", } # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # Decides the language used for syntax highlighting of code blocks. highlight_language = "python3" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "furo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the documentation. html_theme_options = { "navigation_with_keys": True, "dark_css_variables": { "admonition-title-font-size": "0.95rem", "admonition-font-size": "0.92rem", }, "light_css_variables": { "admonition-title-font-size": "0.95rem", "admonition-font-size": "0.92rem", }, "footer_icons": [ { # Telegram channel logo "name": "Telegram Channel", "url": "https://t.me/pythontelegrambotchannel/", # Following svg is from https://react-icons.github.io/react-icons/search?q=telegram "html": ( '' '' ), "class": "", }, { # Github logo "name": "GitHub", "url": "https://github.com/python-telegram-bot/python-telegram-bot/", "html": ( '' "" ), "class": "", }, { # PTB website logo - globe "name": "python-telegram-bot website", "url": "https://python-telegram-bot.org/", "html": ( '' '' ), "class": "", }, ], } # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = f"python-telegram-bot
v{version}" # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "ptb-logo_1024.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = "ptb-logo_1024.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] html_css_files = [ "style_external_link.css", "style_mermaid_diagrams.css", "style_sidebar_brand.css", "style_general.css", "style_admonitions.css", "style_images.css", ] html_permalinks_icon = "¶" # Furo's default permalink icon is `#` which doesn't look great imo. # Output file base name for HTML help builder. htmlhelp_basename = "python-telegram-bot-doc" # The base URL which points to the root of the HTML documentation. It is used to indicate the # location of document using The Canonical Link Relation. Default: ''. html_baseurl = "https://docs.python-telegram-bot.org" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). "papersize": "a4paper", # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. "preamble": r"""\setcounter{tocdepth}{2} \usepackage{enumitem} \setlistdepth{99}""", # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "python-telegram-bot.tex", "python-telegram-bot Documentation", author, "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. latex_logo = "ptb-logo_1024.png" # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "python-telegram-bot", "python-telegram-bot Documentation", [author], 1)] # rtd_sphinx_search_file_type = "un-minified" # Configuration for furo-sphinx-search # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "python-telegram-bot", "python-telegram-bot Documentation", author, "python-telegram-bot", "We have made you a wrapper you can't refuse", "Miscellaneous", ), ] # -- script stuff -------------------------------------------------------- # Due to Sphinx behaviour, these imports only work when imported here, not at top of module. # Not used but must be imported for the linkcode extension to find it from docs.auxil.link_code import linkcode_resolve # noqa: E402, F401 from docs.auxil.sphinx_hooks import ( # noqa: E402 autodoc_process_bases, autodoc_process_docstring, autodoc_skip_member, ) from docs.auxil.tg_const_role import CONSTANTS_ROLE, TGConstXRefRole # noqa: E402 def setup(app: Sphinx): app.connect("autodoc-skip-member", autodoc_skip_member) app.connect("autodoc-process-bases", autodoc_process_bases) # The default priority is 500. We want our function to run before napoleon doc-conversion # and sphinx-paramlinks do, b/c otherwise the inserted kwargs in the bot methods won't show # up in the objects.inv file that Sphinx generates (i.e. not in the search). app.connect("autodoc-process-docstring", autodoc_process_docstring, priority=100) app.add_role_to_domain("py", CONSTANTS_ROLE, TGConstXRefRole()) python-telegram-bot-21.1.1/docs/source/contributing.rst000066400000000000000000000000531460724040100231620ustar00rootroot00000000000000.. include:: ../../.github/CONTRIBUTING.rstpython-telegram-bot-21.1.1/docs/source/examples.arbitrarycallbackdatabot.rst000066400000000000000000000002461460724040100273070ustar00rootroot00000000000000``arbitrarycallbackdatabot.py`` =============================== .. literalinclude:: ../../examples/arbitrarycallbackdatabot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.chatmemberbot.rst000066400000000000000000000002051460724040100251030ustar00rootroot00000000000000``chatmemberbot.py`` ==================== .. literalinclude:: ../../examples/chatmemberbot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.contexttypesbot.rst000066400000000000000000000002131460724040100255440ustar00rootroot00000000000000``contexttypesbot.py`` ====================== .. literalinclude:: ../../examples/contexttypesbot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.conversationbot.rst000066400000000000000000000003631460724040100255130ustar00rootroot00000000000000``conversationbot.py`` ====================== .. literalinclude:: ../../examples/conversationbot.py :language: python :linenos: .. _conversationbot-diagram: State Diagram ------------- .. mermaid:: ../../examples/conversationbot.mmd python-telegram-bot-21.1.1/docs/source/examples.conversationbot2.rst000066400000000000000000000003701460724040100255730ustar00rootroot00000000000000``conversationbot2.py`` ======================= .. literalinclude:: ../../examples/conversationbot2.py :language: python :linenos: .. _conversationbot2-diagram: State Diagram ------------- .. mermaid:: ../../examples/conversationbot2.mmd python-telegram-bot-21.1.1/docs/source/examples.customwebhookbot.rst000066400000000000000000000032001460724040100256630ustar00rootroot00000000000000``customwebhookbot.py`` ======================= This example is available for different web frameworks. You can select your preferred framework by opening one of the tabs above the code example. .. hint:: The following examples show how different Python web frameworks can be used alongside PTB. This can be useful for two use cases: 1. For extending the functionality of your existing bot to handling updates of external services 2. For extending the functionality of your exisiting web application to also include chat bot functionality How the PTB and web framework components of the examples below are viewed surely depends on which use case one has in mind. We are fully aware that a combination of PTB with web frameworks will always mean finding a tradeoff between usability and best practices for both PTB and the web framework and these examples are certainly far from optimal solutions. Please understand them as starting points and use your expertise of the web framework of your choosing to build up on them. You are of course also very welcome to help improve these examples! .. tab:: ``starlette`` .. literalinclude:: ../../examples/customwebhookbot/starlettebot.py :language: python :linenos: .. tab:: ``flask`` .. literalinclude:: ../../examples/customwebhookbot/flaskbot.py :language: python :linenos: .. tab:: ``quart`` .. literalinclude:: ../../examples/customwebhookbot/quartbot.py :language: python :linenos: .. tab:: ``Django`` .. literalinclude:: ../../examples/customwebhookbot/djangobot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.deeplinking.rst000066400000000000000000000001771460724040100245700ustar00rootroot00000000000000``deeplinking.py`` ================== .. literalinclude:: ../../examples/deeplinking.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.echobot.rst000066400000000000000000000001631460724040100237150ustar00rootroot00000000000000``echobot.py`` ============== .. literalinclude:: ../../examples/echobot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.errorhandlerbot.rst000066400000000000000000000002131460724040100254620ustar00rootroot00000000000000``errorhandlerbot.py`` ====================== .. literalinclude:: ../../examples/errorhandlerbot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.inlinebot.rst000066400000000000000000000001711460724040100242540ustar00rootroot00000000000000``inlinebot.py`` ================ .. literalinclude:: ../../examples/inlinebot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.inlinekeyboard.rst000066400000000000000000000002101460724040100252620ustar00rootroot00000000000000``inlinekeyboard.py`` ===================== .. literalinclude:: ../../examples/inlinekeyboard.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.inlinekeyboard2.rst000066400000000000000000000002131460724040100253470ustar00rootroot00000000000000``inlinekeyboard2.py`` ====================== .. literalinclude:: ../../examples/inlinekeyboard2.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.nestedconversationbot.rst000066400000000000000000000004211460724040100267110ustar00rootroot00000000000000``nestedconversationbot.py`` ============================ .. literalinclude:: ../../examples/nestedconversationbot.py :language: python :linenos: .. _nestedconversationbot-diagram: State Diagram ------------- .. mermaid:: ../../examples/nestedconversationbot.mmd python-telegram-bot-21.1.1/docs/source/examples.passportbot.rst000066400000000000000000000004001460724040100246440ustar00rootroot00000000000000``passportbot.py`` ================== .. literalinclude:: ../../examples/passportbot.py :language: python :linenos: .. _passportbot-html: HTML Page --------- .. literalinclude:: ../../examples/passportbot.html :language: html :linenos: python-telegram-bot-21.1.1/docs/source/examples.paymentbot.rst000066400000000000000000000001741460724040100244560ustar00rootroot00000000000000``paymentbot.py`` ================= .. literalinclude:: ../../examples/paymentbot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.persistentconversationbot.rst000066400000000000000000000002511460724040100276300ustar00rootroot00000000000000``persistentconversationbot.py`` ================================ .. literalinclude:: ../../examples/persistentconversationbot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.pollbot.rst000066400000000000000000000001631460724040100237450ustar00rootroot00000000000000``pollbot.py`` ============== .. literalinclude:: ../../examples/pollbot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.rawapibot.rst000066400000000000000000000002631460724040100242630ustar00rootroot00000000000000`rawapibot.py` ============== This example uses only the pure, "bare-metal" API wrapper. .. literalinclude:: ../../examples/rawapibot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.rst000066400000000000000000000154721460724040100223040ustar00rootroot00000000000000Examples ======== In this section we display small examples to show what a bot written with ``python-telegram-bot`` looks like. Some bots focus on one specific aspect of the Telegram Bot API while others focus on one of the mechanics of this library. Except for the :any:`examples.rawapibot` example, they all use the high-level framework this library provides with the :mod:`telegram.ext` submodule. All examples are licensed under the `CC0 License `__ and are therefore fully dedicated to the public domain. You can use them as the base for your own bots without worrying about copyrights. Do note that we ignore one pythonic convention. Best practice would dictate, in many handler callbacks function signatures, to replace the argument ``context`` with an underscore, since ``context`` is an unused local variable in those callbacks. However, since these are examples and not having a name for that argument confuses beginners, we decided to have it present. :any:`examples.echobot` ----------------------- This is probably the base for most of the bots made with ``python-telegram-bot``. It simply replies to each text message with a message that contains the same text. :any:`examples.timerbot` ------------------------ This bot uses the :class:`telegram.ext.JobQueue` class to send timed messages. The user sets a timer by using ``/set`` command with a specific time, for example ``/set 30``. The bot then sets up a job to send a message to that user after 30 seconds. The user can also cancel the timer by sending ``/unset``. To learn more about the ``JobQueue``, read `this wiki article `__. Note: To use ``JobQueue``, you must install PTB via ``pip install "python-telegram-bot[job-queue]"`` :any:`examples.conversationbot` ------------------------------- A common task for a bot is to ask information from the user. In v5.0 of this library, we introduced the :class:`telegram.ext.ConversationHandler` for that exact purpose. This example uses it to retrieve user-information in a conversation-like style. To get a better understanding, take a look at the :ref:`state diagram `. :any:`examples.conversationbot2` -------------------------------- A more complex example of a bot that uses the ``ConversationHandler``. It is also more confusing. Good thing there is a :ref:`fancy state diagram `. for this one, too! :any:`examples.nestedconversationbot` ------------------------------------- A even more complex example of a bot that uses the nested ``ConversationHandler``\ s. While it’s certainly not that complex that you couldn’t built it without nested ``ConversationHanldler``\ s, it gives a good impression on how to work with them. Of course, there is a :ref:`fancy state diagram ` for this example, too! :any:`examples.persistentconversationbot` ----------------------------------------- A basic example of a bot store conversation state and user_data over multiple restarts. :any:`examples.inlinekeyboard` ------------------------------ This example sheds some light on inline keyboards, callback queries and message editing. A wiki site explaining this examples lives `here `__. :any:`examples.inlinekeyboard2` ------------------------------- A more complex example about inline keyboards, callback queries and message editing. This example showcases how an interactive menu could be build using inline keyboards. :any:`examples.deeplinking` --------------------------- A basic example on how to use deeplinking with inline keyboards. :any:`examples.inlinebot` ------------------------- A basic example of an `inline bot `__. Don’t forget to enable inline mode with `@BotFather `_. :any:`examples.pollbot` ----------------------- This example sheds some light on polls, poll answers and the corresponding handlers. :any:`examples.passportbot` --------------------------- A basic example of a bot that can accept passports. Use in combination with the :ref:`HTML page `. Don’t forget to enable and configure payments with `@BotFather `_. Check out this `guide `__ on Telegram passports in PTB. Note: To use Telegram Passport, you must install PTB via ``pip install "python-telegram-bot[passport]"`` :any:`examples.paymentbot` -------------------------- A basic example of a bot that can accept payments. Don’t forget to enable and configure payments with `@BotFather `_. :any:`examples.errorhandlerbot` ------------------------------- A basic example on how to set up a custom error handler. :any:`examples.chatmemberbot` ----------------------------- A basic example on how ``(my_)chat_member`` updates can be used. :any:`examples.webappbot` ------------------------- A basic example of how `Telegram WebApps `__ can be used. Use in combination with the :ref:`HTML page `. For your convenience, this file is hosted by the PTB team such that you don’t need to host it yourself. Uses the `iro.js `__ JavaScript library to showcase a user interface that is hard to achieve with native Telegram functionality. :any:`examples.contexttypesbot` ------------------------------- This example showcases how ``telegram.ext.ContextTypes`` can be used to customize the ``context`` argument of handler and job callbacks. :any:`examples.customwebhookbot` -------------------------------- This example showcases how a custom webhook setup can be used in combination with ``telegram.ext.Application``. :any:`examples.arbitrarycallbackdatabot` ---------------------------------------- This example showcases how PTBs “arbitrary callback data” feature can be used. Note: To use arbitrary callback data, you must install PTB via ``pip install "python-telegram-bot[callback-data]"`` Pure API -------- The :any:`examples.rawapibot` example example uses only the pure, “bare-metal” API wrapper. .. toctree:: :hidden: examples.arbitrarycallbackdatabot examples.chatmemberbot examples.contexttypesbot examples.conversationbot examples.conversationbot2 examples.customwebhookbot examples.deeplinking examples.echobot examples.errorhandlerbot examples.inlinebot examples.inlinekeyboard examples.inlinekeyboard2 examples.nestedconversationbot examples.passportbot examples.paymentbot examples.persistentconversationbot examples.pollbot examples.rawapibot examples.timerbot examples.webappbot python-telegram-bot-21.1.1/docs/source/examples.timerbot.rst000066400000000000000000000001661460724040100241220ustar00rootroot00000000000000``timerbot.py`` =============== .. literalinclude:: ../../examples/timerbot.py :language: python :linenos: python-telegram-bot-21.1.1/docs/source/examples.webappbot.rst000066400000000000000000000003661460724040100242620ustar00rootroot00000000000000``webappbot.py`` ================ .. literalinclude:: ../../examples/webappbot.py :language: python :linenos: .. _webappbot-html: HTML Page --------- .. literalinclude:: ../../examples/webappbot.html :language: html :linenos: python-telegram-bot-21.1.1/docs/source/inclusions/000077500000000000000000000000001460724040100221115ustar00rootroot00000000000000python-telegram-bot-21.1.1/docs/source/inclusions/application_run_tip.rst000066400000000000000000000012021460724040100267010ustar00rootroot00000000000000.. tip:: * When combining ``python-telegram-bot`` with other :mod:`asyncio` based frameworks, using this method is likely not the best choice, as it blocks the event loop until it receives a stop signal as described above. Instead, you can manually call the methods listed below to start and shut down the application and the :attr:`~telegram.ext.Application.updater`. Keeping the event loop running and listening for a stop signal is then up to you. * To gracefully stop the execution of this method from within a handler, job or error callback, use :meth:`~telegram.ext.Application.stop_running`.python-telegram-bot-21.1.1/docs/source/inclusions/bot_methods.rst000066400000000000000000000352321460724040100251570ustar00rootroot00000000000000.. raw:: html

Since this class has a large number of methods and attributes, below you can find a quick overview.

Sending Messages .. list-table:: :align: left :widths: 1 4 * - :meth:`~telegram.Bot.send_animation` - Used for sending animations * - :meth:`~telegram.Bot.send_audio` - Used for sending audio files * - :meth:`~telegram.Bot.send_chat_action` - Used for sending chat actions * - :meth:`~telegram.Bot.send_contact` - Used for sending contacts * - :meth:`~telegram.Bot.send_dice` - Used for sending dice messages * - :meth:`~telegram.Bot.send_document` - Used for sending documents * - :meth:`~telegram.Bot.send_game` - Used for sending a game * - :meth:`~telegram.Bot.send_invoice` - Used for sending an invoice * - :meth:`~telegram.Bot.send_location` - Used for sending location * - :meth:`~telegram.Bot.send_media_group` - Used for sending media grouped together * - :meth:`~telegram.Bot.send_message` - Used for sending text messages * - :meth:`~telegram.Bot.send_photo` - Used for sending photos * - :meth:`~telegram.Bot.send_poll` - Used for sending polls * - :meth:`~telegram.Bot.send_sticker` - Used for sending stickers * - :meth:`~telegram.Bot.send_venue` - Used for sending venue locations. * - :meth:`~telegram.Bot.send_video` - Used for sending videos * - :meth:`~telegram.Bot.send_video_note` - Used for sending video notes * - :meth:`~telegram.Bot.send_voice` - Used for sending voice messages * - :meth:`~telegram.Bot.copy_message` - Used for copying the contents of an arbitrary message * - :meth:`~telegram.Bot.copy_messages` - Used for copying the contents of an multiple arbitrary messages * - :meth:`~telegram.Bot.forward_message` - Used for forwarding messages * - :meth:`~telegram.Bot.forward_messages` - Used for forwarding multiple messages at once .. raw:: html

.. raw:: html
Updating Messages .. list-table:: :align: left :widths: 1 4 * - :meth:`~telegram.Bot.answer_callback_query` - Used for answering the callback query * - :meth:`~telegram.Bot.answer_inline_query` - Used for answering the inline query * - :meth:`~telegram.Bot.answer_pre_checkout_query` - Used for answering a pre checkout query * - :meth:`~telegram.Bot.answer_shipping_query` - Used for answering a shipping query * - :meth:`~telegram.Bot.answer_web_app_query` - Used for answering a web app query * - :meth:`~telegram.Bot.delete_message` - Used for deleting messages. * - :meth:`~telegram.Bot.delete_messages` - Used for deleting multiple messages as once. * - :meth:`~telegram.Bot.edit_message_caption` - Used for editing captions * - :meth:`~telegram.Bot.edit_message_media` - Used for editing the media on messages * - :meth:`~telegram.Bot.edit_message_live_location` - Used for editing the location in live location messages * - :meth:`~telegram.Bot.edit_message_reply_markup` - Used for editing the reply markup on messages * - :meth:`~telegram.Bot.edit_message_text` - Used for editing text messages * - :meth:`~telegram.Bot.stop_poll` - Used for stopping the running poll * - :meth:`~telegram.Bot.set_message_reaction` - Used for setting reactions on messages .. raw:: html

.. raw:: html
Chat Moderation and information .. list-table:: :align: left :widths: 1 4 * - :meth:`~telegram.Bot.approve_chat_join_request` - Used for approving a chat join request * - :meth:`~telegram.Bot.decline_chat_join_request` - Used for declining a chat join request * - :meth:`~telegram.Bot.ban_chat_member` - Used for banning a member from the chat * - :meth:`~telegram.Bot.unban_chat_member` - Used for unbanning a member from the chat * - :meth:`~telegram.Bot.ban_chat_sender_chat` - Used for banning a channel in a channel or supergroup * - :meth:`~telegram.Bot.unban_chat_sender_chat` - Used for unbanning a channel in a channel or supergroup * - :meth:`~telegram.Bot.restrict_chat_member` - Used for restricting a chat member * - :meth:`~telegram.Bot.promote_chat_member` - Used for promoting a chat member * - :meth:`~telegram.Bot.set_chat_administrator_custom_title` - Used for assigning a custom admin title to an admin * - :meth:`~telegram.Bot.set_chat_permissions` - Used for setting the permissions of a chat * - :meth:`~telegram.Bot.export_chat_invite_link` - Used for creating a new primary invite link for a chat * - :meth:`~telegram.Bot.create_chat_invite_link` - Used for creating an additional invite link for a chat * - :meth:`~telegram.Bot.edit_chat_invite_link` - Used for editing a non-primary invite link * - :meth:`~telegram.Bot.revoke_chat_invite_link` - Used for revoking an invite link created by the bot * - :meth:`~telegram.Bot.set_chat_photo` - Used for setting a photo to a chat * - :meth:`~telegram.Bot.delete_chat_photo` - Used for deleting a chat photo * - :meth:`~telegram.Bot.set_chat_title` - Used for setting a chat title * - :meth:`~telegram.Bot.set_chat_description` - Used for setting the description of a chat * - :meth:`~telegram.Bot.pin_chat_message` - Used for pinning a message * - :meth:`~telegram.Bot.unpin_chat_message` - Used for unpinning a message * - :meth:`~telegram.Bot.unpin_all_chat_messages` - Used for unpinning all pinned chat messages * - :meth:`~telegram.Bot.get_business_connection` - Used for getting information about the business account. * - :meth:`~telegram.Bot.get_user_profile_photos` - Used for obtaining user's profile pictures * - :meth:`~telegram.Bot.get_chat` - Used for getting information about a chat * - :meth:`~telegram.Bot.get_chat_administrators` - Used for getting the list of admins in a chat * - :meth:`~telegram.Bot.get_chat_member_count` - Used for getting the number of members in a chat * - :meth:`~telegram.Bot.get_chat_member` - Used for getting a member of a chat * - :meth:`~telegram.Bot.get_user_chat_boosts` - Used for getting the list of boosts added to a chat * - :meth:`~telegram.Bot.leave_chat` - Used for leaving a chat .. raw:: html

.. raw:: html
Bot settings .. list-table:: :align: left :widths: 1 4 * - :meth:`~telegram.Bot.set_my_commands` - Used for setting the list of commands * - :meth:`~telegram.Bot.delete_my_commands` - Used for deleting the list of commands * - :meth:`~telegram.Bot.get_my_commands` - Used for obtaining the list of commands * - :meth:`~telegram.Bot.get_my_default_administrator_rights` - Used for obtaining the default administrator rights for the bot * - :meth:`~telegram.Bot.set_my_default_administrator_rights` - Used for setting the default administrator rights for the bot * - :meth:`~telegram.Bot.get_chat_menu_button` - Used for obtaining the menu button of a private chat or the default menu button * - :meth:`~telegram.Bot.set_chat_menu_button` - Used for setting the menu button of a private chat or the default menu button * - :meth:`~telegram.Bot.set_my_description` - Used for setting the description of the bot * - :meth:`~telegram.Bot.get_my_description` - Used for obtaining the description of the bot * - :meth:`~telegram.Bot.set_my_short_description` - Used for setting the short description of the bot * - :meth:`~telegram.Bot.get_my_short_description` - Used for obtaining the short description of the bot * - :meth:`~telegram.Bot.set_my_name` - Used for setting the name of the bot * - :meth:`~telegram.Bot.get_my_name` - Used for obtaining the name of the bot .. raw:: html

.. raw:: html
Stickerset management .. list-table:: :align: left :widths: 1 4 * - :meth:`~telegram.Bot.add_sticker_to_set` - Used for adding a sticker to a set * - :meth:`~telegram.Bot.delete_sticker_from_set` - Used for deleting a sticker from a set * - :meth:`~telegram.Bot.create_new_sticker_set` - Used for creating a new sticker set * - :meth:`~telegram.Bot.delete_sticker_set` - Used for deleting a sticker set made by a bot * - :meth:`~telegram.Bot.set_chat_sticker_set` - Used for setting a sticker set of a chat * - :meth:`~telegram.Bot.delete_chat_sticker_set` - Used for deleting the set sticker set of a chat * - :meth:`~telegram.Bot.replace_sticker_in_set` - Used for replacing a sticker in a set * - :meth:`~telegram.Bot.set_sticker_position_in_set` - Used for moving a sticker's position in the set * - :meth:`~telegram.Bot.set_sticker_set_title` - Used for setting the title of a sticker set * - :meth:`~telegram.Bot.set_sticker_emoji_list` - Used for setting the emoji list of a sticker * - :meth:`~telegram.Bot.set_sticker_keywords` - Used for setting the keywords of a sticker * - :meth:`~telegram.Bot.set_sticker_mask_position` - Used for setting the mask position of a mask sticker * - :meth:`~telegram.Bot.set_sticker_set_thumbnail` - Used for setting the thumbnail of a sticker set * - :meth:`~telegram.Bot.set_custom_emoji_sticker_set_thumbnail` - Used for setting the thumbnail of a custom emoji sticker set * - :meth:`~telegram.Bot.get_sticker_set` - Used for getting a sticker set * - :meth:`~telegram.Bot.upload_sticker_file` - Used for uploading a sticker file * - :meth:`~telegram.Bot.get_custom_emoji_stickers` - Used for getting custom emoji files based on their IDs .. raw:: html

.. raw:: html
Games .. list-table:: :align: left :widths: 1 4 * - :meth:`~telegram.Bot.get_game_high_scores` - Used for getting the game high scores * - :meth:`~telegram.Bot.set_game_score` - Used for setting the game score .. raw:: html

.. raw:: html
Getting updates .. list-table:: :align: left :widths: 1 4 * - :meth:`~telegram.Bot.get_updates` - Used for getting updates using long polling * - :meth:`~telegram.Bot.get_webhook_info` - Used for getting current webhook status * - :meth:`~telegram.Bot.set_webhook` - Used for setting a webhook to receive updates * - :meth:`~telegram.Bot.delete_webhook` - Used for removing webhook integration .. raw:: html

.. raw:: html
Forum topic management .. list-table:: :align: left :widths: 1 4 * - :meth:`~telegram.Bot.close_forum_topic` - Used for closing a forum topic * - :meth:`~telegram.Bot.close_general_forum_topic` - Used for closing the general forum topic * - :meth:`~telegram.Bot.create_forum_topic` - Used to create a topic * - :meth:`~telegram.Bot.delete_forum_topic` - Used for deleting a forum topic * - :meth:`~telegram.Bot.edit_forum_topic` - Used to edit a topic * - :meth:`~telegram.Bot.edit_general_forum_topic` - Used to edit the general topic * - :meth:`~telegram.Bot.get_forum_topic_icon_stickers` - Used to get custom emojis to use as topic icons * - :meth:`~telegram.Bot.hide_general_forum_topic` - Used to hide the general topic * - :meth:`~telegram.Bot.unhide_general_forum_topic` - Used to unhide the general topic * - :meth:`~telegram.Bot.reopen_forum_topic` - Used to reopen a topic * - :meth:`~telegram.Bot.reopen_general_forum_topic` - Used to reopen the general topic * - :meth:`~telegram.Bot.unpin_all_forum_topic_messages` - Used to unpin all messages in a forum topic * - :meth:`~telegram.Bot.unpin_all_general_forum_topic_messages` - Used to unpin all messages in the general forum topic .. raw:: html

.. raw:: html
Miscellaneous .. list-table:: :align: left :widths: 1 4 * - :meth:`~telegram.Bot.create_invoice_link` - Used to generate an HTTP link for an invoice * - :meth:`~telegram.Bot.close` - Used for closing server instance when switching to another local server * - :meth:`~telegram.Bot.log_out` - Used for logging out from cloud Bot API server * - :meth:`~telegram.Bot.get_file` - Used for getting basic info about a file * - :meth:`~telegram.Bot.get_me` - Used for getting basic information about the bot .. raw:: html

.. raw:: html
Properties .. list-table:: :align: left :widths: 1 4 * - :attr:`~telegram.Bot.base_file_url` - Telegram Bot API file URL * - :attr:`~telegram.Bot.base_url` - Telegram Bot API service URL * - :attr:`~telegram.Bot.bot` - The user instance of the bot as returned by :meth:`~telegram.Bot.get_me` * - :attr:`~telegram.Bot.can_join_groups` - Whether the bot can join groups * - :attr:`~telegram.Bot.can_read_all_group_messages` - Whether the bot can read all incoming group messages * - :attr:`~telegram.Bot.id` - The user id of the bot * - :attr:`~telegram.Bot.name` - The username of the bot, with leading ``@`` * - :attr:`~telegram.Bot.first_name` - The first name of the bot * - :attr:`~telegram.Bot.last_name` - The last name of the bot * - :attr:`~telegram.Bot.local_mode` - Whether the bot is running in local mode * - :attr:`~telegram.Bot.username` - The username of the bot, without leading ``@`` * - :attr:`~telegram.Bot.link` - The t.me link of the bot * - :attr:`~telegram.Bot.private_key` - Deserialized private key for decryption of telegram passport data * - :attr:`~telegram.Bot.supports_inline_queries` - Whether the bot supports inline queries * - :attr:`~telegram.Bot.token` - Bot's unique authentication token .. raw:: html


python-telegram-bot-21.1.1/docs/source/inclusions/menu_button_command_video.rst000066400000000000000000000003201460724040100300610ustar00rootroot00000000000000.. raw:: html
python-telegram-bot-21.1.1/docs/source/inclusions/pool_size_tip.rst000066400000000000000000000016051460724040100255240ustar00rootroot00000000000000.. tip:: When making requests to the Bot API in an asynchronous fashion (e.g. via :attr:`block=False `, :meth:`Application.create_task `, :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates` or the :class:`~telegram.ext.JobQueue`), it can happen that more requests are being made in parallel than there are connections in the pool. If the number of requests is much higher than the number of connections, even setting :meth:`~telegram.ext.ApplicationBuilder.pool_timeout` to a larger value may not always be enough to prevent pool timeouts. You should therefore set :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, :meth:`~telegram.ext.ApplicationBuilder.connection_pool_size` and :meth:`~telegram.ext.ApplicationBuilder.pool_timeout` to values that make sense for your setup.python-telegram-bot-21.1.1/docs/source/index.rst000066400000000000000000000020321460724040100215610ustar00rootroot00000000000000.. Python Telegram Bot documentation master file, created by sphinx-quickstart on Mon Aug 10 22:25:07 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. .. include:: ../../README.rst .. The toctrees are hidden such that they don't render on the start page but still include the contents into the documentation. .. toctree:: :hidden: :caption: Reference telegram telegram.ext telegram_auxil Telegrams Bot API Docs .. toctree:: :hidden: :caption: Resources examples Wiki .. toctree:: :hidden: :caption: Project stability_policy changelog coc contributing testing Website GitHub Repository Telegram Channel Telegram User Group python-telegram-bot-21.1.1/docs/source/ptb-logo-orange.ico000066400000000000000000013226261460724040100234270ustar00rootroot00000000000000 hf 00 %v@@ (B; (F} ( n(  @Y))YdODNI5if',yM'"&](Q,&~*U0>5 .Ysg2]:>|6a?94/K:rSNJE.)$>יזדו3.)$BmTdB=83.(Fr']B=72-JrOGA<72`CkRKFBUp׺E?OO=( @ km}9w  pE xKEEKpE PB"*A><:"0 %n*'$" P%%o,)'$"D6 O%%q.,)'$*{k P%%s1.,)&Z Q%%t30.+)B S%%v5308nt U%%w8P V%%y:=X%%{=:75Y&Y%%|?<:7426tT[%%~A?<9742/,E]%%DA><9741/,*)`x^%%FCA><9641/,)'$"`%%geca_]ZXV1.,)'$!b%31.,)&$!c%%XUSQNLIGD630.+)&$!e%%OMJHEB@=;8530.+(&#f%%ROhjEB@=:8530-+(&h%%T`uaEB?=:8520-+(j%V~9#GDB?=:7520-*k%Y[GWIGDA?<:752/-^YYsVNLIFDA?<97421eXVSQNKIFDA><97A!ݒpWPNKIFCG^MqmK(0` %  ; ݤE'&NR ;H #6  7  _4 S*'%$"  7+)'%$"  x77,*)'%#" / w77.,*('%#" 0d dn w77/.,*('%#!n! w771/-,*(&%#H x7721/-+*(&2 y77421/-+*(cN z776420/-+) z77754203b {77975l |77:97. }77<:87c^ ~77=<:865>77?=<:86531Q77A?=;:86431/3m77B@?=;986431/-,@@77DB@?=;986421/-+*)[i77EDB@>=;976421/-+*(&2x?77GECB@><;976420/-+)(&$#!77HGECB@><;975420.-+)(&$"!77xwvtsrponmkjig20.-+)'&$" 7320.,+)'&$" 775310.,+)'%$" 77OMKJHFDCA?=<:875310.,*)'%#" 77POMKIHFDBA?=<:86531/.,*)'%#"77RPNMKIGFDBA?=;:86431/.,*('%#77SRPNdFDB@?=;986431/-,*(&%77USR\EDB@?=;986421/-+*(&75WUS=GEDB@>=;976421/-+*(7)XVU7IGECB@><;976420/-+)7 ZXVifJHGECB@><;975420.-+%^ZXWwMLJHGECA@><:975420.-c[YXVTSQOMLJHFECA?><:975320o}[YXVTRQOMKJHFECA?><:8753hwYWVTRPOMKJHFDCA?=<:8Y '߸w^RPOMKIHFDCOg'!S}{S!(@ BEks_13_siE s_u%[g ;$Sk xqqwqq wqS! wO{vvJ"! _lkjiihgfW ,^^b-*('&%#"! 'I+*('&$#"! II,+*('&$#"  II-,+)('&$#" ] II/-,+)('%$#" ft$I II0.-,+)('%$#!!&9} II10.-,*)(&%$#D'l II21/.-,*)(&%$w II321/.-+*)(&%' II4321/.-+*)':Z II64320/.-+*)k II754320/.,+* II87543100V II98756g9 II:986i II;:98M II=;:976D II>=;:97654[II?><;:9765329xIII@?><;:8765321/KwIIA@?><;:8764321/.0gIICA@?=<;:8764320/.-+<IIDBA@?=<;98754320/.,+*)U,IIEDBA@>=<;98754310/.,+*('0s@IIFEDBA@>=<:98754310/-,+*('&$D IIGFECBA@>=<:98654310/-,+)('&$#" IIHGFECBA?>=<:98654210/-,+)('%$#" IIJHGFDCBA?>=;:97654210.-,+)('%$#! II~}|{zzx210.-,*)('%$#! I+321/.-,*)(&%$#! I95321/.-+*)(&%$"! IIdca`_^]\[YXWVUTSQPO64321/.-+*)'&%$"! IIPNMLJIHFEDCA@?=<;:8764320/.-+*)'&%#"! IIQONMLJIHFEDBA@?=<;98764320/.,+*)'&%#"!IIRQONMKJIHFEDBA@>=<;98754310/.,+*('&%#"IISRQONMKJIGFEDBA@>=<:98754310/-,+*('&$#IITSRPOW\FECBA@>=<:98654310/-,+)('&$IIUTSRS aUFECBA?>=<:98654210/-,+)('%IIWUTS}GFDCBA?>=;:98654210.-,+)('ICXWUTe7HGFDCB@?>=;:97654210.-,*)(I5YXVUQIHGFDCB@?><;:9765321/.-,*)GZYXVh zKIHGEDCB@?><;:8765321/.-+*9gZYWVߟMLKIHGEDCA@?><;:8764321/.-,[ZYWVbgPNMLJIHFEDCA@?=<;:8764320/.e#g[ZYWVUSRQONMLJIHFEDBA@?=<;987643206=me[ZXWVUSRQONMKJIHFEDBA@>=<;987543:w[ZXWVTSRQONMKJIGFEDBA@>=<:9875g9fXWVTSRPONMKJIGFECBA@>=<:G;9~eTRPONLKJIGFECEWp9!WW! -9ACC?7)( 9qG Gq99%3xJ#=i_\- 8k_)(\[i sc$U;4K@yr[ &#!## ## ##! ## "! 33  y""!  g#""!   =(''&%%$##"!!  $7,))(''&%%$##"!!   %w}**)((''&%%$##"!!  oqb+**)(('&&%%$##"!!  Ca,+**)(('&&%$$##"!!  @a,,+**)(('&&%$$#""!!  @b-,,+**)(('&&%$$#""!  @b--,++*))(('&&%$$#""! (k @c.--,++*))(''&&%$$#""! T# @c/.--,++*))(''&%%$$#""!  YU*j @d//.--,++*))(''&%%$##""! W@1 @d0/..--,++*))(''&%%$##"!! C` @e00/..-,,++*))(''&%%$##"!!#G @e100/..-,,+**)((''&%%$##"!LJ;H @f2100/..-,,+**)(('&&%%$##"Ml| @f21100/..-,,+**)(('&&%$$##P% Ag32110//..-,,+**)(('&&%$$@ Ag332110//.--,,+**)(('&&%$r& Bg4332110//.--,++**)(('&&%X Bh44332110//.--,++*))(''&4 Ch544322110//.--,++*))(''e Ci6544322100//.--,++*))(' Ci66544322100/..--,++*)).6 Dj766544322100/..-,,++*)Xg Dj7765544322100/..-,,+** Ej87765543322100/..--S Ek9877655433211003b Fk998776554332;pE Fl:98877655=}u Gl::988766I Gm;::98876d Gm<;::98876% Hm<;;::98876MT Hn=<;;:99887668o In==<;;:998776654F Io>==<;;:99877655443` Jo?>==<;;:998776554332<}3Jp?>>==<;;:99877655433211QbJp@?>>=<<;::9987765543321104nKq@@?>>=<<;::988776554332110//CKqA@@?>>=<<;::988766554332110//..^LrAA@@?>>=<<;::988766544332110//.--8|BLrBAA@??>>=<<;::988766544322110//.--,+MpMrCBAA@??>==<<;::988766544322100//.--,++/mMsCCBAA@??>==<;;::988766544322100/..--,++*)@NsDCCBAA@??>==<;;:9987766544322100/..-,,++*)))]&NtDDCBBAA@??>==<;;:9987765544322100/..-,,+**))('4{QOtEDDCBBA@@??>==<;;:9987765543322100/..-,,+**)((''&MlOuFEDDCBBA@@?>>==<;;:9987765543321100/..-,,+**)(('&&%+kZOuFFEDDCBBA@@?>>=<<;;:998776554332110//..-,,+**)(('&&%$$=#PvGFEEDDCBBA@@?>>=<<;::998776554332110//.--,,+**)(('&&%$$#$QZ& PvGGFEEDCCBBA@@?>>=<<;::988766554332110//.--,++**)(('&&%$$#""! QvHGGFEEDCCBAA@@?>>=<<;::988766544332110//.--,++*))(('&&%$$#""! QwIHGGFEEDCCBAA@??>>=<<;::988766544322110//.--,++*))(''&&%$$#""! RwIHHGGFEEDCCBAA@??>==<<;::988766544322100//.--,++*))(''&%%$$#""! RxJIHHGFFEEDCCBAA@??>==<;;::988766544322100/..--,++*))(''&%%$##""! SxJJIHHGFFEDDCCBAA@??>==<;;:9987766544322100/..-,,++*))(''&%%$##"!! SyKJJIHHGFFEDDCBBAA@??>==<;;:9987765544322100/..-,,+**))(''&%%$##"!! S322100/..-,,+**)((''&%%$##"!! T3321100/..-,,+**)(('&&%%$##"!! T%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%4332110//..-,,+**)(('&&%$$##"!! US54332110//.--,,+**)(('&&%$$#""!! U554332110//.--,++**)(('&&%$$#""! Vyxxxwwvvuuttssrrqqppoonnmmllkkjjiihhgf6544332110//.--,++*))(('&&%$$#""! V|ONNMLLKJJIIHGGFEEDCCBAA@??>>=<<;::988766544322110//.--,++*))(''&&%$$#""! V|PONNMLLKJJIHHGGFEEDCCBAA@??>==<<;::988766544322100//.--,++*))(''&%%$$#""! W|PPONNMLLKJJIHHGFFEEDCCBAA@??>==<;;::988766544322100/..--,++*))(''&%%$##""! W}QPPONNMLLKJJIHHGFFEDDCCBAA@??>==<;;:9988766544322100/..-,,++*))(''&%%$##"!! X}QQPOONNMLLKJJIHHGFFEDDCBBAA@??>==<;;:9987766544322100/..-,,+**))(''&%%$##"!!X~RQQPOONMMLLKJJIHHGFFEDDCBBA@@??>==<;;:9987765543322100/..-,,+**)((''&%%$##"!X~SRQQPOONMMLKKJIIHHGFFEDDCBBA@@?>>==<;;:9987765543321100/..-,,+**)(('&&%%$##"YSSRQQPOONMMLKKJIIHGGFFEDDCBBA@@?>>=<<;;:998776554332110//..-,,+**)(('&&%$$##YTSRRQQPOONMMLKKJIIHGGFEEDDCBBA@@?>>=<<;::998776554332110//.--,,+**)(('&&%$$#ZTTSRRQPPOONMR^GGFEEDCCBBA@@?>>=<<;::988776554332110//.--,++**)(('&&%$$ZUTTSRRQPPONsHGFEEDCCBAA@@?>>=<<;::988766554332110//.--,++*))(('&&%$[VUTTSRRQPPw/sGGFEEDCCBAA@??>>=<<;::988766544322110//.--,++*))(''&&%[VUUTTSRRQZa#GGFEEDCCBAA@??>==<<;::988766544322100//.--,++*))(''&%\WVUUTSSRR9HGFFEEDCCBAA@??>==<;;::988766544322100/..--,++*))(''&\WWVUUTSSRiHGFFEDDCCBAA@??>==<;;:9988766544322100/..-,,++*))('']XWWVUUTSWuHHGFFEDDCBBAA@??>==<;;:9987766544322100/..-,,+**))(']XXWWVUUTaaIHHGFFEDDCBBA@@??>==<;;:9987765544322100/..-,,+**)((]sYXXWVVUUW{JIHHGFFEDDCBBA@@?>>==<;;:9987765543321100/..-,,+**)(^]ZYXXWVVUT#fJIIHGGFFEDDCBBA@@?>>=<<;;:998776554332110//..-,,+**)b=ZZYXXWVVUIKKJIIHGGFEEDDCBBA@@?>>=<<;::998776554332110//.--,,+**p}[ZZYXXWVVZy1yLKKJIIHGGFEEDCCBBA@@?>>=<<;::988776554332110//.--,++*a`[ZYYXXWVVsC )MMLKKJIIHGGFEEDCCBAA@@?>>=<<;::988766554332110//.--,++5[[ZYYXWWVVoNNMLLKKJIIHGGFEEDCCBAA@??>>=<<;::988766544332110//.--,/Q\[[ZYYXWWVUV\PONNMLLKJJIIHGGFEEDCCBAA@??>==<<;::988766544322100//.--lk\[[ZYYXWWVUUTSSRRQPPONNMLLKJJIHHGFFEEDCCBAA@??>==<;;::988766544322100/...?{\\[[ZYYXWWVUUTSSRQQPPONNMLLKJJIHHGFFEDDCCBAA@??>==<;;:9988766544322100/.w \\[ZZYYXWWVUUTSSRQQPOONNMLLKJJIHHGFFEDDCBBAA@??>==<;;:9987766544322100L1Ew\\[ZZYXXWWVUUTSSRQQPOONMMLLKJJIHHGFFEDDCBBA@@??>==<;;:99877655443221Euu~\\[ZZYXXWVVUUTSSRQQPOONMMLKKJJIHHGFFEDDCBBA@@?>>==<;;:998776554332S]\[ZZYXXWVVUTTSSRQQPOONMMLKKJIIHHGFFEDDCBBA@@?>>=<<;;:9987765544]\[ZZYXXWVVUTTSRRQQPOONMMLKKJIIHGGFEEDDCBBA@@?>>=<<;::9987765bg%]ZZYXXWVVUTTSRRQPPOONMMLKKJIIHGGFEEDCCBBA@@?>>=<<;::988:t)M^YXXWVVUTTSRRQPPONNMMLKKJIIHGGFEEDCCBAA@@?>>=<<;:?lO=cWVVUTTSRRQPPONNMLLKKJIIHGGFEEDCCBAA@??>>Lq? SkXSRRQPPONNMLLKJJIIHGGFEEDCCNczW %__'!IogC/AQamu}yqeYK9'( -YmACmW+Us33sUK+_ =- mk@ +R|5qC ,[/ >WQL )nW TiM&]++49QjZc e g qea<+B3K9:Fw_s# =t; kC/|kC?hkCCc kCCd lCCd lCCd lCCd! lCCd!! lCCd!!! lCCd"!!! mCCe""!!! mCCe %'''''''"""!!! m_''''''''''''''''''''''''''''''_e ''''''''!-""""!!! me {%#"""!!!! ne  Y##"""!!! ne !_###"""!!! A7 l{{{{{{{{{{{#/u.((('''&&&%%%$$$###"""!!!   5)))((('''&&&%%%$$$###"""!!!  7?2*)))(((('''&&&%%%$$$###"""!!!  6h***)))(((''''&&&%%%$$$###"""!!!  {,+***)))((('''&&&%%%%$$$###"""!!!  +++***)))((('''&&&%%%$$$$###"""!!!  ,+++***)))((('''&&&%%%$$$####"""!!!  #,,+++***)))((('''&&&%%%$$$###""""!!!  ##,,,+++***)))((('''&&&%%%$$$###"""!!!!  ##,,,,+++***)))((('''&&&%%%$$$###"""!!!  ##-,,,++++***)))((('''&&&%%%$$$###"""!!!  ##--,,,+++****)))((('''&&&%%%$$$###"""!!!  ##---,,,+++***))))((('''&&&%%%$$$###"""!!!  ##.---,,,+++***)))(((('''&&&%%%$$$###"""!!!  ##..---,,,+++***)))(((''''&&&%%%$$$###"""!!! *R ##...---,,,+++***)))((('''&&&&%%%$$$###"""!!! qQ ##/...---,,,+++***)))((('''&&&%%%%$$$###"""!!!  ##//...---,,,+++***)))((('''&&&%%%$$$####"""!!! B` ##//....---,,,+++***)))((('''&&&%%%$$$###""""!!! { ##///...----,,,+++***)))((('''&&&%%%$$$###"""!!!! &T$ ##0///...---,,,,+++***)))((('''&&&%%%$$$###"""!!! s(M& ##00///...---,,,++++***)))((('''&&&%%%$$$###"""!!! ,Z ##000///...---,,,+++****)))((('''&&&%%%$$$###"""!!! 6-, ##1000///...---,,,+++***))))((('''&&&%%%$$$###"""!!! l/U ##11000///...---,,,+++***)))(((('''&&&%%%$$$###"""!!! 1 ##111000///...---,,,+++***)))(((''''&&&%%%$$$###"""!!!-336 ##2111000///...---,,,+++***)))((('''&&&&%%%$$$###"""!!^5ai ##21111000///...---,,,+++***)))((('''&&&%%%%$$$###"""!8 ##221110000///...---,,,+++***)))((('''&&&%%%$$$$###""&:; ##222111000///....---,,,+++***)))((('''&&&%%%$$$###""P=l ##3222111000///...----,,,+++***)))((('''&&&%%%$$$###"@$E ##33222111000///...---,,,,+++***)))((('''&&&%%%$$$###CDx ##333222111000///...---,,,++++***)))((('''&&&%%%$$$#C ##4333222111000///...---,,,+++****)))((('''&&&%%%$$$u ##44333222111000///...---,,,+++***))))((('''&&&%%%$$# ##444333222111000///...---,,,+++***)))(((('''&&&%%%6U ##4443333222111000///...---,,,+++***)))(((''''&&&%%g ##54443332222111000///...---,,,+++***)))((('''&&&&% ##554443332221111000///...---,,,+++***)))((('''&&&- ##5554443332221110000///...---,,,+++***)))((('''&&Z2 ##6555444333222111000////...---,,,+++***)))((('''&c ##66555444333222111000///....---,,,+++***)))(((''( ##666555444333222111000///...---,,,,+++***)))((('L ##7666555444333222111000///...---,,,++++***)))(((~ ##76666555444333222111000///...---,,,+++****)))((@ ##776665555444333222111000///...---,,,+++***))))?r ##7776665554444333222111000///...---,,,+++***)))p ##87776665554443333222111000///...---,,,+++***)) ##887776665554443332222111000///...---,,,+++***5  ##8887776665554443332221111000///...---,,,+++**iO ##98887776665554443332221110000///...---,,,+N ##99888777666555444333222111000////...--/\ ##999888777666555444333222111000///..4j ##9999888777666555444333222111000/?w- ##:9998888777666555444333222111I] ##::999888777666655544433322V ##:::99988877766655554446d ##;:::9998887776665554Z ##;;:::99988877766656< ##;;;:::999888777666k ##<;;;:::99988877766 ##<<;;;:::9998887776x ##<<;;;;:::9998887776 ##<<<;;;::::9998887776|J ##=<<<;;;:::99998887776@y ##==<<<;;;:::9998888777667i ##===<<<;;;:::99988877776665C ##>===<<<;;;:::9998887776665555Z) ##>>===<<<;;;:::99988877766655544:wX ##>>>===<<<;;;:::9998887776665554443L ##>>>>===<<<;;;:::999888777666555444335i ##?>>>====<<<;;;:::9998887776665554443332A ##??>>>===<<<<;;;:::999888777666555444333222[7 ##???>>>===<<<;;;;:::99988877766655544433322219xe##@???>>>===<<<;;;::::999888777666555444333222111L##@@???>>>===<<<;;;:::999988877766655544433322211102i##@@@???>>>===<<<;;;:::9998888777666555444333222111000@##A@@@???>>>===<<<;;;:::9998887777666555444333222111000//ZE##AA@@@???>>>===<<<;;;:::9998887776666555444333222111000///8xs##AAA@@@???>>>===<<<;;;:::9998887776665554444333222111000///..L##AAA@@@????>>>===<<<;;;:::9998887776665554443333222111000///...0j##BAAA@@@???>>>>===<<<;;;:::9998887776665554443332222111000///...--@'##BBAAA@@@???>>>====<<<;;;:::9998887776665554443332221111000///...----[S##BBBAAA@@@???>>>===<<<<;;;:::9998887776665554443332221110000///...---,,5y##CBBBAAA@@@???>>>===<<<;;;;:::999888777666555444333222111000////...---,,,+I##CCBBBAAA@@@???>>>===<<<;;;::::999888777666555444333222111000///....---,,,++.i##CCCBBBAAA@@@???>>>===<<<;;;:::9999888777666555444333222111000///...----,,,+++*?4##DCCCBBBAAA@@@???>>>===<<<;;;:::9998888777666555444333222111000///...---,,,,+++***[a##DCCCCBBBAAA@@@???>>>===<<<;;;:::9998887777666555444333222111000///...---,,,++++***)5z##DDCCCBBBBAAA@@@???>>>===<<<;;;:::9998887776666555444333222111000///...---,,,+++****)))L##DDDCCCBBBAAAA@@@???>>>===<<<;;;:::9998887776665555444333222111000///...---,,,+++***)))((-k##EDDDCCCBBBAAA@@@@???>>>===<<<;;;:::9998887776665554443333222111000///...---,,,+++***)))((('?B##EEDDDCCCBBBAAA@@@???>>>>===<<<;;;:::9998887776665554443332222111000///...---,,,+++***)))(((''(\o##EEEDDDCCCBBBAAA@@@???>>>====<<<;;;:::9998887776665554443332221111000///...---,,,+++***)))((('''&3{##FEEEDDDCCCBBBAAA@@@???>>>===<<<<;;;:::9998887776665554443332221110000///...---,,,+++***)))((('''&&&M##FFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;;:::999888777666555444333222111000////...---,,,+++***)))((('''&&&%+l##FFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;::::999888777666555444333222111000///....---,,,+++***)))((('''&&&%%%>##FFFEEEEDDDCCCBBBAAA@@@???>>>===<<<;;;:::9999888777666555444333222111000///...----,,,+++***)))((('''&&&%%%$%]}##GFFFEEEDDDDCCCBBBAAA@@@???>>>===<<<;;;:::9998888777666555444333222111000///...---,,,,+++***)))((('''&&&%%%$$$3z0##GGFFFEEEDDDCCCCBBBAAA@@@???>>>===<<<;;;:::9998887777666555444333222111000///...---,,,++++***)))((('''&&&%%%$$$##LZ ##GGGFFFEEEDDDCCCBBBBAAA@@@???>>>===<<<;;;:::9998887776666555444333222111000///...---,,,+++****)))((('''&&&%%%$$$###)g6 ##HGGGFFFEEEDDDCCCBBBAAAA@@@???>>>===<<<;;;:::9998887776665555444333222111000///...---,,,+++***))))((('''&&&%%%$$$###"""!!! ##HHGGGFFFEEEDDDCCCBBBAAA@@@@???>>>===<<<;;;:::9998887776665554444333222111000///...---,,,+++***)))(((''''&&&%%%$$$###"""!!! ##HHHGGGFFFEEEDDDCCCBBBAAA@@@????>>>===<<<;;;:::9998887776665554443333222111000///...---,,,+++***)))((('''&&&&%%%$$$###"""!!! ##IHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>>===<<<;;;:::9998887776665554443332221111000///...---,,,+++***)))((('''&&&%%%%$$$###"""!!! ##IHHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<<;;;:::9998887776665554443332221110000///...---,,,+++***)))((('''&&&%%%$$$$###"""!!! ##IIHHHGGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;;:::999888777666555444333222111000////...---,,,+++***)))((('''&&&%%%$$$####"""!!! ##IIIHHHGGGFFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;::::999888777666555444333222111000///....---,,,+++***)))((('''&&&%%%$$$###""""!!! ##JIIIHHHGGGFFFEEEEDDDCCCBBBAAA@@@???>>>===<<<;;;:::9999888777666555444333222111000///...----,,,+++***)))((('''&&&%%%$$$###"""!!!! ##JJIIIHHHGGGFFFEEEDDDDCCCBBBAAA@@@???>>>===<<<;;;:::9998888777666555444333222111000///...---,,,,+++***)))((('''&&&%%%$$$###"""!!! ##JJJIIIHHHGGGFFFEEEDDDCCCCBBBAAA@@@???>>>===<<<;;;:::9998887777666555444333222111000///...---,,,++++***)))((('''&&&%%%$$$###"""!!! ##KJJJIIIHHHGGGFFFEEEDDDCCCBBBBAAA@@@???>>>===<<<;;;:::9998887776666555444333222111000///...---,,,+++****)))((('''&&&%%%$$$###"""!!! ##KKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAAA@@@???>>>===<<<;;;:::9998887776665555444333222111000///...---,,,+++***))))((('''&&&%%%$$$###"""!!! ##KKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@@???>>>===<<<;;;:::9998887776665554444333222111000///...---,,,+++***)))(((('''&&&%%%$$$###"""!!! ##KKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@????>>>===<<<;;;:::9998887776665554443333222111000///...---,,,+++***)))(((''''&&&%%%$$$###"""!!! ##LKKKJJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>>===<<<;;;:::9998887776665554443332222111000///...---,,,+++***)))((('''&&&%%%%$$$###"""!!! ##~~~|332221110000///...---,,,+++***)))((('''&&&%%%$$$$###"""!!! ##333222111000////...---,,,+++***)))((('''&&&%%%$$$####"""!!! ##4333222111000///....---,,,+++***)))((('''&&&%%%$$$###""""!!! ##44333222111000///...----,,,+++***)))((('''&&&%%%$$$###"""!!!! # IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII444333222111000///...---,,,,+++***)))((('''&&&%%%$$$###"""!!! #5444333222111000///...---,,,++++***)))((('''&&&%%%$$$###"""!!! #'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''55444333222111000///...---,,,+++****)))((('''&&&%%%$$$###"""!!! ##555444333222111000///...---,,,+++***))))((('''&&&%%%$$$###"""!!! ##5554444333222111000///...---,,,+++***)))(((('''&&&%%%$$$###"""!!! ##65554443333222111000///...---,,,+++***)))(((''''&&&%%%$$$###"""!!! ##665554443332222111000///...---,,,+++***)))((('''&&&&%%%$$$###"""!!! ##OOONNNMMMLLLKKKJJJIIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>====<<<;;;:::9998887776665554443332221110000///...---,,,+++***)))((('''&&&%%%$$$$###"""!!! ##POOONNNMMMLLLKKKJJJIIIHHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<<;;;:::999888777666555444333222111000////...---,,,+++***)))((('''&&&%%%$$$####"""!!! ##PPOOONNNMMMLLLKKKJJJIIIHHHGGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;::::999888777666555444333222111000///....---,,,+++***)))((('''&&&%%%$$$###""""!!! ##PPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEEDDDCCCBBBAAA@@@???>>>===<<<;;;:::9999888777666555444333222111000///...----,,,+++***)))((('''&&&%%%$$$###"""!!!! ##PPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDDCCCBBBAAA@@@???>>>===<<<;;;:::9998888777666555444333222111000///...---,,,,+++***)))((('''&&&%%%$$$###"""!!! ##QPPPOOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCCBBBAAA@@@???>>>===<<<;;;:::9998887777666555444333222111000///...---,,,++++***)))((('''&&&%%%$$$###"""!!! ##QQPPPOOONNNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBBAAA@@@???>>>===<<<;;;:::9998887776666555444333222111000///...---,,,+++****)))((('''&&&%%%$$$###"""!!! ##QQQPPPOOONNNMMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAAA@@@???>>>===<<<;;;:::9998887776665555444333222111000///...---,,,+++***))))((('''&&&%%%$$$###"""!!! ##RQQQPPPOOONNNMMMLLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@@???>>>===<<<;;;:::9998887776665554444333222111000///...---,,,+++***)))(((('''&&&%%%$$$###"""!!! ##RRQQQPPPOOONNNMMMLLLKKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@????>>>===<<<;;;:::9998887776665554443333222111000///...---,,,+++***)))(((''''&&&%%%$$$###"""!!! ##RRRQQQPPPOOONNNMMMLLLKKKJJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>>===<<<;;;:::9998887776665554443332222111000///...---,,,+++***)))((('''&&&&%%%$$$###"""!!!##SRRRQQQPPPOOONNNMMMLLLKKKJJJIIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>====<<<;;;:::9998887776665554443332221111000///...---,,,+++***)))((('''&&&%%%%$$$###"""!!##SSRRRQQQPPPOOONNNMMMLLLKKKJJJIIIHHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<<;;;:::9998887776665554443332221110000///...---,,,+++***)))((('''&&&%%%$$$####"""!##SSSRRRQQQPPPOOONNNMMMLLLKKKJJJIIIHHHGGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;;:::999888777666555444333222111000///....---,,,+++***)))((('''&&&%%%$$$###""""##SSSRRRRQQQPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;:::9999888777666555444333222111000///...----,,,+++***)))((('''&&&%%%$$$###"""##TSSSRRRQQQQPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDDCCCBBBAAA@@@???>>>===<<<;;;:::9998888777666555444333222111000///...---,,,,+++***)))((('''&&&%%%$$$###""##TTSSSRRRQQQPPPOOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCCBBBAAA@@@???>>>===<<<;;;:::9998887777666555444333222111000///...---,,,++++***)))((('''&&&%%%$$$###"##TTTSSSRRRQQQPPPOOONNNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBBAAA@@@???>>>===<<<;;;:::9998887776666555444333222111000///...---,,,+++****)))((('''&&&%%%$$$#####UTTTSSSRRRQQQPPPOOONNNMMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAAA@@@???>>>===<<<;;;:::9998887776665555444333222111000///...---,,,+++***))))((('''&&&%%%$$$####UUTTTSSSRRRQQQPPPOOONNNMMMLLRoIHHHGGGFFFEEEDDDCCCBBBAAA@@@@???>>>===<<<;;;:::9998887776665554444333222111000///...---,,,+++***)))(((('''&&&%%%$$$###UUUTTTSSSRRRQQQPPPOOONNNMMbPHHGGGFFFEEEDDDCCCBBBAAA@@@????>>>===<<<;;;:::9998887776665554443333222111000///...---,,,+++***)))(((''''&&&%%%$$$##VUUUTTTSSSRRRQQQPPPOOONNP|HHGGGFFFEEEDDDCCCBBBAAA@@@???>>>>===<<<;;;:::9998887776665554443332222111000///...---,,,+++***)))((('''&&&&%%%$$##VUUUUTTTSSSRRRQQQPPPOOO]IHGGGFFFEEEDDDCCCBBBAAA@@@???>>>====<<<;;;:::9998887776665554443332221111000///...---,,,+++***)))((('''&&&%%%%$##VVUUUTTTTSSSRRRQQQPPPO^3 IIHGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<<;;;:::9998887776665554443332221110000///...---,,,+++***)))((('''&&&%%%$##VVVUUUTTTSSSSRRRQQQPPU 'HGGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;;:::999888777666555444333222111000////...---,,,+++***)))((('''&&&%%%##WVVVUUUTTTSSSRRRRQQQPK}HGGGFFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;::::999888777666555444333222111000///...----,,,+++***)))((('''&&&%%##WWVVVUUUTTTSSSRRRQQQuEOHGGGFFFEEEEDDDCCCBBBAAA@@@???>>>===<<<;;;:::9998888777666555444333222111000///...---,,,,+++***)))((('''&&&%##WWWVVVUUUTTTSSSRRRQQwHHGGGFFFEEEDDDCCCCBBBAAA@@@???>>>===<<<;;;:::9998887777666555444333222111000///...---,,,++++***)))((('''&&&##XWWWVVVUUUTTTSSSRRRm%IHHGGGFFFEEEDDDCCCBBBBAAA@@@???>>>===<<<;;;:::9998887776666555444333222111000///...---,,,+++****)))((('''&&#!XXWWWVVVUUUTTTSSSRR]rHHHGGGFFFEEEDDDCCCBBBAAAA@@@???>>>===<<<;;;:::9998887776665555444333222111000///...---,,,+++***))))((('''&#XXXWWWVVVUUUTTTSSSR EIHHHGGGFFFEEEDDDCCCBBBAAA@@@@???>>>===<<<;;;:::9998887776665554444333222111000///...---,,,+++***)))(((('''#XXXWWWWVVVUUUTTTSST IIHHHGGGFFFEEEDDDCCCBBBAAA@@@????>>>===<<<;;;:::9998887776665554443333222111000///...---,,,+++***)))((('''#YXXXWWWVVVVUUUTTTSdIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>>===<<<;;;:::9998887776665554443332222111000///...---,,,+++***)))(((''# YYXXXWWWVVVUUUUTTToyIIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>====<<<;;;:::9998887776665554443332221111000///...---,,,+++***)))((('#YYYXXXWWWVVVUUUTTTn}JIIIHHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<<;;;:::9998887776665554443332221110000///...---,,,+++***)))(((#ZYYYXXXWWWVVVUUUTTaJJIIIHHHGGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;;:::999888777666555444333222111000////...---,,,+++***)))((#ZZYYYXXXWWWVVVUUUTTJJJIIIHHHGGGFFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;::::999888777666555444333222111000///....---,,,+++***)))(#ZZZYYYXXXWWWVVVUUUTWKJJJIIIHHHGGGFFFEEEEDDDCCCBBBAAA@@@???>>>===<<<;;;:::9999888777666555444333222111000///...----,,,+++***)))\ZZZYYYXXXWWWVVVUUUsgKKJJJIIIHHHGGGFFFEEEDDDDCCCBBBAAA@@@???>>>===<<<;;;:::9998887777666555444333222111000///...---,,,++++***))n[ZZZYYYXXXWWWVVVUUe?LKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBBAAA@@@???>>>===<<<;;;:::9998887776666555444333222111000///...---,,,+++****)i[ZZZZYYYXXXWWWVVVUU LLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAAA@@@???>>>===<<<;;;:::9998887776665555444333222111000///...---,,,+++***)A[[ZZZYYYXXXXWWWVVVUjmNLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@@???>>>===<<<;;;:::9998887776665554444333222111000///...---,,,+++***[[[ZZZYYYXXXWWWWVVVUwnMLLLKKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@????>>>===<<<;;;:::9998887776665554443333222111000///...---,,,+++*/\[[[ZZZYYYXXXWWWVVVVV%KMMMLLLKKKJJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>>===<<<;;;:::9998887776665554443332222111000///...---,,,+++Vl\[[[ZZZYYYXXXWWWVVVUZ]!/sNNMMMLLLKKKJJJIIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>====<<<;;;:::9998887776665554443332221111000///...---,,,++Me\\[[[ZZZYYYXXXWWWVVVUXONNNMMMLLLKKKJJJIIIHHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<<;;;:::9998887776665554443332221110000///...---,,,+\\\[[[ZZZYYYXXXWWWVVVUUmOOONNNMMMLLLKKKJJJIIIHHHGGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;;:::999888777666555444333222111000////...---,,9h\\\[[[ZZZYYYXXXWWWVVVUUYPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;::::999888777666555444333222111000///....---,ww]\\\[[[ZZZYYYXXXWWWVVVUUUTj]QQQPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEEDDDCCCBBBAAA@@@???>>>===<<<;;;:::9999888777666555444333222111000///...----^\\\\[[[ZZZYYYXXXWWWVVVUUUTTTSSSRRRQQQPPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDDCCCBBBAAA@@@???>>>===<<<;;;:::9998888777666555444333222111000///...--g]\\\[[[[ZZZYYYXXXWWWVVVUUUTTTSSSRRRQQQPPPOOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCCBBBAAA@@@???>>>===<<<;;;:::9998887776666555444333222111000///.../G7a]\\\[[[ZZZZYYYXXXWWWVVVUUUTTTSSSRRRQQQPPPOOONNNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAAA@@@???>>>===<<<;;;:::9998887776665555444333222111000///..}]]\\\[[[ZZZYYYYXXXWWWVVVUUUTTTSSSRRRQQQPPPOOONNNMMMLLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@@???>>>===<<<;;;:::9998887776665554444333222111000///C?-z]]\\\[[[ZZZYYYXXXWWWWVVVUUUTTTSSSRRRQQQPPPOOONNNMMMLLLKKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@????>>>===<<<;;;:::9998887776665554443333222111000/1f]]\\\[[[ZZZYYYXXXWWWVVVVUUUTTTSSSRRRQQQPPPOOONNNMMMLLLKKKJJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>>===<<<;;;:::9998887776665554443332222111000 _]]\\\[[[ZZZYYYXXXWWWVVVUUUUTTTSSSRRRQQQPPPOOONNNMMMLLLKKKJJJIIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>====<<<;;;:::99988877766655544433322211110M3^]]\\\[[[ZZZYYYXXXWWWVVVUUUTTTTSSSRRRQQQPPPOOONNNMMMLLLKKKJJJIIIHHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<<;;;:::999888777666555444333222111[`]]\\\[[[ZZZYYYXXXWWWVVVUUUTTTSSSSRRRQQQPPPOOONNNMMMLLLKKKJJJIIIHHHGGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;;:::9998887776665554443332222yh]]\\\[[[ZZZYYYXXXWWWVVVUUUTTTSSSRRRRQQQPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFFEEEDDDCCCBBBAAA@@@???>>>===<<<;;;::::99988877766655544433329~]]\\\[[[ZZZYYYXXXWWWVVVUUUTTTSSSRRRQQQQPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEEDDDCCCBBBAAA@@@???>>>===<<<;;;:::999988877766655544433T y`\\\\[[[ZZZYYYXXXWWWVVVUUUTTTSSSRRRQQQPPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDDCCCBBBAAA@@@???>>>===<<<;;;:::9998888777666555447U\\\[[[[ZZZYYYXXXWWWVVVUUUTTTSSSRRRQQQPPPOOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCCBBBAAA@@@???>>>===<<<;;;:::9998887777666555hm+}\\[[[ZZZZYYYXXXWWWVVVUUUTTTSSSRRRQQQPPPOOONNNNMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBBAAA@@@???>>>===<<<;;;:::9998887776666]9 \[[[ZZZYYYYXXXWWWVVVUUUTTTSSSRRRQQQPPPOOONNNMMMMLLLKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAAA@@@???>>>===<<<;;;:::9998887777i 5e[[ZZZYYYXXXXWWWVVVUUUTTTSSSRRRQQQPPPOOONNNMMMLLLKKKKJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@????>>>===<<<;;;:::999888C7U`ZZZYYYXXXWWWVVVVUUUTTTSSSRRRQQQPPPOOONNNMMMLLLKKKJJJJIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>>===<<<;;;:::99>wY[mZYYYXXXWWWVVVUUUUTTTSSSRRRQQQPPPOOONNNMMMLLLKKKJJJIIIIHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>====<<<;;;:N]GgYXXXWWWVVVUUUTTTTSSSRRRQQQPPPOOONNNMMMLLLKKKJJJIIIHHHHGGGFFFEEEDDDCCCBBBAAA@@@???>>>===<<>>Imw)}u[VUUUTTTSSSRRRRQQQPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFFEEEDDDCCCBBBAAA@@Rn/aydTSSSRRRQQQQPPPOOONNNMMMLLLKKKJJJIIIHHHGGGFFFEEEEDDDCCOcxg!+e|m`UPPPOOONNNMMMLLLKKKJJJIIIHHHGGMYdp}g1EwqC-WqK%5Ss{aC!-AUgyygWA+ python-telegram-bot-21.1.1/docs/source/ptb-logo-orange.png000077500000000000000000002234721460724040100234420ustar00rootroot00000000000000PNG  IHDR+sBIT|d pHYsFF&2[tEXtSoftwarewww.inkscape.org< IDATxyliY߿ow߾,wf̰# \@PD !5(h%CT(D#" ; ;3wz}GUKUy缧|sSNB!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!cIh{B!u7W-GmB B!DM4B!*`fۀ~jpFC?uuq+]e80p)dC=ُ[p?dHE!K4B1483N߳Y+嶭/\mG | ph@M!b@!D0~ϐ[[뿿uWjB!D@!D%EKX+[Y@ ww|O;BXliB!4B{W~/=LK"7+CCܦ!h B0}_W'D8No(pp;nBXnmB!:B!6`f3^$W>!&zg JLBw:!ţBL(fxO:!MCB8ڄBB1/w_~w[{BA !xBt3<3~5oKBtg|2ppC6&" !DG0G{a=u !&zg !osaB!B]LO%B.Á/\Bh -af{GOOJ"|Yupm_7,Z BIEB3 S8M U-az%.v@~0 Lɩf+,R9.<sφniuUB1fh 50+GmI8X>:Nt'v^b6[:#˛}zЎ蓬X0va`jNM`j:ms'h 1OinuQb#`,壽bKGNZWDdo ԡy+5z vu߰Y T | >}@!B ̦]y['{.j%X: Kl(,Y'mEI?o|j L ̹υs`;;A2|p-,!@4B̶{UI=.jY: K_a0x/gncȪpdy`Jkӊ1(-l΃{Ás 3{ rU88Lo׵!.I!G!DbfO'O MuyXux*{ /I8lf.`^>3셙s5 I->BBB@1_&ہꢺ!X ߳tϺW-Ъh';__19aA5{Vw]BG!XS+X: w>X[ʃh1GbJkgY?w`ń-ÖKz?'!ijx7C[^B$G!`fO'\ a6X޿,뽂z# Eu)ɿGTJga$ONn#eݧl7 ]\f09zg=!Z^B$A!Dg1Yz{t~j}|[}5cG FUs$5sb_wY7˙ :{`=Bt .?ZaNX n[{oEb: 99k?Lo0{^Nf/>@,0-G!*xJμ55InoaP6" {$ONnwHk V!\ٹ~xP-), -^?^ ~Df俵xɿ#bΖK`c ;;ӻ}fB8ZD!Dfv Rz}x7/_cZ80m]#s=$5sHR mɿ#>"V}ŽGv=7 z ?L>m/FMQf0Yz-ֿ?@9D9ߓ[=R`I-F+4.5{dOC1}6| acaד`u42WBh{1B@13{83-/g#K䧱S_ ,!v//*K!5 7l{ |a?WQ|x!O!Dh =OPڑ~8I{W췥uMɑ'ȑ/wW̑;rr-YO!G&"( koOm{-k;187&z$ONd|;zd=xl[[=ɿ''{rjg=ρ]|ȺYK!!D9h G=ϣWN| o'/?[俑.L=\ g};ᬧ2P?!϶!Dh Ąӿˁޫ?濾y?o|]I$ϔ#wHؾ "kdB!C!&3C~/Spc]˨LHHo3{"k&gx#h 0l ׁ>Ȗ'cSׁ-Q171͛ߴDA˧ [Ө#*m<{rr)7Q9EDNQG ]pS luHB ! kolm{xϺ;%)%s$Y%H̜|7g쥎~ɸxYmm.B1-N^}$)%s:&.`M%H欎yu5-^O͠cmWg7ؑcKGE4? uiD#J-ɿ#>"3=}6lXKmN{{omB/1٣?s7bG '?ř#VP#s#O[=9#e +F+4.5{dONn$Մ-[ {~v\# ;m4BE!ƌ@XSN]O1Mv//*K!5 獯2(8? ;ٯ6! #^o"'>[X-&ZT.S?o?CcU5<~kq'!7T lUL3M{#w{ D~\DџG]HmyoEk&ɩ+]W3}fӻ#R^BX=Boi8Gߋ;X:@.xx$r$gy?c|c]̹~< VǺ\ Xj(HBt3!@lw=" % <~#DrUa0}6ÞԶ|k,XQAl'߁ޏ+W%wH/?[俑x!9I%<8:#C6L S<*{>h%r˿'''2kM=RZƓȿ''2~CHkSGhP<{-T8?(܍@af?H6vԗ$#e?c?k?qœ5Ag`G } !Bt3~wY-8q`H(_1c?4,^8~]?aN!򿚝#\@G(B>w#!D=4p,w G߃z \Md=rZȜF?a͜1;#r$0aO.GU/s6BC! *[,޵j" j9D9ߓ[=R࿻bBN_GONnO#6~,GF0!ϓ%ȌB] ؓQGB_Da r$B YӰxg_[.;l"G! ̞O g{/vͰ|j"oZ~xU99K <? a6Ge'psBx40.71_i5&Z:L*- {$5IWWJ/x=p5A*s;!r5Bġaf ,KWA_Dт!#s׊M=9ߓ[H`%s׺Gi8Gz s6 BQ\B 䅗Oa(f5Fw]wx/ϔ#o$K&5 fC{gl?BxsBh D+'/pv1q4{gG-GH?Q0;ᝈ$ua!Du4E,~:y l1vUytGRHܵ&Q)-oIߓ[kH!5)B#r$rb;p5mʫB?bs4E/IZtv‰OxV'^7^1G5^8~]?aNn+"g%\Jqy+rBF!Z̮~-,YQxxƜ XӰxu99(+=4ao H B&uQ!h4KZ/SC9d?c?{|qɿ#>"G7>av_MWazH~!𛩋 ! cf#R[_u`A?y}wNng.ӸE=9ߓ[Hr_1{>/ަN]T1 h3voi .a KHK3įo)dM%y[W(+L \Iu7mOYT1 h3{,`w'z?+%ߴLa r$s$K{8﹑7mc!7,*؈B4]|0IؾW+bWh-Gx?*G8FX'G2Ay'\b`*2w(3Bb#3xlK} wt]#e| 95/IN+_G6ONnO#U|]ӈ}5WAuI!RBE!2bfKI . Y"GSɿG.Ȼ 2G7^!~UN߆~c#r`I.3fd4"f]hrJj/aiTm[Z4*>R؞$WHtxM]~ZG ńT4&SOKlm6*:r`zԤ IDAT8E_$W+!{-j>wY[=.iT7M)QBBL*Q_c_KWn-iVКFwMKs4HӚ;{d wmox{ޱm9EDNcwPlw}_d|hBBL"!j`flUVКFK~#] 2;"`>6p¾4t=j?ǯ!r˿!Gw%%|PZ[S[=jiT?AS`,,칁-"$N*x;V*zV'p/lMrZGdmB XS\ɿ#'>W5@B80AQ9$M_SW'!w~'|[dBx)pE GN}ޙ[=HQ99TON]xɿ#>"G7K_#x/[DIB"3;*8/eSn.Dӈ'L=9#e Z6޸E=9ߓAq)+ޗp75aR"_c=(]d?o?C.5{HƷ15lkdݺE4"3ZELQ;.9 r&64kH'l_ap'#6]fE4";'oՉiTm ;獗g_#O5IOK"wln! Z5fy:Eu4{Ȑt cԚ;{d wmox{ޱm9EDNc{PhM'ਵ_0-u1h DRk8gRLKdXG<$y㓟Ι+#]xRHܼΚx~B;QPS?vr˿!Gw%%|PZ[S[=H%$Kɰv]+&Ę`fQ;9Yk 4*UwJiyO& mOYaR)]КFH*:8y.Z1{|NϩS@qF!6's? F$H(@B6Q99>NkSW'kZ#o}"@/$ K+WY_2^1_[P=9d=jůˑ'̑D{?XB>wU S(qjD=CnС)Ydk#{hbP?K W - 8y?հrllP>iT|Ro`{^V Z6ފM3޵俅m<"Gw%%|PZu[8uc 3[B |OU|__F|d?o?C.5{HƷ>(akӪY#G5n! OlGQ%[>t"ٶ5 -\B*)_!~UN߆~I7^_?;afG<) ĸ_9ys7W6(_W,CSq)WM=5,K-Z[O4GAq,#zrO wɿGTJx=RKwI7-Sg|ӻ ~Lms?!7YqBgg ;֋ +p]\ۯ"l _gG֩.PPmuAGak񞇲V O4GRБt ylo Ȱ%$DWΆ]Fm^*vzg l?O&.`M;/iTN1{_6\3]>LgU8vC3pb@Uo[)2#xaհ㱽 8a{ %yr$K9]ww x3lX|BpB ĸ0|gW]#6?lpޟ[{ד8Aߓ[=R࿻ʩa_?a?~/_$paᨷ>P<ӝy=:(a.e!y^7`r./[:獗;+HCn,JJ&8|[t;d! D3  N}a-KH2)4g}guWGpvkf"/wW̱?_; {גu㭁)V?"Xo!<Û,8O tN}q ㍞KsaU~\filpS_y=}}H` r$s:&xƜvi41-Bݙ'?,ȸlTs%Zل ^H-p¥C8`7ZKSq)W-{-j>wY[=Kď3aʧ d+md! ,OswȰBwQ%ic v]Ľ)LO'\;(4"Y-oɿGV{זJ[Ӻ{Q=ɿL`sp ZxOq@!z<ȕ|N} f$Ÿǰb¥ <"%r$wMW>iT|Ro`{=#{)u:? >Y|D[ Cnل v?5u =gzPlOI7ɑȑz#,c哱+\ef}4wzgɶ%-p~Eg ?"΄)~ lv\@"{-r˿&+65U*]КFٶ࿻ \|rQIf6\ <xHoCS.%`w N|j&@k 'GGu992Ѽp#}ϠE![ݙ>{@硅3<8{#k N{Cџ3BXY\Χw°p/b}Y${-tK(`MzHHzט?MpW 234p'Y.ojzarr?Ԅi\MBolfS؞ Ÿ,}wdr6>* YCgYb򮤄Jk!} bh &]@L4fnQӑH|=şGessKB/) !,>k 0c 翈ާ$6!o!|0uC|3>i aYſ [;"'rݲɿseC x)_ؚF D4Mh &'zmET,[VN*g ޗ@v+BpB! E3Nz^)zWwY?][ޞ*wlo%~@NӘ'L='']`fB%IyWF_LT|lZ9 n…?SIw5!) &p0bແ} kJkO#o$kؾB򟼾; o`{^bFgC1Na}<r˿au@0s^oB !h+H'oZB&1ɼGIh &WWt15$!INzkzK9;Hl`9!{-ޞO4Gb-r˿x`d!bqO}m~%!MUK3S *.x!LNG{x俀iCHkSW'·&>MkZC1qh &ܙ7A ld <%J% 0=(ՙ>pA-H持G GW4/+}-i4by/mo]sN-g=f/3}BLQtBCO$(V=-%aMG|D?o|]ݵy}#k,3,D@L23؁D՜&zgE;CMQ+?@j0mȽoyxwwS CnYb4qX,?+)Қ{hwM6:H!b"1K] +G{E;GFHgeN<𙺅HS€3wHį7_G6{ג&)㭁)V#*%Új 61Qh &{mVZ9$~`ד"km`G2!7\9ς-A'~UN߆~I7^1Q4,D@L*rg.ܺy ɿ?3A_BxSB]'[%y)N= &r˿]%J(L%s _7Q.T{W4*iUv^QȘo/֪[`Ez!'ONkU3޵Rzx,-u/lMz$CDSC4@m+`$CIpΏ !{B8<8/2M8ّIxc]QI忁)zY ?ЧB1ﵸMwJN猥[϶_-2>O3#v?fV GKK7C_L]t 24B Q4M}&YK[8J>]0}u3ֺEƜ׻ *S$!{-TtAk#8!{`Xtt(4ȕ+sec'״Țk8t@a)V~LБm/qRF#s%J|=<_Tc7s\Dbw c71UoZj D]j}B 6wm mzr$zԊ_#O3}SGg *0ܴdk#{-8K,YޕAiM=thMW'Y.D.,ݻuT5~@N[^?Lv"_|֜6>'O|6{גxkGUwJiy_<uIWUgTwIیӰ !u L*!vwɿ !M9^?o?cNk8:Bt ${ 4&b2ia}q]^p53g-{#oOȿ!{%TO#e?cN b`޲F?A]k0G|*>fw"oOɿGV{ז+|aRxOFCn[=dybV@L v縒Hk sE! #%y{r&U z5}E}l [ӨwBTA1i?N_]#?a> yx;@򟧾'G2Kz|d?o|"A11h & ݙK)t%<=o}-ަ !P_#EVai]=C*.hMz$ǒ[= lO]5f$ %4%˶ltU,6nw3Lƣ xuCHkSWiT,Mr˵V'Y.4sg.Lpkx_d{}SAZHk#O3-]yS{ŋtd!b8ߝ|$"x?-x3_X?}R ܧsu k;#r$y&C*n_Q Ĥ- 9{?Bu.XKa?̮ *|iT,!{h,XrdyWR5б5IHb`Txif@p_TD1:s!MJ%?Nl%-_8~]?aȿ'b b_Q)oBsͅkK$/2ӣ&zHΑ;ɿbI?,B#rN?`M3ؠ3b8s'Cn[=4qX,M%KxkA/bΌ? JMlm;<_iQ_&~Zs#iTN@"~Bb ?#= t@|fݛ,Fr7іi7I*koCHSv俱x99%>NB $՝5JJ7U3wڶtt@\e.,r;s'vtNI7^1nɿM$j =A]; C `R'Y ~>s6M=5,K-Zp.iTd!{-$Bx@Lakx`d1p;\9MïVJЫ)B#rD=SZ[W!V@L5@򿁅;"uP|g<,57j_?o|6|+je }$BICntG4&ׅ;;`.4YOGmZ(~HNAZxɿŠ$2\ /46o'̮t%/>h7m<* !iTN)]КFH~X,y0-J ~+,.OVvN$/ϘApwk/" dV2 KG;^Fٔkfo4.3{J6_1)A]m#gk 3rdyWR5б5%_Q HZk28vE}9%±G$IˈC7~mO+>wʿGTJx2oh{ !V@V- u[Ce?&1Qdɿ !M9^?o?cN?I|=$B.(Dm&x]jc^ A0{y͉bnLlpM=5,1{h-iiTX&@7]j{s@Z~EC;e2~>žB!`wXLnp{*]q8oaMEDNc{_Q9s˿IE 4俌+¹ފaoBd8؁7=IwMm7{HL;ɿ.rdyWRjk7`f/yX>+87tcDMW+Ss'*G/Dkh D4M;ʥZ>0]K8Yn  !{hxU{JQ=F-.+CSq)WM5, Z^Z?$~JOcG'> =RV6!5)F+4* z$ۖ?B2/D. <T$Jz<?8̜&z"l?~"-/bHå.Sxu992!]?YN򯁁U@Jdޱ4"r˿>e8V[!l%l$xT ⁶'N~N|a eP$kbr$BBd!bgԘ'T8,g{ff->;ްa.l& 澼b~*8wU9 ݞ[=Cn,JJ&:$ ߃_X4"9Z=RG/`M!y0}V, b wW̱?_; {גMKwȾ"sתW'_)=?yX]SKtxM9$M Q2-^JqĝWnZ7p+x{&r˿]%-l˿|akC_3'vP\j7CB$!w/@BrʔӄgkSm)B#rBnٗ[=4(!covd9F&=\MW+SsV5;ɿEzA&;+d=RKY0і獗g_#x4G iW.UiTJ信)qWni)]КFH*:8y.ǩ8+x{:(BB!\HkO1OD8m)`Mr9?ݥzVG \SAqSV@h D4ZtPk 'GGu992!]?Y_.9iD=CnС)Ydk#{{6(׮,Dװ@*h ?{+YvS<9=DzQH4! g8|JH b8 AF #@L~ $6@q`aF I`$#jLOW}wz={^]k9ߪS %'T3lUxXdSܙm "ߡj`-?rxURƒRL5lNI X hT#egU/Vt8wfk)(.4I%!^rBmD9j qzs9}Nk` (.F_CdCZ5X˿ד hNM5:Sɿk׀{Oc$A5 Ȣo+T2ϴ 4=9r˜Frw~=Ozl3'wqL5X˿2mho3&q#/ sV@?Ƥїs4^Ƕq*&k멳_ ȿM5UܥG9ȿm|Zw?Yn$ ɱ Z5ƦfS!̩k Z#W%%<(EűlNI_k( DO;@Ep\ɿ㤉78wz j"fC׊tsB|B@ӍG俳jrȿ">"w?Yn$44ɱX˿kאcqNt0&Z5X˿k^0~KE7Aۍx@N* ,p\ɿ㤉78wz jbgsJNh Z5t@5N7ȿAwtva'o)*kEO548] IS#X+֓Q Sz P(+E$ r*}΂A@ 6 vJ <"xsE5X˿wŒ.Xzt=G@,lFR *,W@NuА.+@ 'G9jd` nğ4~[!Fx[˿ ɝks|=ɿkTxMGBa,_8|3~+kʿYc?o-6ȿ F!tR@^#W%%dw,f r 'HRɿYgӓȿk`-_Z;ߣs:45?[@h-Jm俳jr.5%4 @`-1,g Z5X˿]O5C"Ђ v6OT#gx78zZߡNSTxvm@FғHEeN5R#r$Fͣta&4 bkCUU'"Wz!#1o jG M?s9%xkpɿf8דkMt 4 "7סG(6]c[ba*?wgZ5 ZhVX@;,o7RxgBαsdȿ".B&V3H/&z6$Z5X˿kפ{V :;E_Q#*;ԝsȿt)$MV@'h=P; &RX_h@{ȿk`-X]@$JO !D<} v6OT#gi$nQ[:ZNM['S+!5_J'M;h |`-A;Hğ4~[5p N?/bMdbTD5X˿kTxMGAЊam ;]5 69/9wR 7AU5R#*h!zm)@uܘm 6EC k`-?rxURƒRL5lNI Z5 BXh$gXL~N!]O!C箨+jDƓ45ɿ&Q  Q9}i9!ȿaN?I|Q:;7c )/"_kxLh- \_395@;6̩s|s$HB[dY3oL]lzp49.?"''L5X˿kאdVZ5 ?*7uPgsjO*iƋΝ+/sGPL(G; )$#sx \NZGj /I6t_kȱ M6t9ȿm|KHk&Y @" ٦`._k 9*)A)&z6$kאQ .j@5T+w|4fosw!]O!C箨+jDƓ45ɿ&ݑh#Y ?*o!sz<'67'Qo7JsF  I Z5t;: Ν =T3\O*|Y+:;W_85ҡNO@ $ 6krU}9ȿmG ?z9?Kh"bTLq ,7.6u=8Nٜj$ Z5d ]5X˿0AU5Rgԭ[gsjO*iƋΝ+/sGPLzЀB 9ȿM򯪑2O_<>2G M;h$Z5X˿kאyB5C;SSƲ~TZ5ds9ȿ25^@ =^/G9\ q07o+t'S6;SpZ5X˿rsgrج_k`uwtor9+ тїs4^CN1^+\OꜲ߳kH<\@gVH]@OM7Aۍfm@R@oP#eQM]5X˿k`-?rxURƒRL5lNI_k(ohN 5@ZNkɿI kpv= 5RRO&'cCH'tGM %4T 6-s?sN#9=$9j FxȿZ&hk K._]#egU/(p )k ( hl#5v8=wȿA>5$Q8NZ5ƦgSͩFcYgw*׀H+[tj/׼R&ް?6h2*R95帑M]k<;r'g8N_3㦛kMY˿8 pG_A欀sxTpNM9zpr=usϮ"W I`Bȿ">"/}oi?Zx@r2g3XP˿k`-_GJJxPɿHȵ/!>ɿI  +_ܝKNw6R!D5X˿k`-tGRM5Į?@ۅ8Ӳx6Oߐwqz46V )[?@h$!cwO9!ȿaN忳jr.5aB3H3`-:np!a^Y"lI?9"6 AHbS׃q!9?QzVkʗY-pߖ(\ Wgx3_k@@ )&K#raa IpWyMOӹ0룓]4<4:c-_.ќrq&ȿmG $4Ԅu|9d&F?w'/pgsR fZ5\U0ߖz"fy=ş:ЎT#]b?@ihH3~?[< !+jD7FJ86n*C?hz*k$\ h G sz(^8j$gyjm8[{^+h@Z5t\Mh- Y\5;SSdZ5X˿9͟%E{)& ?zHx@Kh&,?;{np+r\DN6OT#gUe9UR~6Gw[bqX,9h%o9ȿm<{ӧD[5 '{K^q{I ok@ 4ڂ#$o7;2^$l/rl(+oZ5䐯sj595H*zr4I8p%aQj9.%I5&GHR95帑M]M`r9JE׸H8P-T$ueIpvNH5򯈏Am$Ek {~1rp5mH{Bc-Ip._C9cAMj_ ~\sR|Vd8'a9 ;UGց^qzGW6א.X<''i9!ȿaN_q'S?!7%E@oԑIxV^+[p)H N_CEd;_395H&lI~kI5^dn\ GRC ' dgk\,MH*'/ gp'rJTwŒ.Xr% ?o9ȿm| _OW|/G7^W%lLdmXUEw9 zgkۻ 8Y9v8[~6"*S(rjh;fX˿ hNM5ʿ'M|ҩf8N?~0~sH=oG 4Z``NM9nzSWh\7u礒wǶ5~{JVWEdD5{EG,0ٜVA9ȿ5G/H-̯k^֊w.?"9(k 9n72~K7}_;l? T5] +4a@5$6\;9*)A)&z2j(S"Ji\-yo7}I@Z@mΩgsj)*Q)0~\$E!E}+/?C!&r%uX4wx0DZg%L?*<RS] xV^+wY>"kq7k(Fq lNM5ɿk?A ~`ϥo@ Y3jrO6\Iq!9?QzᜆR?sn/FPPϥ:K,wŒ.XTPN'_$LC7yJڌjpzosoA;w4T 5v8=w?p$?-apd0??ngk X˿ hNM5:%U`-2\OK ~numE@+3] WVdcZFdu?'oKnx^BX`N_3914Z#)sV@<Ź'.i;p5 H91':Ȥ;"s_B8B 򯈏Am&K_r{EyjȌ9?Khpb]5X˿k`-?rxURƒRL5)p𼄽/Kx2@#WX6u@rzho&F?w'/pΩgsj)*Q)mo5w>#RmD $ZD**x@K%KbqkQX4wx0'c;{H|j99[rr2>~ RUFxPiᙖ&nX֖oV8 dX˿ _CGq $jSH_町>_$ wL7e<e9>~3*荵nA97t@gUA!iKJ%lB@j9<|Mo/}ǿ`j@/$\k^`W )$#sx$L?"ayw?9d&ًRG*<pY]+oMi@j˿&%c;!sg{UےGds[w/MFdQc]f@hDqRD;0 Z5X𪤄kF5z7%?+aqja2wL&x`f+}_cL9?@Кl$ƛɿI sjٜrʿ_{%|CF)CidtzSFIV7:Q|^ֻbqX,=9NIhH3~ G sޖ{VH 8:z]%$UXvvp+x+7뵢ڒ\ sBN߹`|vC8 !9AIԻ_N5 u}$ًrr2K6`!;;O`?.R;!)@W:v9T#k1:8{޳6~nJ krxZ;h-M \% QKFq,?@l$8w.?:SS|R7ԓOEꝧD ż''s_ӫ^x#RU|d7,lUx6O_<>2OƏK~F/d:}\xr㟈v} oeHx#΋L hNM5:%U`-i)3"aC"2:1p^lm=|ܕc1ȿm|埞@+hriq@9YzSדi?A ߔz "Ý;!ݻۏ&c@`rq lNM5x 1 6~eqN}?{(A*+I.L&ƾ+{LjH/}r!ww@[E|Do7_R9U ✞.d6{QNO&W@&2OL_I‰T7~?l߹4hEv`-_kUI J!G%!Y,^ßտ;Hӛ2nTZ^ҐSd6i&F?w'/ ,~;Fͩ)'6g$L?"+Rw`Cvv`lVceYvֻbqX,=9Nx4p.&5xyI8ԻF}Od>%6p%M bifW4m|>"VXZӃ_.ٜrrp{F/D }u_NMSLN5'XOh ɭ5T#_Ž}]OT ƄPׇ2ݒyxODž/#@=3o77]+& BiY =tCnN="a,ab,7A5e{a:N5Ƚ4:}D,wŒ.X 6dH.ќ@ S5z3֘/azۏYj<IXpgsjAB )._395H~;g,Z ?+f)_395@L]95M 'uN%a ߐz<$^㷲 F2>!vkɫR?'G Aۅ9SSK?c6uHOKɇE^uxӅf/"kpS'e0`dA5{Az#IaTɼ@ɿƠS#ԔrS6? ag=J,O% FNoJE#ʌ7Pc9]bl&N@Jh@ۅ9SS< !:lK%K9>~K[Rkǻ2%w-yOexI(:Γ2l:RD$4}.ٜrH tN9+ 4:z9 {_~X,u}Rt.ɜR~_te6{QNOKOEDD66d2yTR0`q>}S_BwңcN IkGUa.E%ZSydk=&\` Gڅ;J=6=V(/kj /sgo?r\ߑ% Oxy4VwUT=opN,ZqA-0ԓO?-S"F1a D޽%''wJOj 2 qjv w8dGRB@EwԱW_]{l)u {_~۲X"!ԥU5FSYQQwŒ.X 6 ksX!lJ9 {OK|XxP[rr2+=ܔpTVnP"rԔ@#ќj$3k'loH9y=X/BUY,n`!;;O`0.="&̾+pgsj)*  t\˜򿆟崜pO2ߒ>*=KF2ޔʊ  Oߐ@)gANG$|M'yQק2(~wxG&'Kax gߍNofq lNM5ɿ & 69WFJ8~e>YB8-=+8Q1׵Uo*o]RX-h$Z5X˿%휱\T~Jxdd6%''wKOe)[[åcl&oȨ\^(a<;o@v,Ȑ3Y,^\}EJOsufگwQj_H-~+v1e>%mީ-E%c2H1ͿN1Y A =]oi95ůGy!ZSi2ޔhRz*="m7罊:.B3JP#U|X˿f5a JfX񝷿T F2ޔpTzBQϤ:ߤFdwf 4: dB#r5zu~PN[rr3lΓ2KO8{,wŒ.X s1TPsWLG洐zs"#^XܖW$ޅp%M ؾ4Sױ>"fR#zWTXARB5CdQaC9MjCOKw8}jFNS|q߿o?\ l;~l!g9cz* =o f!bTd:}\%Ϳw.oR%!֛sR=#aY 7#p||WKO5[[åǷ/erW\5;SSdq?4@mrVD?oE*nCγln;B>4Zc-_C9cxHNϊ`2,a=Rdll엞k̾9hN5RB-Њo@v贌lK {K`d {JO%dSq@>]o?F%X54b@m{ a {OK=}Jd0j9<-IuD1d:)VǷ_]UD귿p ?"֧9% @R-{}FƓd&ًRG`02XoX/T}$rfd p8'I%?m1Yg_CDT ~Z%LCw>%9:zTT 2>):nKV6e컑3@':9.ݜ0~$I=rt,/K]h"M[3VM6hZ#W%%<(_Ip$[׸!1ؒz_V驨d2yl?߂Y#bKÝͩ)?茣Eov9IԻq ArxyC|#M`  ) Ge 9>ْH}Zpz'rzzXz*zX,= mZ"(&!XKhq@f5r?ȩJ=ޗ~BP(G,O߻J&GecDY).ͩF2׀ G/e%Ao9>~K[R׽ꛪtFb(>xmw?Q )4qW˿kpŀMwt}ۿ,"p1X%B8w_;ҙ`(MKOŀ5| ݦ-KCͩ)X{hDh$6\͟<,HutF >ߊץ:=F]GsJ:UO 4ZHK+C]lvKNNfpSvvnJUKO%!xχBw<$=$?4Zh_{`We-&2>!U5,=p#>sȿǻ0V&X˿ jBG%?/aF<V/J]J2]L38Ϳkj~>49)ڔz7%}Iׅ*QקX$GGoJR66d2yTJmj?YwaSƫ[PjɿsX=h$pRÆpW$C!||s||Gf[I$esA~4:'so!aLJ 5RE?@ Ű񤄃oI%b. Xf_wekP?oߐh$p1&$L>dVX&W2<.nDToߐ@4:c@f5"?#O꽧E*9Pd^z*9'dSz*T@&e<-=AK׹g?.CE<54vΰKR&_|~KT̩J&xSz&#׏R❭wȿaNa Rd zEdp>C^`$M ΂w8 L+4?v6'aȚY;OUdgI 6Kn~?]N/"rыWٝE5X˿k׀h _D$lZdM~ߢT1ntzS[nPww?Qᎇiyl.4"^۲Nh"RUÌUͿtd>c…q!?{htZ5XY GGobZ<P|Rj MCLls ߟwOWʑg;?rxUҚʿ9u"aC9Yk ^=~Yܹ{=j(M woG}766>C[RL5>YaoF$CB"sS'raa0ؐh4I8*/]"dC?HXW~7c`5lZߥ:}U:z )oC9>Sz* 6ew}2l$V(/!UûשZH8n hEC.QCtSuc" @O]ݻ!wIc4ږIUx/^(]?lk,kY_k jOߒO;)CblvP᫲X3dTU^s"OS4׻lFRpC<ޅ4RN̿+]꽧ET#qz'rz?ƁL&^ uHZ]{t d53b7dߑя?z2j;w~փ2<&:A~kͿL:}CdÆ0N?J]X94?+v,Dۑst,/ߥ1WZm-Gr󎿢%yBkא8 -Z R_ODvz1VNp,KktT@&e<ލ2zkEZw_hxxm =YǶ]ZA@CE@w+;\? $lJCAO XܖW$6`0䦌FmFY@G5Bc?yΝߕ~ 0**pz:E>Γ2l6G:f֪;\UI }ٚ%zQE}?.r|u#^kѣv:kl$oKOLO`;mꬬ C=C9Qe ?3@ }9-7t-~E~ERO?)I'!+^KOD'K"Ϳt?o\k gkxsX/h%k}_9)7țLM?z >%2?!{𐿋ǻ2<.Uu7O:o7$4 @ Y?A߃ W}(BAw$lv?-a|S1>,Hu驸cc@&GuM w{le Z5X?@kx8w^P^g$lHxTONOe>%''Sqò๿i/S!*/h Vd__P?$0S!We-l⮢Q8xϦ|ݻWת,Rkv)/ _Cr}NKROE"=?")w>-2h&8>+-SqIU d2y\]6O넻QT#e<HN!}?˦E2xE}T?$aԻ=YbSY,^7JO-U5 v k':$jq{Spɿf8@ ID$J5f ẀOJ'ǃ.i驸Ʋ F'WT]| 15495 IZ}O䕿'ag$~vd6{]`);;O`0N>6o1ڽۯUwDod#^B@ H񳜱\;guC^k+a ;9_.Wc8ܖ ~?]]w"\#I|?@]957l誣]ԓ<%gxe>QNOKO=L&7D[sֹR\L5>3htGR,>2gwdpgdK:_dUfխ[H*:# #"u^u\F2* /6 #Ȧ2μ +4M7 w޷oUeVexy2Ox"8Py:Ɖ8נaf`o"x KK|,uWjAuv#rV:Ä8WHCs|!!b͎Mtv`nY}nl2ɵJ-=y_'Ǯ"[K{4a?!!!" \Am}?L~`nVm/Z~t'Sw6lٲ332-M+I:+}5RT| B?  80G? (0}:0 Xt:Gy?i`nB4ۅg+H*T]~_ IJsW- 8@jfCk=ƉO'>4fPl`KQ̿w^}کRi`~LO?_/%Z°!㝻x~9 !pU_;6/J xPR,h,\,\i}}^ ;u{{֢>N(8誎1Mlz frW> NZ'_Sk߫Fx?!NR'ȑMdd>ʠ.+o̱0 7,S٫ h]!h_,-CQJh4Zغ24ն}s:A4t(k1J㔝KD B*AYrrMn"xD!OQt++ɟ+[07w) d#1k5}-bAŐ O6 $8#^#|މb}7l 1,-(Ɖf`~ IDATs+.ac_l?HE9X֠{O ' JQr4YjJch'?DpUo"X,.GI܎0LjZ'_Pka\M8Ir($#>5_{9g;:h8uy^%9Pt9̯2;{fgw |lşcQ?[![%JB!LJ̕ 犍h?!NLʿW)=aovm[_;%,-CqYg˖ݘ9{̿B+HZ5(=n9"CkB* B?mH0a:O&^sY,/w9܅hv_?IG>vIŜ)x\k4Ҏzl#&;P [W}nDQtL407w h^ԮGxus%*1WbLB'd\!^LFNjH?B8Dcj;/ĩ{1Ss-6UXu^B nӍW(VBB BPUjO3SH {.0:F [^o(C[Zى:7Ə2ɡm-͋{9]IۧVaY/9-ض57#kyb\S[ǩ2ݣ@ʿn< ę(= ߧfہ53'fs bxwu|oϟMjUb $B8AWAWjZ Zw ڞ\ZغH_WmV0hjh:m nL?!Y B*CWAWj-6̜+by!XmxZ?i ʿwAsL8@H%X˿TV#^NWbtŭ@s+`Mۍ]b^Z:k^16 [P7O6QLn?h! 'QsdksɿrLh'>7I]sU^e3?8="J8t+5S@'N ͬOe`v Ƙn33;5ZGV7cﱫ~ZsbN^[ǪA!Aqh@'A[%L@ 4Sضr4[Qd?cWqSCp}" #h˩?>vƅFmۮԖ@-OXu&l('(9xBH? $c uSϰOCr `Ylv%H[㲿vXsҀ5ns%*1W_B'BN ?z|+:-mh4=[峿?oIvD^%.Ub|Gو膐 U/o“v圮ԬOh.S,6"%?Z혛Hc/\2~q~y,J׎+Bf'HT̩^l1ffB!ߟ gnEI]JL;!sD !!!"(:%9Ϝ8e.]wU__I-/5+GtU{(s0! n ʿN|I3m9gs`Shv:ăc֚eNK8ܒ,BH\@/_|f}*t/+1rhqS?i`~]ϯ RgC 6D1߻`9, [ϑΉ&9sHH? dPlKևFc [^Vk[WȘpC; e-*1Wj֧ /!Dx B*S,?bPj EĶmW`zzˈ ̥[q?;ZZb["V9N[Ă'ĀJ&qʿn?LccŶmWј9eղj!ꔋ: :5( B$Oڮh˿{9?wWtZc `zz۶]F9<̩ MxW r|;pw&7uM|s}ͭغ23<r+oҨS]q 's%*1Wbs%s'&; _7> +;b+: L!_k*f7nǮNcdo-٭"];/O3\@HUd߱yQRt9+5x@WչC؟<ز|_3Z]?Wq>FCWP+Sr(!Uyi#^N%9JSmss`v<.[XwJud#loKW{8q1@7 )950vִ뤥јو'`ZfZ\UKvh? N x Sw1i'$/OH/2xPsj(v4;w;fq)Ez*z?U\QϑlUf쩛ֵ5$aW=jP lvbAP#.pؿZt:GFN;dxIFW7zgB-bX%@׫A8@H@(՚/ȱ]:w b7T]7=xlbF-?vi*/!~ffE̵WQ ' ZSJm ZZ?| ^'ëu`F-?V|lu Q~#S^% N(Kӕ{9]yZ+ [vi9}d T'#k_XڦC6_)OH 8@'h򯽜3+@oE4wl#6a&(ze8%=rڪǪ~ǀђt+5Sm'Wk$>_ -8qŠn&oIǮ)?ni_C#[ԏ-!d DZ3<(^v<˝ؿZ,-v\U\:Suڗdz$B BP%.Ac/vB.^Lbd6Cs2z9lHP#˓v ϰO9uW'}aeeAB:0W.Zˮc+1Vc+'+x BW{(CѦi3:@ X^>S3ׁXuoHΆ_rZmC'$9 2ؼ() N_>e.\y☠>6EO C^f63yU3Nanz$a^ 3jq?e%؅KEL;!gRUbh?!D' ߫95MgЧahu E.>t3->$NM:s\5arb?'  BRE%XJ圮XJד4;xt,.þ`s4Prq(\*#răC'$g8@H0NF{9j^V'Enw_'he\.W$]%JB 'U#d<_!/g@jw ˏ&0ck)~:\_^.LPur2=NxcK'$ ċO^/!qN%ez*)6\l]ر7[~:Y,齃Cq(ΠOjB B8D_M]L I)Vu &l؈Ǐݏo@OϷ3֏]b^UbX%JUbDʩBR BD'An?XN+cwȹGOc`i`z:/c6*sX1"埐pg\-{,}@z3 z8x=rȅv\uȣ_4(I)xBH8@FdҽԬOAtf&9'}סWS^>Z۫D=Ų-?I<_/!d!'Mxve:0u&RǶ|LX/9:.Pg2az6,O_ <@!1\NVGqīB\!і 7ɿɪ.Fw +*ۖ0,y5p~e?p3;u8>yǨA'$ $(NdpޔaԗsFpʿ[N+`GmUwx!\_N=. w {PW *1Wbs%rUb~M=!!&_-'?,ZuܱŽ= z8oa{wK:4>Wkvw;NUb ?'s%*1W(WQ=0vBaQ- tp{ߐEE?Ma={*c}3[ĔrbȿM'D N""83u9g}QcxAΠ%ᰶ[[z Pu6ԯ~UO3vmxu_)O 'q& 2KЖ /!OA]'u@vp_`Ȑeoا`{^߯]';Nh'Cȡ ĉq.n%&y!d( $OI_{P*B04T=1sNs~4瀰Uwb8X4ݞ={<~ߛXcvBxUbX%抶B|!Puڗdz$c(.jW"Ykm_î`t£bŜW%'5P؅Q7ތ!|>$sE&;oﳞ B-&@ O1j5BoqLI^3L1uz'GxbIPΩ 2|Q !p/ 9X[%D\6Sx+m0v%`'LqZ|L_d|WK9e _Ԫŏȡ{P  D _F$:.+שal{ܻ3g`(v-7O_{w||.WX"M&sE[%h˿\N"V#d<__1Ns)6<&f`{Ѱ풨1_mɷqbx;VH'us\;^Ɛ V2pg_B*_!/gi$ҚM?0݃]KrIԘ5}Z nOp}O'8C38ioJɰO8Ж Jw+r7u'-ݻk06{5^D!_ {/`،QX%JbW2>- 2\yL$'k_-4nj1ݷuƥ8'MX.OceL y'SjkrNR'e8+P`>u;paJ' TOہS_$+ 3~e%9'B $&_Bd@'A[%XAwķa =܄Sz1f<= mݸ_>FpﻺɿD1埐p 9%A]? PƐ05,,Zm}ʀql]?n!K$}VTߙ[gvBxUbX%JUbpPur2=Nxml[WSTPG#O~={C"ߋkH?'d}|-0 $ $;>F;al۱ݍ }Z>Jt>CO+kc7pt{ >\r(9B Nm-_Bj:'.Amoh3˟?D9P`IA;d퇁ъP !q!Đ.n%&yvٱ]r犚ƓאIi!DN"U/II| +(#X@sclg;:ҋj R+؃Cq߫# oAP BDH? 3O5-<2%a781Xl؊MzG7^҄roGq`|ܱ΄cQi 5ǨA'd!)%>e)Wt2y=yk[ =*ۖP)V~^rRzN#џEA@Q/A[%DڬV6A!p'\uDNxPr(ZH8GCNX[l[RC9Qc=o ؅[umM%~=%9WxƳ!DNd/f} 2-b,t_i^nm sۖЬ{N!9$Qb؃.hsXi 5r&^ŔBrTf ,k_E&6!hvp[ صx5|ѻ3&_ˆE_}u@x{jdvBSUbX%JUb!dz$n cWpҽrEƃ˿m={^Zr{BlD3|rI&Lw BrH{.̠O)#Z~VMꬳA.܊3߻`9BH>p ?hҖ /T4-n\Px)Gr 멎en+Gƒ{hdQڗP d8@+A?€˹[Iuϝڠ.דs+n5;0hxkݷf{9.\[اPT{8ԍS7|\і 1Vc+Br@׍ +x]щ=+_wcVܤf;;"g)v WC{犚ƓאIi!$8@HU(a|%<2*~f:4egޟVM8j,;ZXMd%㺰{V8,?7O{ $!$9 AiNɿ/A[%=;kZnt-]k\tꭘ%gX]r4VÞ/=*]p3L2U ?!SwzI{SJ}*I&Wt2z=_?c;vOr >Ec3W,a0olQ>$S#ݧa`a.M4۩_(į ~QjZ%d}" Bd8+PFi.?6MB1u4f`h.w0ma0Z0SК;߿K\˱tq_YljN""dd@'A[%POuc͸pSrJLB#'a.Yn (ڰ\1ߗS7:WP #!dr*B04T=/A[% LɿmqꝀ}'`%v·Mɿ >cLB 'q":5E+v]U.6&QJNؓoKN-b?' + 2&ނ*ɡԠo ˨Oe񋷣G^P/?U%'k+2yQ h8@H%2-m xI9XNC?Ţv&N~1ژqP"ԍι71jP !p Đ .9_N&@oVkO? ' wȉ"j,?bk`]aKЖ _Oj 'O'_$?? 5Dr(r6m~ GuOo/xs)+8B'"U)x /A[%X ZhF}VاV, 7ߗS7(2p1N)"GF8g40&(-*4{QZ 3K4QOw8@ʿN|jx>wŜFpW1 jh߭>_wo?)h˿ BN iX<D+{P_ͱ'>➗'aO\ 72P;U>WP !!h˿mOxI/C?5OPk YlK?!d!NDp9 <7jL["_KЖEIJ?CqW.ЮFN݀bS}*%k(F؃z'}^x >ONzx0zR=1?!BHNpJP=`hX_HxI~:>f=7薿Ϣ% j&AcԠB& dPIs9<_K*{>w*_ $dSxf}* !" к;^3 &O8졿o\n-1p LNg0pI&Wt2zkܕ}J.3SY`?彰xj_3s90}6И&{vv`\_KzR;W(F gp6V+Ż:'\nmLQ?BD%h˿m'g!^L{ $S1,>SǨ! TLg!bb,t%rNW2/i4_7PMWʡB BĐă$:ڠNa]%h˿m-߱yQRL%ԬOA_R8@3 5r$=kɿ% Mdzj|ʿ"k'n%~'9Wx%IF0-_BדڹBባB2@WʡWk)g_OB]mf}**^OJOrx! TC?x Q|jdQr&^ ?!$< 9_KЖ /I5M% QZ6_7ߗC'o8@HÙL'cC+v]U.ƽі Sv/i.דKЖBуdS+e0TIߧcXF}*;ocԨs4c8%sE&'o焁^ BrdXg6fv9] Pxʿ !_7PR2_)5;|O! 2I,3+M$&y nu /A[%h˿m-͋d/f}{PgjOP $w8@& [FPr4ǩjt?s7w6B]RpJIr-_ߛs|L |AʿSNNM6U ʿn<_1f$>F gp62d1[ 21pLę͋<$l9+1sc9ke.Jr(!)95oAd܅AHpLHMM%IF0-_BדڹBባ]{$ꇼ; ʿS<DK1?+eB3Ӱ,f֧ܭį$ .+8̶I6tBv1+.zfx(kcԠWk&ߗS7:WPCc*M;d_NIaQN`R$_d@'A[%h˿mg40&(-Y)a|~|O6'D 2i(ΜFARA04tJ``)$LoX)޵9Q?@/!GЖ Sv/i.דKЖ9}Z&PO f325(Zm+'+6>xDa'#>w}'Rs֫օE! 2ix+k/d0߹9Ї2,ؔa(J9Zs}?Id0< Y_Bj:St-_KЖEIJn}{Pko9;}ZtR8@&J͹L$"g<0JVw_u,x+yBKЖ M̥nMR8@&3;4Suj( RK׍/I*_.t걍!9NXM.fwxO1Ⱦ 2|~i93᷁Oɖsc9+1sV` _7SCTI?X "gj+̕m!BH81=7к8u}30ܧ>7 NIE 9Zվ$gR?+eB3Ӱ,f֧܄ IDATį$ N\뉨b~om/1!CH0;@H*q`AhTu|(DK5DCrJ}G׍+Mu枇|hrƘÁzEH 2ɼ+{* ~!A[%h˿m-ʿn<_!/OBҺR$dbw*K}8C2n S#ہ?Vwm?tN4T#Krm}r5CplƓȿG 5h˿4T }ʷX$@%\@&c' ?]5?r(yǨAn?X(кoI+Dcp#n0ڈN|69)ΡM>g DWʡמE㹟f. ڭ !ud~bE̩/>&{!}9YCE%9/і@5B5&Ua(Ns\>`j[z^`1Dcn 1s', cԠW ˨OeKЖ /A[%Phl0W}(S Y+`^gk䵫7<86NOGFSu)Krʿ7=AgB"9NXM&+C6{'V/l Bְ~'O"ʘ8trN1J32SY8!u"p '?}j(>5(C3Ӱxʿ0b׹-s3Wh:Nkn~+Q {#^9}ArG{w{pЅN7Pşcvfjkՠc.U KAhlcT $gSw1cJ3 cRO(9R7(:9r7c~MBr pu St-_埌;?91m;BH]!0t1?G inxUފG 5W:N^dKЖ 1Vq#c># N1x}E'Ru9>'YKSZII@ox1ԝ!npG1x%3E:~?!TI?X x171ԝ!pky7x'N.Ք{9]+{UƐ9ds5PCh"7cSw: B<03z2nV3,o ͨOr8>'Y#wS3ms <.^'NcƘՉD.e_T#d|_!Ǩώ9Ƙ7c!' 11cp9WkKI;ajP7fԧf8')W(gG>.37?!NBcLS>e%Xxg%Zb '_埌I_KJdB_>dykqc\Rks5%d/* 1%{>c9KL $2k_#(nm.YR[;w3CPq5xI u~MPur(n>,w 7۳ƘxNk_/&&L*4 Z\CE%h˿Z?1dFIXkkCϮ+!d !D47*oţ+'/OMA?X]Ӗ p!UDZʿn&8'u"SZI BO8@IDTO'u[P%9`5!d!$ {9+4[J*cHm`pM'uNU ?!()g0X^#@(yyFʩ6BT!$.xq $"_ﲒ!L {LFQrJd __1kR'!qPZN% Lɿ?#ȿm-BF B>;JΉ*jPI]I&w+m# B;LsORO!d ]%h˿m-8')BH48@D &J% nV6P2'T-Y@!.pRK׍"SZς!, m8@Q&܀.ܾ'$*eԧx?+2~Q !TE _e - )d@נBHpDiZ`F>Q5v?B BH&Pfԧh58'u#.+g!'!@QrJဟ;j!, pmg40&埐}7QO!$ $$ہ?Vwm?tNTP#SHS 1䟟 NBAQ)dWBHVpWASHJ(B( DF"S-_埌;/A[%M!pW_IR_[%OD%k)-ݦ !$8@dwX5(񄄆_-mB NB&*cHm`p\ IMBNB"?g0X^#@JIIDU{O!e(B3S2xƳ? !H!DЌTSLBHJ&HiBH08@J8g40&_%;(Bb BH4ʿzxFpq҃_j/!M!!$ a,O#9ʿO!u__\#hƌCp Wxy?߫3 S@: ИuLP +JnlH'' bQUy ˿m-߱yB$ʪ*_߬R8@ll!J%ګ$O#)D g›Bȸ B(d+VɒҰTO'ڮ)BQ$#"l?à- )lDֺd#5B'qQ\gOq&KB B(&go _'>FL[N6Ⱦ|U?j*v?´O-c$B B(dKe`k?KΨOe5J&S>S?ׯD''6F)QgRd2X>#,>yiJ< M$G6o/DEkR8@}~e򼵟(:90 mII^* ;oAL̨NkR8@)p(yYg~o6?`4_!h4c ;Esw_?lNeF)*"K30^KЖ /A[4}0}1xHHWi_?:l~BJ! 3+I'&2;od vdR{k;} wBxӿZ0{O6?!pR/HͶfzDTFM׍)ul I&G#>2xQk>\# BH1xL<fǷ퐺KW \c;'<%ʜ>fw y0xO6?!pRO33,G`p\%͇Sf dzRىcvޏ e;1S0pR&/]Y&2hw0![>=oc}DUξdO3 >7;+V.c0g_f4'wђ朲%#' e6fak}g#LDg^S" B BH%1'|F\WFh/u>*LÜ>-\'c87}UO` B g6wÜ=Ja,>wu"`\AV^LZ"ߕ(Ж f+y!dL!ąp\lv>v(՚PrvAin\[B|Ii0㳀q|d9+ʬ\frXMo^$bf7050h@?)aav̧~B* B+oK/A[%h˿c$s%EOd,\F|QI9_I_'!!ĕ&n{2G%IݟDuk5Ƙ> sg.-CԖ 0g}7W4ƘCCo8@qcի~ !R51yӂwkl6@Əϵjc-_1w5-0ݷ BȄ B#6M4.S1&&Paa.`+0VodI > !qvcw埌\kk|2Pw!'!cz~߫\}q O_|>mG'ƘEFx^} eo?vXA/!TBXk[X]pG+C3j>(' w=4}_u к=Nş2Xkg<bV`-pq מ.4ϵcX\ךB*Ƙeճ>`lc*sbɿ}g2 cL%Y澺b<00W}(ڟR !p!D Z9bw{Tf4T&g %Tasz/jhy/X~Z9ƻObaQ_)c!d B;g<«h\17"/4^C?Osu!W}sM[IBS !8@s x74h|gQUm>_SZhLƘO湟C9a?B4D<⍵<w(!wBŎ8ƻC0A_b6Dcdލ}z'@@L Y shbc!#L\@fmyO Ĩskиj`j/_KЖ%I+J/)D1Dww9|4) kxcVx';* '?+טj40,:CycjL&?3A[~ ,<__Ƴ? >;Tx1h !'!4Xt`x3쑏j57ƌc,cuB5H&k7  5Xa.x#̥4fC5BcM$L&@ 1 ?bi\~ 0͏Su忹+!D'0,asm1HygxuFZ<0qH_BBXkhqp}6P7 L.9o0"7-5FƘׅnk|TFE_KL _5>>`ЍB&NBT־ 6:tmD9~@4fa.M`kۯė|1& B6`eѢo#h(hW 00uˍ1 B&NB԰) O.OmH~_MW\0;ݱ Ƙ## M</= av~VsB!d!D km'JQ3  jǰzs' -NP)p(Ud7Q0WCs~*@L8@Qe>;Պ,{ݰ@wyeǿڮs AD# $cJLT4DC0bxј| &bKOk6ZnI7mIZ|lHjMrOСgq"' bˠs,7c[WG}Lz47N|{p`7N[,b bͭkԵ[1Z6A5wbեVK22L6$LcM/Wl{o=%onA{*͞s3}I%BbE}ƛ?s/t k| xkEC7W<ڲbم~v?.KZ ,iaOɑێkcV'OFđ9e@RyA&wѻ`vr0u0 t߱X=; 1MƈC S̋ULl'mM0|+9|4չx1m D߹کxCD^;j2s=cY35w;0@,t@)D @ !p=evXj{y&}KIDATZ;cFadSy:`6r]յ":ӈ 0?n׺vQ;+i V?:;]y&"ǷVR&3l6Gݧ={|XpsFm{`l39 Ʒc :J-= g}Y#{kj -dˁoϪerl+Ll+Ӳt@ZXz"ѵ=Cc,[ݏ;,3_AkϬe=0/?_Z#:+H'KKOgBzX޺,szY;k/rr4&v;arGi8˅cw2(Gj<l|_=zM.C7; 3r̦&ă^=p4 \Y&ˠsy޵At]96A$/E-0ڈ8{ҼSe|VDL#I`@Rf6`O!7Xk$--5\rS=ViN;f5%QIVROI{_4X7Tzwq';lK@!('* ݳ|~S#&Xk_ʂ~,.v\ǣX;ApG15d:VIZ[ Ig}{63$U^pDAIhǏc&2'3HD5'+UYܯ}5Տ2&oVe`6N+%qi_3ʦ~IT/iϴc2!Zۦ3sPR\E0}S`*W씴s0r ()kIM8zU!I;fx3c zs 9]@F5nnZ49k\I3ӟ_4W.8y@F%mt*[$m<6 bbUٿJӪ\_BS'5:1U}lRSF'51i5<>SФ=IMMJu{Ru~W[%IH:*cZ[[4m-lk$uyW{Z+-܎6T?lSW{:cPjSxAIU?6cve:+(6lIU?UyY n'4<6 L/}P\F8?h~W:ZުymloUW{tiNGNZg[wE]Om&jׂ6P=l!IJ1f[SDd=Gҳu2+$-tRa`dBcW'7<ǟZLhx|Bg뫳 j-C ?ު]mZծ]Vğ'?_ծy'l0 TOݒ~ 6km*w޿QҳTyL'UBSVCc:>0b?m_y|r*b3 ?p[3--Om,ۡ%:d^mגyZ2CTߩ.6 6}~*'6JjiHֶurѿ4IXz=8cʟ'Cggg5<.#d_㐀?f~UvhN-ۡs+vhFAN-nWV?6cg;%JZLM,%JIYMLP߈Hߨ1 kX\1}op$yhAV-FKԊ]ZpV7 /<΍}=..I?P>;$ T]?W 3Ӂa, ##:?ٞqڣksd?Qċpb`9Zx,5j9ZhZhb:$;*vcLw83( k홒^ʂVe;c:;#:;=#:;y X^ſSuW6՚sfZ47ߖmcAP4-km*7{WH:'僵ґ, ٟwv=_1Gu,C|eNG,9qܓK*IS$}4+;b]-U,/i^3Ĥվ!96džvԁiB,)-yqH[sn|]:Wg-3Yi܎3mZ̀2Dܳ֞?WRgJؤv,Sc.?H^9_ ֆ]2WW̯`IeU 4Y"VIcz2PrZ^eO4u[{Hҧ%}5(Z.5~C%d;G'ȾʳF'N[㙄9}?GPP#]Vhê%:"]nV/nڗXn1g=`gJ/~]ꌧ脶}ڲW h|r'㑄,c_;EPYt 7trU?cz2 ^m~AoJIMlĔզ=z`wv0O&c1Geq?`fOAmh0ZW_+^%jC>IYXkHUIotAil|rJ[in=GOԔl'#R)nAXGCm3Ԇ5KVsk~W{$)%1xsЄ I33N]Fmݭ'Mh̠ ~ãX5g\Ԇ:3(hmlksW ˵a¼7>!鯍1  /H*ON]=czpWzliFy /栢ՆjՆs: sV$}C҇1f=Jz7Jjx:3=;ОNL6C_gei ?[&rAk1F^\7J$o98%VI1ܗdW|^#%J9{`6>qL>٭GjrDF57~X7!B"Ydnx{E>h'6sI/z.mߧm;{<. KY5Ikh!\ (ڰhn;ond;oZ[rZH1z"#WU @v_TN3o?m?I5|G,g"Ev!iՆG^,Ƙ,' ]l%b}pwDwn=m=}nWE|w]K,$\zh?Ij]zUgO;Kg,%K#c̭YN@zJZ^+/=[_{pxB|wi-݂PgMPi`$]a^uzEb2kvc#YM@: Zۮk? )7+7w>0O ~,]sFEm3jaɼN䪳6h.9Vb((kUMi~@>lگMAiFTXG[UPEmp\mhkmѳ.\ްA4آwe@ {TykT=zhwOxh̀1krRfP^4j7,}>g_OI[P,lbLgS;۫]dž܂']: >P_.W_Alu〤4|-ͤPI"$stbJyr> VI KY5Ik5,jC"jé+9[J"K-c4 Ykϗ$]FI}uls/O&׬3w?p!RdWɞVmhpdAkyzӳ+=[m #mƘ秊@xlMZFI \cSu}{502&( ~Bh1",C6^ڰpNn~yzٞFT7` Є$$kr-{< ~Bh\n ?Q6HGm'ϵanc4 T/EUIg>]zȠh}RN?^%^ jC,Imfև3nHϻtL]wc>x&4kTay?4OݵS=9\pk?\QHQsTaDo|R9oKmZo'h>uNxQM.AYo0Ȧl݂PgMP6Hk_|]E1[N@YkJ&cZ}롃Ovk`t?H E$/לQrQ̀XEf4/ٷƘ/$6֮5I'<=Õ'KRfDEU6O*xrZIDٞhäHϨwHDcy ufk-gjC)Ԇy~y=$$ 'MꟾCp5>F5 'njCé i׆ X?zӴae"o8&闍1K"8rZaI w!7վyk|?!43}.ON]3o4 wGwxqkSx) q"?$AʠiïXL{5Z/fm5Ƽ;cȐKݐ1G'w}\?}yk|?!4h6x(lmOBm9*'aN>]ל/vmb@k%=Gv=oϼAݎO$WԆ) "?)k[Ն)+!cGP@%B|hO>j5>F|w y?iՆh uf@mp@m;ėy*\y]~fz1#@ʬo[Su|44Q#??x8 Sj*Zm686~zm8c<ٛo Ƙ? @ml)־@%u79eOB|~yO |1krRfP^0jCj6u V埙Ƙ @ml)^#NIAG'xLv4=A' ~Ԇ܎6wx'ZZ~%77)fTȠfbH,IHZ"^>-z@ ~hE?!ij qG 7?<ŗʄ;Wc* f#釒 w|qy+ULYg@?pjCՆd'6Q W7_VYOҳ1Cp*6Yk/HzMx{G[֑ƹk|?!EkI4OԆ) Sj[8]kUg/ןlsU-$]o* ZPpRCz6H?h"??YM3 !ƈ-%GzzbI_^A 0b}H3 5m{6eik?(RPDdIjZ6z-ԒAH(fcTHv[`|pL#%_G!%j3U?tSb/%OݭW*FZI  @WYk%DվF&/l֞C4͞ _ G EmAԆCV܂6lמe Ek1_  WIXOC_~? dE25_yT\Ir0W% { ?P0>a=3D0\eJFI>q&'+ ~͈4Q#?*?g{6ڿAe[  qU]H҂9^- wKz1f"D0̸zs/Ir-R7b t4 /@m`6gBmp|mx`35:>"˳%U@@ٱ'. ͇k$' ~;7<ƖI666:FMM;W@%ow9Q <감hg6ԙ!K( IDATp[3ouCUq![Bd3>i]"PVlRO_۪owKg@(6 ~Cm2!"E!i$Ԇ# 2d[Ǐ _(#6 oOvlghRΑ4cm3 !ƈjC / SSV^=6bn|Lwo;om>T'9 G~4pXybpΙy g¯'+B#KR2Մx\Fy3rI #k$'Qhʓx|ė|4|$4i(_FSfOpj9Xڰpkx)k\yw<ǝ7໣;"< gJmH66D3;61vQ@@YI}|c=1pw@+$gg?Y>O4Im> 4mmc_H"X)齾A2aZL gzF~~y+h?{^U4sMj57I 1FP"툱I߫q| @Y?$9_nf%}5Rl'H5Bhq^P7k-OmHexsՆG>uFA`ZF}bں/J641C{̀ ~1L6x͠L! !𔷼#e:6ЦG#Z" @^Is]N_Y|4"E4ODm$!a!6; S#ݧ7E௭mAcZAүOvgh|>{̀ ~>(ڐjCbү 6D2LJ;:٭ZOIEyhc5?VسIKmȒԆr6l{\z$Gks@>^?Yk5>󖓓xhimGHB|aN 8A j>݇58:"X'f@4uthe8B4 ~{laFO9W|EP߫%:S+I WGg8SazY$M[22=WԎ2>9OQ @}th=yI<(Eءq?BBwNmpOAm8jC)oyGʘVmV1lohf/߿x#໳41!Q \감_WؼW{d=Mڕ>az_t؁~m5|4"E4O4JxO`:xjCm~3?I wT D\ֆYG(jC '֢\F,-_,nVv~Kw=vXS5X|? fexZ_tK~oc̿Д $2=M{MܟO} LcsW7ܰ)od_>}ݽȴG#@41`!QԆy6.]k'z]~Nޡqmf%:lZDoZdNxyv\U=۴@4A5j2ֆ3`(b0jC UnߴgE.wrׁwЊugvK/ҟB//_߼|͙2Hy5>V7TjC2Z'!} 4[mظk[ND:(6IZ#Eı@qb# js~Ztɪҽ~> N" ~}PZlҍoGٯ<ڳǴ]7%.{Ƙ1S:yU7O.cƘwcs9c$}'>#i8dZ[^~^tyi=#=.6/@momx`aW (5kQ%MNi (Itpu3j!Iot1ƘI&3i1UywrOZ"=uwN"ſ-??oxk u5>fbIK]n;Ящ8GvE+Q]wJz1JccFJ1qIHY9ZYn hicOyHRfQ`ԆP68:@bewqh'q/ I; ֢Jr.i5>V7TjC2Z'!} 4_mkAu:?u @=ucb[Nn6?|%jk !Ic2p1ƘA$Ÿ̤mz/\;E8 ~i\''C1#o0jKj{ \;ސYZahRl윮z'HtLs;Z=f5ca1&%myKzV.^}Z[h2໧v¿4#Y'+ ar6Omغp@ҲΓt$n$K/Ic4mƘoIzyz˳ϩ ~/໧HqyXksd km=~= ~&R9I/4myQ}Izx~%i܎SQ{H OOkh7ԆyZi (]>~x`/rh RHeU PhԻ^zhk|=?`X# Ʌ(Xm}?6P:l.s4evEX^}Z|WC^d/D3)`sY$|H6̞jAԆ̦kÞΧÅ!(3+hl˰icY6Soa}Iz1fS`͢ :Uy7o:_]m@S]מyX /@mTڵaqBhlk%rvz?xڽoHK1zO c--1]zu>14hfPXc$PNjC{ym2 (K]θxlXϽ)Io3{BM9Ik9#DmHP <감JSd6sP( \:6hNs6gƘ cT->1:ZKώn EljVҔ?/MPP7ŁノI7 @Y9_uK8v%pB0K@ɯIҚQw[ڐI*j$74Y㞁"02>cîW wl6&t^ĝu!J%cދ3(fIPkqv#i#ňGD|! x&~x#Y'+ ar6H:|]P*l^uwҬ;d"]vb04 11<,+#ntV.s {N3H(;>Gi6s[m (%u <;R竜Yd9gOAIo42ƌK/34Gis6$ _Ϟ:E[=A )pyḻ.,@9ˁ^_I\J/_cvMctVx=hi`i8Ti0l0WԆB6 *(#@߈1.ZP@)?4:7|ל(Pmh8&Y{Z{6挒+lRM6Ly_]( 6PFk]q;=o<Y :}B}.t陋|Ahs9_sԆȹ|Nfm]E2:eс__ּ ~']gx*7C|42]<6 E5,e8=E~jC@3aetˠqMLN?p9z]YIcLo1qI_vK:> ~[GHm32-x?a!Pg6ԙA_۱ .S (#;NQ]t"-jwn%}{6)z6ؚ@4llߌek+xњjou.3?292lR :uƒ3 b4^y$^gҭ^|@Ru[Ύ=lZ!M-?a! kԱqz?Wp3r.p`0ϓ=g!"V_#I̼A?)PgY2hptxH}X>ׁ"<?D5i.?Sy ar6D~@hll ?,'YzmA1#쎺嵡M+iSg'>{6)z6ؚH( k45 W[]%I}ѐ?6$͓j?2'\@/Ijos./Q1fJxkwpP3Ȣg+OQNè  4<6 4n/q\wBm4A* ~Z-ݰO0@@oDy 'SLN9תR IDATu "qNy(om#6Ob`9W٤ua j@x\` \!߁d`ybAY `2@]lL\xsN5DoLss\eO9 g0fuq6xLOksh!M,Y9_ 9hCȶ6/$ȜXg*!L΂ֆȏΓi)ZmpIp,$\pԆ, P7yǩ ?4H⽔b0W, Bm=/@}lL@{نxT5jr~>xs7f6E s$iſw^96̈Hm(VׁSOp8DZ`&]?Z{fI]ұш"e*jk}mM't.é ) ae~$Ֆ[$p:e&ׁE}?8/OP4Agox:j 6 oV/fw2_ 5vH%vGUPB?Ն4eTe:?'Cͦ^$i@+iS+hcmH,O*_0%x4_mHHdXbeq@I$~u )/}ImmH_;I9TkC  '͌ IW$r?eO7[kQYk%oxQL:G+ֆسI37!s1@Jrsh}@µ!< gJmȢrkmlrc$lo֗w>rh)WmHm~FyGP4uf!QY,5r!R 6D 64$~TwAZX_ĸ]:-pQ#ӫafSzY$M5g3׆ Q# !g] 2I>,i }=>GV/nP1QwNmpOAm@ l $^ہah[K+k3|%'WߣщiA#z`T4 gm7Z/LXk/|b M{vM !OB'4^3qm?$B?Ն4eT2 Ҟc$yd@8;uZ|"vH7I}|g> WF[3o4 Y˓J.L G+?`SfOp,$\pԆ  6G4IJMOXk0D<ֶXk^hhSȓXδnXG4!k4ImH' p[t/KZk?Xw"ޣ{/wnaPdry$nXkBPfKm(bx3 L课ES%kPZ{$JxW7kb=q.F9gkn4;/לԆԍHm(3I9¬G=GɓU6[k2hVoBonyk|?Ap!,݇6 (I9BãnٸK퉟hvk%n}oc]l Tnޯ=u'#Hd, OEmAj"$!QSVf=>?:$}Xңڟ 8I[_UA<}kk9J \D19XڐjCb ҞSnǟ@GG'o[kZ{n!Ykt*o>;Oo{HSP3o4 Y˓J.L  arRx&~x#YI|f7xQWHh$5b־ZmI_$뽷ܯc ~xZ ~D"_!lR"?8O$'B#O=oixVI7Kڿ^e zZmt@-^Tq} j\4{EdI*?[TR??Z[BaOn~z\y3$!k%)Ƙm>W\*s%-mDF[/?o ~\g> ڐP3H6UXI`'ک~xo^s#gJJ$qI%t19m\gJZ/BIKħ=~~{ g4 snXG4pMR,/lp!c􇟽_~UZ3̴[$GSxPw{ ~Ԇ?`X#X'"0{^jCbx@npgAL8o`|'Bsֿ&I,]sFf6kfD6D(@lyIq}苛OwlKMi`d\m46^{C1ߖ6}xjC_ܠ I>GcH'qwC#;__6FMt==8VR7akϞ: y}@µ!< g!H`@R<'"5ڪ1]~^p=jk&mu#iW=uHL PkCx?jCԆH@*$'a)쉣G|Awj=:odx)mף=n=?|ʢ6kBPfKm!Q,԰$d]]lo%g,Ug//չ$z)mӣvӃ;k~MN'?jGeUw%Wل1P{!tI=uLl~.XP\WתEsbAJՑQp}3A#})" ~^Gc>/nPKm6DqEl`T?~D?~;Zhn95o(8:>1ugh\S&J sXc$PNj h`H 'qwMLH߈$uxm WVҔQmH,Bڵ!7y h*4">WE_㐴 #jCԆ)*3kCby"<3Bi 62<'_ňGDD'C1#o0jKj{lkC kC#Yb'񤃜8 ~I3I6e e )R  ,)$ ~=:7|,Im,![TR?hQM!pH $~"7sNY'fPFhM<&$:I[:=W@/e7 7J e@}f AFC$ୋ7jh&{67e2"x@@6q_-$Z$4;~`1#AoPӱ M״ xCRC@6*O 7"|g-%MkeAajs'ߝtx"ex#m.̠Bq7qaOc%&$^+"_d5TѼ!&!Nx< o}ufwYAV 2Uw]"bSmk)7+4`ձAoCeKbܗMt4 Ư* V: g}uft K7XXp\lNz5ޞxC:"xB 6q_$V:$]V@@ސVATo,?oW'$'tz߁!3AO:=WbPѼ xPHuruzY'oo! Ru B 6q_-$BDH;D7dpo0YMѼaf/@ưjY 7J54(A]fzW콡Uo@|0"M_]m:  AQZ .w (ܡXPNhBD| ~@H1 7h)87xophP^h&wF ~,h ~L2JE )򆮡D|ufPXC l3HuGld[,oPձ7hHd ~RRj?4LaұEo@.A>H!SR͂z:xՌD 0MJjFV$$wo{?Uo^Mg2j |+.+ w ]oLޠw*7t7pǀ}B?N;7?)@ xC,*7A %ʛAos$:x(FC{fP@ *l"$AC!7k&¿4!;( |c-^LFo7k5%^(7@'hxBo1#_M* H\ xAQʹCA|&-%M|;b oRpoZHѼzA6q$Ɗ$]V@a oh3$Vsh!XA$#o( 4`AoCeQ4I}uf :X&7X,oP(+4,a&N!m'%Աh7h_U ]F  -fd_\%w w7{Cثzj"=VWuoА "؏fP$ WrC0 ~-|xC.`~TVސC6qu$tVSf/!7k " G : [uܦ o}}*Oe:.7Dޠ7l6q-%M|)Wn vx G Ё#@7y!Sy&6MPE+cQ,B_o Uo0`t 9&*0,#,7?#?<):-_U VwID= 'aZ[&B!L_8p?@e('V7);KI  hK >ſD+п`?bъ7 G >ſſAQYZV3 xXjQzKPDUC4 ½:GE-/\e:?)4@DJSU?v)$]VP?a6(9J*Z` 7(7%T ~X?KTE%T gY?O2?,7^Pҗ"3[Y/@EyJD&QCoc5Y3A:PT~_D~?uhj86N/v[$SGyp̴(#`@tz JPߍЅ ~NKgQ+jY͠(4 9LBXw֥7`?*?7t" ½:/Z?8w}:?64 9L*Wd_Sn:e?@8h31ZAWiH &]CIFSP 9MKW*t ^ `. !zhZDd,u(2* > ~Ҁa1 44LW"fBEYkȢ7~UYJ'a  & ſjN-Z)<^Z8ſHSć@a_?ND)cu(E(;h&e*[:=W/E)S$ RR?⿳.ſAQY\gv$Y~ ſ)do_}*OKPXC0NcB﵂2k vYCzcǺW?&"/nLQ~I2/b_1x22|Q(P _C"?O2?,7^P Lr ~,ſ:V:V35_o(4=9MWnH?ſN( ON|_(OYZ8ſHF1}Y/ oki~qB4f?+Z'%G fms&(\AmސE ϾݦmKJY%\ uux[x@6qW !Hhy[tV1iyI=#FE6J*7onJP8U(|;Co7=o貂2yC0i{CL(| ;$^+ wv:ސDd " M]k$N4$b~ocee\ grBAeH]wl!ObܗM NI$^:"$3Hu-77XXp { UoE` kH5c%$Hm"؏,IVNz5eVWH2*7VkLuN$!6 x`9 o0֡1lJ\A|g`|7sଓj77Ha7M鹂$&*xCxC-zCW!J #]5w%7`?_]fz6[Kg5E󆽚LFo7kQC lZ&"FE6J*7op^oo  ~;Co7ݢe7xL95L3@PM\ H!7JE ){C ?@,h(&n?&$N轂(mD,uU7N/7@[h(&n?VAEop"H!SRЅ'$:$]V@ ~ao :Wu+Κa/ t ~aIUuHtf[X&3oh}U_Co:pN H ^lo;:KD\s!rB *l'tz߁X $:xC(ˀF 4`7M|g8){!`Yo0`? oPױ^loNM<`0Z"MuI]5MtV17_`7O/7@7hX&nEo|s|o\UVy!x"z!lZ$]V@ t&ൂ2yrsofί7+;Gp0x"eYxCxH!Vs0PThM]3V|w c7$ {Q4ۈX,oPձ7 kLc% $H]wl!O2%EoL04T!w$!Ui|E:ޠYPo0~UYoұA`j}hDN7qM|U|+$V:)&xC:"xC  t\x ',??3R{ o1oqm (\ ~ +7w\o袓op 7N4aw!o\ N ~xC-]oPױ7@7hx&>[C$ . j"gz^͎_Xr %3l$Ao~ xþI1Uo2opM]KSbF EDIoZAA97M_" kr`$Z  ^+l7]C +r*0D%B_7\'_L'JAo0I!` & KT|:ޠ!5,fd V xA6qwM|/O]FgJޠYPo0~UYoұ7xQ4oAi7q|U|+Κa ֫LZm_b7O#)& W$aJ xCgixksop@!aW?p||\xC + h`7@{hM\ :4  "+oh\AN{ȭ7ث CPd 4 k鬦hްWɈ 3cx| mU4(|uo\UVy!xt *$FiA2op "Rn v -7q|w |nHUulJ F3Hkf!VP oЀ_&"~pɤXL'JޠзV7#4|aW 2IUul L7D)e+Vxt@`5$H݇wI))zfAUeJjf !=x |$$v:֫LZm_b7`OpM\ |#IjtN >!714\`Ws4|$"$6:xC(ˀ n :񆖾@Q` o$:${CxC-] oPԲ7 :RB6q5H"؏"Wױ7YM| o\5 "RXC6d;@o;P>V%opop7KPB6qU[I@2op "xot =l `ﶂ2%WHJ F5L3@+w(7+4L.RڭE/ksY !@` | Y$:@:=WbP:sK'幙 gfGgc:X'7(;4 as$:7Hur Q|y_:7T+ܔ|x|Y?b_G/GN??… u)` AQYZV3HcP[o0`?*7> ]:)2/c26:(wߑW/sTa_D`0M]"&NEKgߪ$ < W/ MI~O*ݔG5PxA6qwx< ~(] xAQ:D*"ON@6_,m?gW7/]0@LHR_?ſt|_ (ndO=@SG{||Aww9̎ E d(&Nomr9L3@? ~W7jՊ|YrĤTo񷡿V~S˿=3xfLo (& ~0( 77ɟ72 RWeA& OƊq踼wpFx?@ hĀM<?i.vS'_Go1e6P+מKSw^.GxDP@hM<uHuUjo,j%&Hcrqy HFҩiן1o3DhM< Zm_ ݿSypowOq מ' 4B&|w.S|S"NDX O{1תzKKNH_Uwm7O(7^hM<$:7(ur -}+ȉ7x{8rhT>zwj Y,_ !NѼ!PY +hhSM+,E-B"DDjUy yLrs'izkF]/"?@Фx{țx|7,o]W<{C ~ԑ1Wdx0pbzU@Jx@ZI@2]o05|t鄼wO_L{Co}hh@%W)J FU+y\9L:}Tu+cb5 4SM (_sf$z oAyqq;^O80< l$U:KHn?NSPbD`́Ҝ\9LΟjRVDv4$"zlhGI%AFN7xxns3cמkĸ/7K,i@6@)JiS_&֫"ty`.zR;ρ~8!77p4\`VWuj7U"H^?74)&Nomr9L3@+|Ed ժ3G#Kf%}ݡ7Y"IpEckIWT=ſNh ~'ȿ+@dll84$nMkQ6q5Q+h ^:"}5GK'܉\OϾq_op Ư 4`ZAb Eaiox:ϼT\89%zR|aF2n>biAdUp1l+NO-|5,="c#c27ш \(> IDAT wM0M{')E\SO tz 7y@Rrion9oP\P>h(&E'? E+ݠ7ɍ7uz o86y@>tB~ʜQ|~l47 : R˜່xrYZV3(Å 5XgWOʩ#cA .ן<7Y]oſſ.ſ& "ByœSr1ypւ?xC7*{C 4< 7_DaU(ݧK+./WT{PĂ#l]VP_?A$ZA oU+3Gcr)U"Cnlɭhp _x1Q Qo3$>|#FKsr1[]} oАx>@ThXM<"/.U@o?og?P+gWOp_I6 zM Hg *ˤWqW w_\>?/<'ÃWfAG`*hB $lauF*P_/Gȡ7/+e~β \Tz5e(w 0M o:q{|ݿS>pMW6z oȑ74 `ﲂ,6q+YYOFhi{| rn^&8:p7xJL x"w\'7 ~K_ ߶5r\9L̍KR :?o| B .(fP : _;Ɂ~`[zk鬦hްW Z"߫dDr[gp?ſV yÁ;fK>} ߸d lh `{x${CZOMpxG/y@@6q]O_N+7Lǯ+gen>/$ _uuH'weṙN_p\y_q:` Wuu@(hQM;'zBN͌B87u2CVO~s{I{=Q&oh\AN WQYZV3(Å0?>Ր/wɁr'vv⚬nl{o r""zY @lM#؏ʪs-Մ}ώ {ep#j YXYW9sjlX 'ݞ/7,(r[B lGQM/@JKDjՊ|r\|nRk>=nl5CRTĈ4'\t@a 1Vx`w5J K8.+(Wzn)w!OV+2{!eOXؒ[9̯778G i&mTZ4]$ @O.2'ˋRQ YZٔdV v\ %z !2l _ǺCrܼYW ٰ+wWemsG-f&sGY߼yDbL%` EĽ",7ſ^ss2?y+dO%.mHK㔿oԪЎ~(Ko?@&P"M51e$XoHtөV+کi|v^|aF(֎Y\-C22/FRYN\ `ΎI m7@/h(&nſ gͰZgόȇ+gPm/ fK=\CˑCхKdAwGM&o^w4z@l:qw`e&˕rzv\Trby}K.nS=ƐL(ŵ'3osQX&\x{騁sYe/9(zB~ p_mʝUYYjlX| o.t$7K{EEe5?\1T.@ h]JJEdЈP]T|{PR,;F.`˒7@[hhAo~TVSkfJEI +8ۻrllj"|͖|'7/t9eieS/nad_NJʝ$\E,c%?ʚ AT7 :/YFPYo0w\|a[K^\NϔˑCS$llwy%ߕ7~UY'XedKDxADBojWƋt& Wf .zqG1!L76`?_]fzſjjUyq+1[;ªlhg`}SwN S Sā'O^;)otJn%wfJEdf|D D+o,H_;G+ZM{`'hP⿳nx_>yY y)weٌWTdvbTFh8io\AN]ư4,fP{x@S";M*4U9:ِ:4Y\ِK=ǥXw%o0`? FDUZώ{e@YX^ZU'RͅctZ_yt@j;@>=s[6vŠlD: nHV[zcE,[@&ſt%S qGel;͖.K+?FshO&|D@ﲂ2 TlRp`Oy3rgqMw^i efb? VKux _L2E ̡w:6!.^8"ClGfS.V&Gez|X(ՃeY7_Jݟ71x222.LpA+(]+{dv(r4c džd1v09ЈƯ*+P@f/uS+`|EVN9 ޕۋ~"2=>"G2/#noX;UM~`x LNgU.+e|;VKnzfJ"G'FdtJZGT⿳p,:[gX?PʻqX#wWe+CPVenrTF1ion/}u.@:=Wa v9w'?Cb/gG/rlݥuy:jUlH:wd{)"9*[:=Wl ?@D#_N0M?#Z?>o^v7'hkwvҚogɆ*LgӟZPrE-/| 6q(m*gGË99@?YXY7e|PNJ? ~W]>}eJ8$^F1m\`秙1(o!n 6qw Qu`.\8Ɓ~]\lذK0\||rL%* ==&o!) ӪV+S9yԌq۔;!OT*rtbDFY/Ov˰P?O0o22D@)ZzlX>t\}eV& z,.oGlV+2{!"qm6[rEÙZ_yNh&&}Œ|x8Aw⪬mu^&Crx& '|qlnԾ2W%U!pM'WO^:*iD ivƆe12rE;qV)uu:XwsA?- 6vlnw*Cr`RrC|̊d74w0XɋqjZ~:4-pMV6^JWՊN4dxTƄطw}ibܟ71x22|< `&nA"!y¼Y9zh/>[rgqMvwY/+}ժN6dΧ]"_DdcsG%.#.#s7 q`)U92eg)wWee};WQ> )߉_/Jn?-ho?tl|!?&o|TF8Ғ=%~{TC<ӎԮڍQd 74Law ? ¼j?dL>o/C}2;ِ}[m^ъVw^0M܁X{SReȖV%FˑCB)ߍO|##-}+7JN Q {ʹ9uPguc[,N,C2962!ED,˽{^,ſAQYt`N9sf=q9wlBU.WB4rwaM׷^Sc2zI;ž~saWZ_y?@h&ngO?6ِ/xDG)V -V6Қ~yBOvI<(A6qsR䝗gË.+We='=ZщQz)rQ\8 P_8qX.7_,ңjUjUf'2X/aE-Evv?&}^@Q!{Nq7qa4yYܑ; C_h)?)wW`YV7U;"/LB@ *6"vjE:=#œWn)wfKqb& }ߦk7??R-#<`-b><(@ 17AVa^@?66Y/ʼn>lHRO(mݾ}^ Ue@}ufXN]􃜱+wdu]H)<^oZo}H?y@p}uBܸ{vN~./Z".p5^l_/zLgiE4b&nc=Ǵuy\>;+sNY+Vds;ؐL4^F0z'~l=ڋX0$7@e% IDAThxF|Vȥ+Ʃi+Ac͖.K+"2=>"G^J(OUMI,oPͱ9D@0J+iYh3ezlX>xL.2+du}[,v?o/JENP6ï{6أw v_t6q_-{krY|vN̎IA1mʝUY!OU279*b(wɗ Vy7bI&XLyn|t8A!X\ސץVɆkY/EnhmSZX6n\80@J17T`]>~Pw⪬od5;ܐZݠwzւӹdTO`C@b4T+y)y@4[-Yx! +>o/C}2;ِZ)m= ;"FRWe t}uz1upX>tLyh?> ƶY\P]f&F⟫^CVS-cD)4Ӊz_M~yY9w|P6rwi]nfUFdj|$qS{ m՛ c(_U֡whx&ӎ|xa^~qC!YYߒ;kz)j p˰Uya46?s : g}uevbTk'ݳs2Pde}+륨396$rְ+Fc\j);K/GЂMJg/'x%9op$iD i唿oT*rtbDFY/njn(bÐXIAn?DcHN{礯iPL6V1>o/jEN`K Pk*"r?W}XpV?W5%ĕjU;oc4-pMVujUlH=uq׽lɧ=lb6t}n\8`ǫEՋS%"KrѺ4BGeШT!J{qZ'l_DŽߝ 9%ah-@c.%'H]*k;o/2=1"J:Y6`9sη(R"*,SLM|ld@|Of'xGْ{eie C2q`(e{-^zT}uቝD hl`16jUu($;r{qUw^Jp&ɡ 4TfIƯ*P[X͠p)lV:{>: )vM&[Y/%8JELHc: NOzw'SW@6q+G䯼~7@R,oɝ5-GRĨ g`?=_3oEʰΏ$uP[ۻrg=Z٩ עksPkB~y˖R?V!!#( m⊚sra ZU'R\S{ &{iyC>\ַ@Â*y P$c(dc![? 5jH_MPKzZmG-jv2 l91u7 @f-h]K~{Ɇ"=SŸ^ ыVyP gUyɣM^CˑC6d F)_8(4j@b;ov!lv,,o?ۋ pfymK,'k2?Րz_+e! on !bEى1X nPā*l"m7/e(-;r޲|`Ev14ɱܷT7!}F* "oPԲA [xfƶ3+KJhOՒK퇲r2p]Rm?oncW_FjR;`7?W.Eozk䯿sZ>֮^X͒>翗#2=>"˪ϺH|<cPKvzbwS y| oxM/O'z\Cu|GْeiyPDɃCVsSRg-Q@.h @ 4yM~O˙YKj'Cyx)we?R#s,c73vgbQ|僀INSd63tE6f%\On.Ʃiyiy N,b۔k>/JEf&F)F!wax9\՛`L'J&-7_T9st\~y>`w;3,,˃ w1WԪUz-8sjՍ/"*k"oHІM[gR'Gg맦.PַvlnqjUlH3)D)S]'+]'vXpn7- kcwo[߻w]~_^iyԴ2!4-pMVɽ k2!v)yvO ?ſN(`xfo}&?kSs2h[rwqMvv9o/C}2;ِZ+F!2ꗚ*;K'o [0M<3ӗ׷~qS~RɅ8 cg)wWee};$Gc.3#RSsnۺ?Y0-op -h&.&nx;O3G{gQ5@T7ui6Ielt@GˊSr^}U,E-A6qu6'!䨼´=$mޕۋRؐL4"Fq Q <{ْOR:!7 R+l:6;qoEnL}&sƩixrCjɃGDR%W 8Ԛ]7١^-߆]<.PM׹pMϿ?/dt.N_^,16rwqMvhvT*ѡŤ CWmA%+sgswd &n7]a-_ɟ|qyԴQm6Һ/%/΍맦32;!,lk!UdC-D?bZ0,3'o`t}t[|rcA>@O% y+wWeC2_Qu)˲Ս݇D:u+KN0M\U]!l7-ˍ{OQyu戼ptb~f dn!}S&[Rgݏg|47)u7(v bdYZX3 '& G̞.{qU68C2;1*gJIvXѿ⾝_ ?ſN +V&^S*_I߿=>D2>ZC͖{&K+gL<]f%[j;>i⿳ptFڛx&Z9.!/>'> o0#?zN4\V[׷6^J.8|pHd Qn/Ϻ{K;b: E+ݠ'4&n;=O4[-57N?3ȇ6⪬og056,\5o˱n˽k#U{YZV34&n;=~(7n/?o726\KO/ʅ&᰷%Zȑi ׭Q{f*?ar!7 R NϦﭥ,mOqC~2P'/7_~llʝ+!TlLomǡ֤u|iyt&ſ&폲#?~[~~U yԌr(C[͖{.K+}zDUe~!Zϱ Y׌N#{*W|n:[4&n7]agi9D<=#oxT'"rgqMwoC&׽OvyoYf%׺~ſ; iIw3@q6q;yrzv\"l݅5Y^D? dnjTjg>1~Y}qw*R(dQŲ^lvSq&vMϿly%)5FcymVlu X Aw~H9gΙ93y^Nċ99$gܙwpHGǛ=3 7O~5 68pF=DpZ<;_By Νs\TZwi~ߏA :Y&Ϳ88'$n?I#'?DS/h^C'jwxDc箂 fe g|#YwvGtpϮC hdtAH_xA`pIN&feo,MLzt^utDz5kѲERūDZk`h\1[YxV.=L򩅳lzv~@|;UhɆ߫i$n߻$7^M5HRPO߀nb^\p*]jQi=4I>3k٢cLӟ˖ ]s3aeLLմu^ɪo(o`H-x.*>%i=8ރ7ꬥ teKW Tg{%Vu`pTC#i]YQ$Z@Kϡw17N5sHӵ?egP?Pnlp7N2E'?Dp]Yzɥg9F'tjzLGGg.b8So]LO߉+V,G0ш @X$n:<y5q8Ccg]Y׫Esg%"VitvDZbsZcRo 4'ߴh0 s`p7O>d q'u^-]0[/sWNrfuu:m uwC{4P7 \R7^M4k^HbN]j?Rw\꟰38Ԙܬ;Ո=82=CjO`6+ mۇ[m;f;~̝g.TG*ԇB7iWBTɨ6WF8MojV潃uފENSmf`ù2ug{uoʟ3i͐}` 4lq7ek H/s/TC滿Gm<7N5yH~|i 󠓴I4}6D8GUw@1YgʥW~β6KRO/8]gG.XX J<κ=]o8wO߀8bf yA4@$4V!sʹ n.ov=۝˲oq$Ϳ_/B'q!'~u& OgG?k͛m'ZCp#%ޙ&c4:߫ixI>;> 0LoxɺK١ Z}3@XNEo'լc},ߧ24txdBG($sgwU5gVWڴIE|p@u2EL yA <;qOͿ8mW?lW̟ӥ V.֬%zh N=y86cGFdb5io16cn` =NWvkowf邕qo$m;>H8xtE7lO@j sv| 'qۘIbYMV(n/IjhlRN9SX8G- IDATl6sͺSk\wO XF#10S8o~S3Cd\xڸWX2_g7kx꬛bf@Ϳp 6gswh*T+xmw!e3hŒyX6κ?B^c$(ûs9Au7F`['x xl%i˞CkvWgEsboֺ?̀$T#gPo>n bUt@8-[%Ϳ$MM]gpT?~v&.꬛ 0{G58II΢w>B8uR)Iߩ!21PǂtXGu=̂i~9[ mzme8߯^S7h ZNm$^?nlW=&Kku=^?\Cp#{ qFXAipO{OiWQQ|OCG1k.nmnN.uOLմm` ͱ$n3:L>,2G'?4V(m kߟfuo}Hӵz% lͿo,MTS 'q {v4t}&x&KkîCzjI&Bot)Fsf_,G0,* 3NiIwxD{Z]R_h=yM}NԊ Y?KǢ̱$I6)2hǭV†\&F&}n mgss*eXwόsFX78c4? @"8{})0cLƋȫP7Gn x|MM8(VssjeYȄv>h͆;S7A'q'qtoܙ;KҶC l>pQ=i͡on.O<+ûs9Au7F?@+l KR^צ݇ĥgnMMgk˾d^m4\| $|gP'n lqOXrjjM8MڲoP?yv&k m2 ~ ,q#n0#4LB=TK${tp'|vZUK=8kޚG‹p8IҜī'~q ھxPs?19]OSC+RCTmݻ khŤ7c4:7B-BbHYdp~鿛K{7}@=v{ŽCzd>6ybm7cno>\giAo' c'qnK4Cx@SG&dׂyp;>ec߫o,M-68'SS+Ȥwd`x\[GitMmۯ )tO8sӃKc_|FP7 6K=NWviOUu}HCzp>LL93͹̿spڴkIn0>*?KǢaIߴWXV#eigݧd{kr^@o0"Y<  $^ g!2 KRa NOZFZ'wS;5mpA8sӃKSxݧ?07f`$$T4VP_q]a~hkסz` )eG{]=fo0IW Ժ!@BēGo4'P[qt|Jl٧=PXwr{Ihj` YqPܺ(#68'I<:! 6toQMju=~@Z=͹?̀⬻파Oi#;Y4NGn Y6Y 3oqd8CSu=Y<}oh} ƺgf}'Vj8߯^S7@ j້Y?#7u@[}i=eEOuqOo8 IMN-i`rʹ#:xd4,5c7?e;i4q{R C NDZ6#[ibe..^L΃M"7cp?XnVCZOk? GҞCG]ޞzq㺷g %eigݍ<ē`DͿ8FSyj@*el߸NȸXXB(#gkCM>\u̿y$i}+<] Ϳp7K`|el4 r@leہ!=iF'knH[J納;'ûs9A[7U`4 vƞkbSWnptBў}6vݍ/c;swo|8(nT1N1jSmVkC+r^_4Ut@[O~`@7r9Y4NGnY6Y 3oqd8e{W`M{5r;'rnCe'6ƴ8߯ nb;bH;nbVoOWok@$}hס'p#E? ݽ4&#c} Ժx5%@Qd3A4_u'_F'c]XqIZ]O8'vkՃ Ky|ʹ ~zvᄃisU麡@"= _$ݽ}`/oo`X֑_,)6T شkIA/پO`?| oĝi>[v{g~xV M{^.p#Exw''b`&68;<̏ ^pB뙾CztMN78˧ۀug{6lsx4@n|$̏J62p=#wn?`}6Pu7߇,[_@/;c4UXnVS g̘ix4%ؽzY8Ĵnڧ{=-FSqnrdC4fÝ,W #lUi?x\ߑCjce$8֦tdt?⬻m%޹v d4"X(n568;Eo=Qrq=VO&ßD5/'ͷ<6=@lY}`7YqPܺ@{lpw*:! yPݺ靊cQ֗z6PMqLS7!68;}OhVs~.RާNu0FOn _u'E"4w Kk%Xz|s1|`>wh \Z(44h$L7C->oܽ2DZ~b޺́_޸k@쭺{v5 (κSֱB@qP؀$^?v 2iIθoXw=]ϴ6)ɿ=?#>"Lڴky59OXqz,gս#]?M۵Dk]?{kr4 /U?c8Dy?*=rÇr /A@@Rĝ$Wi:P/IW7kKKTalbZO&{=:YwSjǛ4fóO] @ձ$IܩOk=or  SΔ,76iOe; s8'ꍃ l8I<WGm7{HSM=vLڍڱwsC!п?}܄? Mj~ 71A8Nb} /l)qw"[/di')kX]ҞRLNP=>?i&ۯ .O}7Nm 6qFEnpuP>l)xo}y8)$?őM?v^bprF861oݠGqnc}g)_s{`s BuS qO enOy@.7ã17<>S'[rss`pDGXVͿq Q7ؤNА _GwSAZ*SdP74[@B]O0Qy5qՁ# u/4<6o޷AS7ɸ 8ֿۭzt4cY3ji$"D'q |_Ltں_-zhn/?ա%wNaH},Oqu|6B`pd\~ N?8082?{=Z]?^7`?U4"Xn @"4!h`ww]̶J7ICc=NFnck7C2H dU; 4lA)IͿo(z].ޑp0m.]ͧr`ú-۫n䆩iwʽH>SKr)#h 574j kfo mcsLgwٜ|"שv}zPh> ;s M||h6MU {l.q(NA! ْ;<~}uqs R@\bͿw]rz]Л*ZWlyPw< ziߥ1;5/[n6LpO<o3d;؛r>qv}Ӵ-=iOh ?7c:EvIܓͿ8 ^22Nzyπ&>= :tS;rCSo>G5];Ho/}@1r8hC`H' GVZ-݇R"[߼W?mo`!ݨw(42p71 aH''S}+3וGzvk7 ~=}~nv.77rlqB?c%|#}f n]|##|QMղ{C4~`ع!{ p FUNmv8ͿԺ|^zCfJn'>vKo|6v:wU̼ `?y}IC+ȤjlͿ$v=Xzl^Á_4<݀w ]Co1hV? sQh=NAnh/V󟺕ˣwhD!zU? c6=u@ (O?:1?>i=4ܧj Oj6j@R%>7IBy I0e}O3L}@!G[pe i /Aשr;lIp(NA O/o8'멭 >jm5w=/;O%|4fArC+@>R$jUj=0v;Gb-Z VpZۏ}> Mf8 5Y_HR)I? vGyh|4~! M`V٤{->ߓֹu;kZkxlR; J*_nFTE`'ף IDAT,d)}_cav@c'jڍڱw|0'rCGߡ+~I |w_@hpO2q&#v]o{ĴF2RܐUir-=5rw:7Np71˘`xeHXwnOYwucTy3 &7 _mimwiOo(6DrU lF8^Eo'E,4u3;}{t!rnOe G߮/'3r !_sn@+I~Olݧ }ɞPONm]ot-٠Q{.7ni}TX 4;ԫ'S {l$!NdgPߎ?:=u_cpxx\w=]_)ms8*O] j7lؖ}z'7N9Sqt{DͿ)2 eЕ$^eARry4c\ON/>bjY\ ͞q{cШvY͒MQon8|t\w^Y;{\1ÕA4cYW e@'!2>[0hK^?Pl $Np> |e+ 郌NL׵Wcnٖ݇N}`F &7@9{v)xUo?yL b冓ܳnW3;ZU' XSdǥ 5 |?RI;e4(>CVqx[ 1r53f* ; ԱR)l:.mL |(bnϿ>Ȅ}Orgq:Dn6r/AjhmSۇ;.]nXݧw|{zr~ <簾f  * JW;$px)~47ov)>΍z|ʘ-726(I$a.'Tr* T+#7xaQӵ>GM^/ q˘ - |q2!ե'O 2 a\||>_@O{m٫/ݱbdArQr}ҨIZ>B,,inH8rC#Q}ooͷ=ɩB,mGnj&~bUx5 |7}?x5U ޯ 37n)~KzhkyrOmۯ;5:1e8`3^@lT$^?pbQ;Si6+ۭ|6 v*~Fy]y/(\?u^]RQxɦ=WOo &7(>6~b zB6v(ǡorCSc#G6kz5kՕТ[=|t\w>]d60N$7N2An7E%r2b@'q |{)S"7$rCWre 3E

78xq O(MS;j0eƱ@n.$Nv2 |! M=GJ/Nnp@8Iq q(O! s+B ƫ3j$7 G37K\"^\2_h%ohQu\JKң^vo=Ui'~q䇷8i)(]Ȫ'7+[ne2<ܐVʘpK]|-Zl鞔tcE.& T^7JzXybAS[7N"Dpq ɇ77Xb?wo᯾F r1]Mˣ(zd@HbG>f7c4(~[77H9]Dnp&܀͙ե+^J+EG\N8ϒ# Ws>ip:80䩄HEPO 7郐 G)DnxE<ڋZ}b>c(*68~1qӗQ_iE.( q([!e v!7`>37Kv=u(zbN$y'-}ݻ51UK3[?Z`~T1Ģo 䆔ah3E:s}IzkEw(*6T;%Wщi=~=i޼O FS্e4?q,7Te,c+!h}cOm7^s/Q9S(:6qJ>,82mu1<2#?J FX?}38?̆҆w--҅g- >CW_tnpn^oEQ78 @q_$>I彖&2ݤ){MW]2QR_<—ZqLg1rC-]o&eX;KguEQ=!{@(%+ˍiI͙թ9DjͿSϙթEfhg,Ӳ>fVtui9y/'4@sΞvMEJzy\M |8 1lC%wSESy/@ QmtG^K9Gs40V(>EtދBFE{tS1\ٚ?S(WA*/!IE'^Pl DQ4E{%}XAR9 |?#7p0hKQIEQ (>'i;^Kqx`? @(?(h EQC[$}P.=CSa7!>?e 7}iI7EQQ3X( .9I9/)@ {CnՃwHoK>y/(26(Eч%]*%3'ITn2cY 7ʌq!g/>)(w/Ա%/%i_.A[dHonphDA X=uaEh ePE;(#IJz4BE6T@zsC?.>IEQ?^W ($JL ɩ)CxdLH ~T'Cx\n4Ey\r`@lͿ)(3Aܬ&TY(U)74`3p )Oc]ܐ%r6l(p RH (cO@  I@lͿ}\ | .B -6,߿ |71)ؖ77dN GY6~7^ >`ҹ,c@N,hPMi>\b@ OC g 7kl(q(ς 74Kn/[i |L},7ꁠ I m rCv8Cn #h1(?cl@yS |\0IeY`gely>?r$ Tys{ srg#7@OC ! 6?y +>7x"<.'6(i$,s'{46?y, |EWn큠@gqO 2j}yJj()Vn -Vx(~C $l8-V@/"#7rBl,c8N?ZL@}_''c j a8 6~;W)-6x}t^IDAT4)Cxv4(*}{l® Pp ([i qiDiv^H (c_}4(rCv8Dnm1? hQT@#)~ Y6f~@vm6k@A(3‘E| Eєi Z`| OpFG ; ~A _ЎUx6-V}ȇe^;i & X]l^ h|cAn9h3謥 NͿ }+Gڪ38a%7vlX/yWEVOHǣ(s~c.١\w?$=KWsiKonteIrҸSҴٳ:/%74u %D(t_yjЬe_ZANgG4ޛf0!AIَѬV9m6:;֗\f{-@WlX2Oo|- jS 3ε&i`u^;k麋DnY&MIzrL|G|uDQïC2 qӕi&~-$}KҰw5IkDn@(Z b@b. D%3-w"?s/]sJ>&ki\? /=w~'P܀4lsLA^LY_.+ISWkY9M2dKһ^uɧ%}5(Ty?}ϙzrmSb@Bl0l/_NR I_J;ϕ/'dk3|_֟(~/f`$PQ=*sgӿԛEKt%TH-IZvwn[cM\Ii[u_.N:;J]{]E Y:Vmߪ&y:@_:wݤp^Eу.&P=\ WW*W/_Z</lTO8'4gVݯuKҗia+' \|>[os]U/I@ Lǟ_]uȨ+kC'o9ќ%sn aKY?}kt%J6C&D5qyIt1]=6}(^zorvH.î&P=lp&㹒t&kĿX?UDsXAO׬ч~fwu 6cՄ8IzL.}c6}(~妫?2͞,MHzUE@58.DZzFOkjr4g.k6#ʥ JQIT͒~ _I3pTk'e7BWg~W^H;ɗ(zI8*}ߠ+hCy-[:7_ԋ5g(zR/8OsV?޵^vo&ۯ&Hn˲\v>}7DQt@ ]99]ͷ<:MLN A^f~?W^{D(19a7qV҇\ϻOu;7i[ Ї~^~^IzRMQ>doXw=xu{^qyB<.Q zMdz$}W[}̿nA}'O- 2nh~WUWg=kqqֱ[c'ݧ~'zj^Á.9[~5zUku,@% MIo]5$ܚ? vW^}y׮ы.\%I7p-;bĢھZٛ]]}^wyzg85$\EYNv<_kr٩xS$!5睩H7B]rβ,C.? S> 3ǟ wޛU =iڸG?ٴWGNVA4 K_x^yjla٬}WүFQ4Gpdǟ pweq=ݫHOpe@ YW_uY(e|K;(#8`@D_i'QO6Swo{OUmS^ɂ93ugKV W -%}\Ǣ(;ROH^Z}8m,]8WW]RW_R7\v,uvj>&QqY8_cϮsCcڸk@wҺ̎N5gQEf뚋V˵e:s|/+[үDQ6q&I_4u<2 =S[k"e^+/X5矩5ЕЙKk;$bEq|H$ﵜ01U]PwosXU'k%k;;"wbp2]yw/;wfwezQFRXa!xq_cyr6ڲ{@}CZQtvt輕u+t+u+u35+w[G;(d :$?Qe|lRz}G}?hAmwDS_buyu񪥺謥x}But ұo^B8Sdn2&&jj\ՅHEZV*"(Rх] *Pu!@Bl(IdMf:iwqnjK:m2;l.9S6JMm4z|ȱ wk!16Z$Krg,kgxl/ɉgpl+kY^}*Wֵץ7-myb['dzkj{fvLd7fG-:iV`J$oi$_NrO)e~ z[^uǓ|%ݥs$^c>KltO}|ruHW$x&8g%ɷ|: sC: IJqS,<: \M$_J$qr|'ɷ +`$Zg >dq^~)euh:I>O:GJ)a.)։$I w$NoJ)O7CekM$75%U{̥0 TkK$NxvN$?.,nk;LMEj^7O["`Z{J$cmmY%ϒP80tÀwDr>ɟ<]=8e16Z$I$MmMo1>Mֺ;ze{FalZ֒ .ߞPFR#$K)FcŽ64;nwlVRkݗMIޚmIޘdiu6ɟ-[Ϸ\L$-G[Z']868%4T; նjko{moPSm#cpOrk [LuJrspri*`Z½e+Bݟ5dNܒ]Dߒ׷u g;-K)6M A=w(psIf[溄~9$Rk `D\ZT 曒ܘQ=c_6 SIKr<IǣINX)e Zuځ$uٞt_ ^Oiz `\%./p^M<8N%R6,$K2{m%ݝ \J9a!.@w\IENDB`python-telegram-bot-21.1.1/docs/source/stability_policy.rst000066400000000000000000000201701460724040100240400ustar00rootroot00000000000000Stability Policy ================ .. important:: This stability policy is in place since version 20.3. While earlier versions of ``python-telegram-bot`` also had stable interfaces, they had no explicit stability policy and hence did not follow the rules outlined below in all detail. Please also refer to the :ref:`changelog `. .. caution:: Large parts of the :mod:`telegram` package are the Python representations of the Telegram Bot API, whose stability policy PTB can not influence. This policy hence includes some special cases for those parts. What does this policy cover? ---------------------------- This policy includes any API or behavior that is covered in this documentation. This covers both the :mod:`telegram` package and the :mod:`telegram.ext` package. What doesn't this policy cover? ------------------------------- Introduction of new features or changes of flavors of comparable behavior (e.g. the default for the HTTP protocol version being used) are not covered by this policy. The internal structure of classes in PTB, i.e. things like the result of ``dir(obj))`` or the contents of ``obj.__dict__``, is not covered by this policy. Objects are in general not guaranteed to be pickleable (unless stated otherwise) and pickled objects from one version of PTB may not be loadable in future versions. We may provide a way to convert pickled objects from one version to another, but this is not guaranteed. Functionality that is part of PTBs API but is explicitly documented as not being intended to be used directly by users (e.g. :meth:`telegram.request.BaseRequest.do_request`) may change. This also applies to functions or attributes marked as final in the sense of `PEP 591 `__. PTB has dependencies to third-party packages. The versions that PTB uses of these third-party packages may change if that does not affect PTBs public API. PTB does not give guarantees about which Python versions are supported. In general, we will try to support all Python versions that have not yet reached their end of life, but we reserve ourselves the option to drop support for Python versions earlier if that benefits the advancement of the library. PTB provides static type hints for all public attributes, parameters, return values and generic classes. These type hints are not covered by this policy and may change at any time under the condition that these changes have no impact on the runtime behavior of PTB. .. _bot-api-functionality-1: Bot API Functionality ~~~~~~~~~~~~~~~~~~~~~ Comparison of equality of instances of the classes in the :mod:`telegram` package is subject to change and the PTB team will update the behavior to best reflect updates in the Bot API. Changes in this regard will be documented in the affected classes. Note that equality comparison with objects that where serialized by an older version of PTB may hence give unexpected results. When the order of arguments of the Bot API methods changes or they become optional/mandatory due to changes in the Bot API, PTB will always try to reflect these changes. While we try to make such changes backward compatible, this is not always possible or only with significant effort. In such cases we will find a trade-off between backward compatibility and fully complying with the Bot API, which may result in breaking changes. We highly recommend using keyword arguments, which can help make such changes non-breaking on your end. .. We have documented a few common cases and possible backwards compatible solutions in the wiki as a reference for the dev team: https://github.com/python-telegram-bot/python-telegram-bot/wiki/Bot-API-Backward-Compatibility When the Bot API changes attributes of classes, the method :meth:`telegram.TelegramObject.to_dict` will change as necessary to reflect these changes. In particular, attributes deprecated by Telegram will be removed from the returned dictionary. Deprecated attributes that are still passed by Telegram will be available in the :attr:`~telegram.TelegramObject.api_kwargs` dictionary as long as PTB can support that with feasible effort. Since attributes of the classes in the :mod:`telegram` package are not writable, we may change them to properties where appropriate. Development Versions ~~~~~~~~~~~~~~~~~~~~ Pre-releases marked as alpha, beta or release candidate are not covered by this policy. Before a feature is in a stable release, i.e. the feature was merged into the ``master`` branch but not released yet (or only in a pre-release), it is not covered by this policy either and may change. Security ~~~~~~~~ We make exceptions from our stability policy for security. We will violate this policy as necessary in order to resolve a security issue or harden PTB against a possible attack. Versioning ---------- PTB uses a versioning scheme that roughly follows `https://semver.org/ `_, although it may not be quite as strict. Given a version of PTB X.Y.Z, - X indicates the major version number. This is incremented when backward incompatible changes are introduced. - Y indicates the minor version number. This is incremented when new functionality or backward compatible changes are introduced by PTB. *This is also incremented when PTB adds support for a new Bot API version, which may include backward incompatible changes in some cases as outlined* :ref:`below `. - Z is the patch version. This is incremented if backward compatible bug fixes or smaller changes are introduced. If this number is 0, it can be omitted, i.e. we just write X.Y instead of X.Y.0. Deprecation ~~~~~~~~~~~ From time to time we will want to change the behavior of an API or remove it entirely, or we do so to comply with changes in the Telegram Bot API. In those cases, we follow a deprecation schedule as detailed below. Functionality is marked as deprecated by a corresponding note in the release notes and the documentation. Where possible, a :class:`~telegram.warnings.PTBDeprecationWarning` is issued when deprecated functionality is used, but this is not mandatory. From time to time, we may decide to deprecate an API that is particularly widely used. In these cases, we may decide to provide an extended deprecation period, at our discretion. With version 20.0.0, PTB introduced major structural breaking changes without the above deprecation period. Should a similarly big change ever be deemed necessary again by the development team and should a deprecation period prove too much additional effort, this violation of the stability policy will be announced well ahead of the release in our channel, `as was done for v20 `_. Non-Bot API Functionality ######################### Starting with version 20.3, deprecated functionality will stay available for the current and the next major version. For example: - In PTB v20.1.1 the feature exists - In PTB v20.1.2 or v20.2.0 the feature is marked as deprecated - In PTB v21.*.* the feature is marked as deprecated - In PTB v22.0 the feature is removed or changed .. _bot-api-versioning: Bot API Functionality ##################### As PTB has no control over deprecations introduced by Telegram and the schedule of these deprecations rarely coincides with PTBs deprecation schedule, we have a special policy for Bot API functionality. Starting with 20.3, deprecated Bot API functionality will stay available for the current and the next major version of PTB *or* until the next version of the Bot API. More precisely, two cases are possible, for which we show examples below. Case 1 ^^^^^^ - In PTB v20.1 the feature exists - Bot API version 6.6 is released and deprecates the feature - PTB v20.2 adds support for Bot API 6.6 and the feature is marked as deprecated - In PTB v21.0 the feature is removed or changed Case 2 ^^^^^^ - In PTB v20.1 the feature exists - Bot API version 6.6 is released and deprecates the feature - PTB v20.2 adds support for Bot API version 6.6 and the feature is marked as deprecated - In PTB v20.2.* and v20.3.* the feature is marked as deprecated - Bot API version 6.7 is released - PTB v20.4 adds support for Bot API version 6.7 and the feature is removed or changed python-telegram-bot-21.1.1/docs/source/telegram.animation.rst000066400000000000000000000003241460724040100242320ustar00rootroot00000000000000Animation ========= .. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject .. autoclass:: telegram.Animation :members: :show-inheritance: :inherited-members: TelegramObject python-telegram-bot-21.1.1/docs/source/telegram.at-tree.rst000066400000000000000000000075661460724040100236330ustar00rootroot00000000000000Available Types --------------- .. toctree:: :titlesonly: telegram.animation telegram.audio telegram.birthdate telegram.botcommand telegram.botcommandscope telegram.botcommandscopeallchatadministrators telegram.botcommandscopeallgroupchats telegram.botcommandscopeallprivatechats telegram.botcommandscopechat telegram.botcommandscopechatadministrators telegram.botcommandscopechatmember telegram.botcommandscopedefault telegram.botdescription telegram.botname telegram.botshortdescription telegram.businessconnection telegram.businessintro telegram.businesslocation telegram.businessopeninghours telegram.businessopeninghoursinterval telegram.businessmessagesdeleted telegram.callbackquery telegram.chat telegram.chatadministratorrights telegram.chatboost telegram.chatboostadded telegram.chatboostremoved telegram.chatboostsource telegram.chatboostsourcegiftcode telegram.chatboostsourcegiveaway telegram.chatboostsourcepremium telegram.chatboostupdated telegram.chatinvitelink telegram.chatjoinrequest telegram.chatlocation telegram.chatmember telegram.chatmemberadministrator telegram.chatmemberbanned telegram.chatmemberleft telegram.chatmembermember telegram.chatmemberowner telegram.chatmemberrestricted telegram.chatmemberupdated telegram.chatpermissions telegram.chatphoto telegram.chatshared telegram.contact telegram.dice telegram.document telegram.externalreplyinfo telegram.file telegram.forcereply telegram.forumtopic telegram.forumtopicclosed telegram.forumtopiccreated telegram.forumtopicedited telegram.forumtopicreopened telegram.generalforumtopichidden telegram.generalforumtopicunhidden telegram.giveaway telegram.giveawaycompleted telegram.giveawaycreated telegram.giveawaywinners telegram.inaccessiblemessage telegram.inlinekeyboardbutton telegram.inlinekeyboardmarkup telegram.inputfile telegram.inputmedia telegram.inputmediaanimation telegram.inputmediaaudio telegram.inputmediadocument telegram.inputmediaphoto telegram.inputmediavideo telegram.inputsticker telegram.keyboardbutton telegram.keyboardbuttonpolltype telegram.keyboardbuttonrequestchat telegram.keyboardbuttonrequestusers telegram.linkpreviewoptions telegram.location telegram.loginurl telegram.maybeinaccessiblemessage telegram.menubutton telegram.menubuttoncommands telegram.menubuttondefault telegram.menubuttonwebapp telegram.message telegram.messageautodeletetimerchanged telegram.messageentity telegram.messageid telegram.messageorigin telegram.messageoriginchannel telegram.messageoriginchat telegram.messageoriginhiddenuser telegram.messageoriginuser telegram.messagereactioncountupdated telegram.messagereactionupdated telegram.photosize telegram.poll telegram.pollanswer telegram.polloption telegram.proximityalerttriggered telegram.reactioncount telegram.reactiontype telegram.reactiontypecustomemoji telegram.reactiontypeemoji telegram.replykeyboardmarkup telegram.replykeyboardremove telegram.replyparameters telegram.sentwebappmessage telegram.shareduser telegram.story telegram.switchinlinequerychosenchat telegram.telegramobject telegram.textquote telegram.update telegram.user telegram.userchatboosts telegram.userprofilephotos telegram.usersshared telegram.venue telegram.video telegram.videochatended telegram.videochatparticipantsinvited telegram.videochatscheduled telegram.videochatstarted telegram.videonote telegram.voice telegram.webappdata telegram.webappinfo telegram.webhookinfo telegram.writeaccessallowed python-telegram-bot-21.1.1/docs/source/telegram.audio.rst000066400000000000000000000003101460724040100233470ustar00rootroot00000000000000Audio ===== .. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject .. autoclass:: telegram.Audio :members: :show-inheritance: :inherited-members: TelegramObject python-telegram-bot-21.1.1/docs/source/telegram.birthdate.rst000066400000000000000000000001351460724040100242210ustar00rootroot00000000000000Birthdate ========= .. autoclass:: telegram.Birthdate :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.bot.rst000066400000000000000000000001121460724040100230320ustar00rootroot00000000000000Bot === .. autoclass:: telegram.Bot :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.botcommand.rst000066400000000000000000000001361460724040100243770ustar00rootroot00000000000000BotCommand ========== .. autoclass:: telegram.BotCommand :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.botcommandscope.rst000066400000000000000000000001551460724040100254320ustar00rootroot00000000000000BotCommandScope =============== .. autoclass:: telegram.BotCommandScope :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.botcommandscopeallchatadministrators.rst000066400000000000000000000002541460724040100317470ustar00rootroot00000000000000BotCommandScopeAllChatAdministrators ==================================== .. autoclass:: telegram.BotCommandScopeAllChatAdministrators :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.botcommandscopeallgroupchats.rst000066400000000000000000000002241460724040100302200ustar00rootroot00000000000000BotCommandScopeAllGroupChats ============================ .. autoclass:: telegram.BotCommandScopeAllGroupChats :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.botcommandscopeallprivatechats.rst000066400000000000000000000002321460724040100305350ustar00rootroot00000000000000BotCommandScopeAllPrivateChats ============================== .. autoclass:: telegram.BotCommandScopeAllPrivateChats :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.botcommandscopechat.rst000066400000000000000000000001711460724040100262700ustar00rootroot00000000000000BotCommandScopeChat =================== .. autoclass:: telegram.BotCommandScopeChat :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.botcommandscopechatadministrators.rst000066400000000000000000000002431460724040100312540ustar00rootroot00000000000000BotCommandScopeChatAdministrators ================================= .. autoclass:: telegram.BotCommandScopeChatAdministrators :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.botcommandscopechatmember.rst000066400000000000000000000002131460724040100274550ustar00rootroot00000000000000BotCommandScopeChatMember ========================= .. autoclass:: telegram.BotCommandScopeChatMember :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.botcommandscopedefault.rst000066400000000000000000000002021460724040100267700ustar00rootroot00000000000000BotCommandScopeDefault ====================== .. autoclass:: telegram.BotCommandScopeDefault :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.botdescription.rst000066400000000000000000000001521460724040100253020ustar00rootroot00000000000000BotDescription ============== .. autoclass:: telegram.BotDescription :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.botname.rst000066400000000000000000000001251460724040100236770ustar00rootroot00000000000000BotName ======= .. autoclass:: telegram.BotName :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.botshortdescription.rst000066400000000000000000000001711460724040100263630ustar00rootroot00000000000000BotShortDescription =================== .. autoclass:: telegram.BotShortDescription :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.businessconnection.rst000066400000000000000000000001661460724040100261720ustar00rootroot00000000000000BusinessConnection ================== .. autoclass:: telegram.BusinessConnection :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.businessintro.rst000066400000000000000000000001541460724040100251630ustar00rootroot00000000000000BusinessIntro ================== .. autoclass:: telegram.BusinessIntro :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.businesslocation.rst000066400000000000000000000001621460724040100256370ustar00rootroot00000000000000BusinessLocation ================== .. autoclass:: telegram.BusinessLocation :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.businessmessagesdeleted.rst000066400000000000000000000002051460724040100271630ustar00rootroot00000000000000BusinessMessagesDeleted ======================= .. autoclass:: telegram.BusinessMessagesDeleted :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.businessopeninghours.rst000066400000000000000000000001741460724040100265520ustar00rootroot00000000000000BusinessOpeningHours ==================== .. autoclass:: telegram.BusinessOpeningHours :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.businessopeninghoursinterval.rst000066400000000000000000000002241460724040100303130ustar00rootroot00000000000000BusinessOpeningHoursInterval ============================ .. autoclass:: telegram.BusinessOpeningHoursInterval :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.callbackgame.rst000066400000000000000000000001451460724040100246420ustar00rootroot00000000000000Callbackgame ============ .. autoclass:: telegram.CallbackGame :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.callbackquery.rst000066400000000000000000000001501460724040100250720ustar00rootroot00000000000000CallbackQuery ============= .. autoclass:: telegram.CallbackQuery :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chat.rst000066400000000000000000000001151460724040100231700ustar00rootroot00000000000000Chat ==== .. autoclass:: telegram.Chat :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatadministratorrights.rst000066400000000000000000000002361460724040100272160ustar00rootroot00000000000000ChatAdministratorRights ======================= .. versionadded:: 20.0 .. autoclass:: telegram.ChatAdministratorRights :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatboost.rst000066400000000000000000000001631460724040100242420ustar00rootroot00000000000000ChatBoost ========= .. versionadded:: 20.8 .. autoclass:: telegram.ChatBoost :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.chatboostadded.rst000066400000000000000000000001521460724040100252220ustar00rootroot00000000000000ChatBoostAdded ============== .. autoclass:: telegram.ChatBoostAdded :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.chatboostremoved.rst000066400000000000000000000002101460724040100256150ustar00rootroot00000000000000ChatBoostRemoved ================ .. versionadded:: 20.8 .. autoclass:: telegram.ChatBoostRemoved :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.chatboostsource.rst000066400000000000000000000002051460724040100254600ustar00rootroot00000000000000ChatBoostSource =============== .. versionadded:: 20.8 .. autoclass:: telegram.ChatBoostSource :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.chatboostsourcegiftcode.rst000066400000000000000000000002351460724040100271700ustar00rootroot00000000000000ChatBoostSourceGiftCode ======================= .. versionadded:: 20.8 .. autoclass:: telegram.ChatBoostSourceGiftCode :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.chatboostsourcegiveaway.rst000066400000000000000000000002351460724040100272200ustar00rootroot00000000000000ChatBoostSourceGiveaway ======================= .. versionadded:: 20.8 .. autoclass:: telegram.ChatBoostSourceGiveaway :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.chatboostsourcepremium.rst000066400000000000000000000002321460724040100270570ustar00rootroot00000000000000ChatBoostSourcePremium ====================== .. versionadded:: 20.8 .. autoclass:: telegram.ChatBoostSourcePremium :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.chatboostupdated.rst000066400000000000000000000002101460724040100256020ustar00rootroot00000000000000ChatBoostUpdated ================ .. versionadded:: 20.8 .. autoclass:: telegram.ChatBoostUpdated :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.chatinvitelink.rst000066400000000000000000000001531460724040100252670ustar00rootroot00000000000000ChatInviteLink ============== .. autoclass:: telegram.ChatInviteLink :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatjoinrequest.rst000066400000000000000000000001561460724040100254660ustar00rootroot00000000000000ChatJoinRequest =============== .. autoclass:: telegram.ChatJoinRequest :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatlocation.rst000066400000000000000000000001451460724040100247240ustar00rootroot00000000000000ChatLocation ============ .. autoclass:: telegram.ChatLocation :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatmember.rst000066400000000000000000000001371460724040100243640ustar00rootroot00000000000000ChatMember ========== .. autoclass:: telegram.ChatMember :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatmemberadministrator.rst000066400000000000000000000002061460724040100271620ustar00rootroot00000000000000ChatMemberAdministrator ======================= .. autoclass:: telegram.ChatMemberAdministrator :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatmemberbanned.rst000066400000000000000000000001611460724040100255310ustar00rootroot00000000000000ChatMemberBanned ================ .. autoclass:: telegram.ChatMemberBanned :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatmemberleft.rst000066400000000000000000000001531460724040100252350ustar00rootroot00000000000000ChatMemberLeft ============== .. autoclass:: telegram.ChatMemberLeft :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatmembermember.rst000066400000000000000000000001611460724040100255510ustar00rootroot00000000000000ChatMemberMember ================ .. autoclass:: telegram.ChatMemberMember :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatmemberowner.rst000066400000000000000000000001571460724040100254410ustar00rootroot00000000000000ChatMemberOwner =============== .. autoclass:: telegram.ChatMemberOwner :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatmemberrestricted.rst000066400000000000000000000001751460724040100264570ustar00rootroot00000000000000ChatMemberRestricted ==================== .. autoclass:: telegram.ChatMemberRestricted :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatmemberupdated.rst000066400000000000000000000001641460724040100257330ustar00rootroot00000000000000ChatMemberUpdated ================= .. autoclass:: telegram.ChatMemberUpdated :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatpermissions.rst000066400000000000000000000001561460724040100254710ustar00rootroot00000000000000ChatPermissions =============== .. autoclass:: telegram.ChatPermissions :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatphoto.rst000066400000000000000000000001341460724040100242430ustar00rootroot00000000000000ChatPhoto ========= .. autoclass:: telegram.ChatPhoto :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.chatshared.rst000066400000000000000000000001501460724040100243560ustar00rootroot00000000000000ChatShared =================== .. autoclass:: telegram.ChatShared :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.choseninlineresult.rst000066400000000000000000000001671460724040100261750ustar00rootroot00000000000000ChosenInlineResult ================== .. autoclass:: telegram.ChosenInlineResult :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.constants.rst000066400000000000000000000004021460724040100242640ustar00rootroot00000000000000telegram.constants Module ========================= .. automodule:: telegram.constants :members: :show-inheritance: :no-undoc-members: :inherited-members: Enum, EnumMeta, str, int :exclude-members: __format__, __new__, __repr__, __str__ python-telegram-bot-21.1.1/docs/source/telegram.contact.rst000066400000000000000000000001261460724040100237060ustar00rootroot00000000000000Contact ======= .. autoclass:: telegram.Contact :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.credentials.rst000066400000000000000000000001421460724040100245460ustar00rootroot00000000000000Credentials =========== .. autoclass:: telegram.Credentials :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.datacredentials.rst000066400000000000000000000001561460724040100254050ustar00rootroot00000000000000DataCredentials =============== .. autoclass:: telegram.DataCredentials :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.dice.rst000066400000000000000000000001151460724040100231550ustar00rootroot00000000000000Dice ==== .. autoclass:: telegram.Dice :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.document.rst000066400000000000000000000003201460724040100240650ustar00rootroot00000000000000Document ======== .. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject .. autoclass:: telegram.Document :members: :show-inheritance: :inherited-members: TelegramObject python-telegram-bot-21.1.1/docs/source/telegram.encryptedcredentials.rst000066400000000000000000000001751460724040100264720ustar00rootroot00000000000000EncryptedCredentials ==================== .. autoclass:: telegram.EncryptedCredentials :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.encryptedpassportelement.rst000066400000000000000000000002111460724040100274110ustar00rootroot00000000000000EncryptedPassportElement ======================== .. autoclass:: telegram.EncryptedPassportElement :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.error.rst000066400000000000000000000001611460724040100234030ustar00rootroot00000000000000telegram.error Module ===================== .. automodule:: telegram.error :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.acd-tree.rst000066400000000000000000000002301460724040100245320ustar00rootroot00000000000000Arbitrary Callback Data ----------------------- .. toctree:: :titlesonly: telegram.ext.callbackdatacache telegram.ext.invalidcallbackdata python-telegram-bot-21.1.1/docs/source/telegram.ext.aioratelimiter.rst000066400000000000000000000001571460724040100260700ustar00rootroot00000000000000AIORateLimiter ============== .. autoclass:: telegram.ext.AIORateLimiter :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.application.rst000066400000000000000000000001461460724040100253570ustar00rootroot00000000000000Application =========== .. autoclass:: telegram.ext.Application :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.applicationbuilder.rst000066400000000000000000000001441460724040100267240ustar00rootroot00000000000000ApplicationBuilder ================== .. autoclass:: telegram.ext.ApplicationBuilder :members: python-telegram-bot-21.1.1/docs/source/telegram.ext.applicationhandlerstop.rst000066400000000000000000000002071460724040100276210ustar00rootroot00000000000000ApplicationHandlerStop ====================== .. autoclass:: telegram.ext.ApplicationHandlerStop :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.basehandler.rst000066400000000000000000000001461460724040100253240ustar00rootroot00000000000000BaseHandler =========== .. autoclass:: telegram.ext.BaseHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.basepersistence.rst000066400000000000000000000001621460724040100262310ustar00rootroot00000000000000BasePersistence =============== .. autoclass:: telegram.ext.BasePersistence :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.baseratelimiter.rst000066400000000000000000000001621460724040100262260ustar00rootroot00000000000000BaseRateLimiter =============== .. autoclass:: telegram.ext.BaseRateLimiter :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.baseupdateprocessor.rst000066400000000000000000000001751460724040100271330ustar00rootroot00000000000000BaseUpdateProcessor =================== .. autoclass:: telegram.ext.BaseUpdateProcessor :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.ext.businessconnectionhandler.rst000066400000000000000000000002171460724040100303240ustar00rootroot00000000000000BusinessConnectionHandler ========================= .. autoclass:: telegram.ext.BusinessConnectionHandler :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.ext.businessmessagesdeletedhandler.rst000066400000000000000000000002361460724040100313240ustar00rootroot00000000000000BusinessMessagesDeletedHandler ============================== .. autoclass:: telegram.ext.BusinessMessagesDeletedHandler :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.ext.callbackcontext.rst000066400000000000000000000001331460724040100262110ustar00rootroot00000000000000CallbackContext =============== .. autoclass:: telegram.ext.CallbackContext :members: python-telegram-bot-21.1.1/docs/source/telegram.ext.callbackdatacache.rst000066400000000000000000000001701460724040100264230ustar00rootroot00000000000000CallbackDataCache ================= .. autoclass:: telegram.ext.CallbackDataCache :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.callbackqueryhandler.rst000066400000000000000000000002011460724040100272240ustar00rootroot00000000000000CallbackQueryHandler ==================== .. autoclass:: telegram.ext.CallbackQueryHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.chatboosthandler.rst000066400000000000000000000002141460724040100263740ustar00rootroot00000000000000ChatBoostHandler ================ .. versionadded:: 20.8 .. autoclass:: telegram.ext.ChatBoostHandler :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.ext.chatjoinrequesthandler.rst000066400000000000000000000002071460724040100276200ustar00rootroot00000000000000ChatJoinRequestHandler ====================== .. autoclass:: telegram.ext.ChatJoinRequestHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.chatmemberhandler.rst000066400000000000000000000001701460724040100265160ustar00rootroot00000000000000ChatMemberHandler ================= .. autoclass:: telegram.ext.ChatMemberHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.choseninlineresulthandler.rst000066400000000000000000000002201460724040100303200ustar00rootroot00000000000000ChosenInlineResultHandler ========================= .. autoclass:: telegram.ext.ChosenInlineResultHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.commandhandler.rst000066400000000000000000000001571460724040100260320ustar00rootroot00000000000000CommandHandler ============== .. autoclass:: telegram.ext.CommandHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.contexttypes.rst000066400000000000000000000001511460724040100256210ustar00rootroot00000000000000ContextTypes ============ .. autoclass:: telegram.ext.ContextTypes :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.conversationhandler.rst000066400000000000000000000001761460724040100271270ustar00rootroot00000000000000ConversationHandler =================== .. autoclass:: telegram.ext.ConversationHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.defaults.rst000066400000000000000000000001351460724040100246610ustar00rootroot00000000000000Defaults ======== .. autoclass:: telegram.ext.Defaults :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.dictpersistence.rst000066400000000000000000000001621460724040100262420ustar00rootroot00000000000000DictPersistence =============== .. autoclass:: telegram.ext.DictPersistence :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.extbot.rst000066400000000000000000000002571460724040100243640ustar00rootroot00000000000000ExtBot ====== .. autoclass:: telegram.ext.ExtBot :show-inheritance: :members: insert_callback_data, defaults, rate_limiter, initialize, shutdown, callback_data_cache python-telegram-bot-21.1.1/docs/source/telegram.ext.filters.rst000066400000000000000000000006161460724040100245260ustar00rootroot00000000000000filters Module ============== .. :bysource: since e.g filters.CHAT is much above filters.Chat() in the docs when it shouldn't. The classes in `filters.py` are sorted alphabetically such that :bysource: still is readable .. automodule:: telegram.ext.filters :inherited-members: BaseFilter, MessageFilter, UpdateFilter, object :members: :show-inheritance: :member-order: bysourcepython-telegram-bot-21.1.1/docs/source/telegram.ext.handlers-tree.rst000066400000000000000000000015071460724040100256130ustar00rootroot00000000000000Handlers -------- .. toctree:: :titlesonly: telegram.ext.basehandler telegram.ext.businessconnectionhandler telegram.ext.businessmessagesdeletedhandler telegram.ext.callbackqueryhandler telegram.ext.chatboosthandler telegram.ext.chatjoinrequesthandler telegram.ext.chatmemberhandler telegram.ext.choseninlineresulthandler telegram.ext.commandhandler telegram.ext.conversationhandler telegram.ext.filters telegram.ext.inlinequeryhandler telegram.ext.messagehandler telegram.ext.messagereactionhandler telegram.ext.pollanswerhandler telegram.ext.pollhandler telegram.ext.precheckoutqueryhandler telegram.ext.prefixhandler telegram.ext.shippingqueryhandler telegram.ext.stringcommandhandler telegram.ext.stringregexhandler telegram.ext.typehandler python-telegram-bot-21.1.1/docs/source/telegram.ext.inlinequeryhandler.rst000066400000000000000000000001731460724040100267560ustar00rootroot00000000000000InlineQueryHandler ================== .. autoclass:: telegram.ext.InlineQueryHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.invalidcallbackdata.rst000066400000000000000000000001761460724040100270140ustar00rootroot00000000000000InvalidCallbackData =================== .. autoclass:: telegram.ext.InvalidCallbackData :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.job.rst000066400000000000000000000001161460724040100236230ustar00rootroot00000000000000Job === .. autoclass:: telegram.ext.Job :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.jobqueue.rst000066400000000000000000000001351460724040100246710ustar00rootroot00000000000000JobQueue ======== .. autoclass:: telegram.ext.JobQueue :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.messagehandler.rst000066400000000000000000000001571460724040100260400ustar00rootroot00000000000000MessageHandler ============== .. autoclass:: telegram.ext.MessageHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.messagereactionhandler.rst000066400000000000000000000002071460724040100275610ustar00rootroot00000000000000MessageReactionHandler ====================== .. autoclass:: telegram.ext.MessageReactionHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.persistence-tree.rst000066400000000000000000000002771460724040100263420ustar00rootroot00000000000000Persistence ----------- .. toctree:: :titlesonly: telegram.ext.basepersistence telegram.ext.dictpersistence telegram.ext.persistenceinput telegram.ext.picklepersistence python-telegram-bot-21.1.1/docs/source/telegram.ext.persistenceinput.rst000066400000000000000000000001471460724040100264610ustar00rootroot00000000000000PersistenceInput ================ .. autoclass:: telegram.ext.PersistenceInput :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.picklepersistence.rst000066400000000000000000000001701460724040100265650ustar00rootroot00000000000000PicklePersistence ================= .. autoclass:: telegram.ext.PicklePersistence :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.pollanswerhandler.rst000066400000000000000000000001701460724040100265750ustar00rootroot00000000000000PollAnswerHandler ================= .. autoclass:: telegram.ext.PollAnswerHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.pollhandler.rst000066400000000000000000000001461460724040100253600ustar00rootroot00000000000000PollHandler =========== .. autoclass:: telegram.ext.PollHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.precheckoutqueryhandler.rst000066400000000000000000000002121460724040100300060ustar00rootroot00000000000000PreCheckoutQueryHandler ======================= .. autoclass:: telegram.ext.PreCheckoutQueryHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.prefixhandler.rst000066400000000000000000000001541460724040100257060ustar00rootroot00000000000000PrefixHandler ============= .. autoclass:: telegram.ext.PrefixHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.rate-limiting-tree.rst000066400000000000000000000001741460724040100265570ustar00rootroot00000000000000Rate Limiting ------------- .. toctree:: :titlesonly: telegram.ext.baseratelimiter telegram.ext.aioratelimiterpython-telegram-bot-21.1.1/docs/source/telegram.ext.rst000066400000000000000000000011441460724040100230540ustar00rootroot00000000000000telegram.ext package ==================== .. automodule:: telegram.ext .. toctree:: :titlesonly: telegram.ext.application telegram.ext.applicationbuilder telegram.ext.applicationhandlerstop telegram.ext.baseupdateprocessor telegram.ext.callbackcontext telegram.ext.contexttypes telegram.ext.defaults telegram.ext.extbot telegram.ext.job telegram.ext.jobqueue telegram.ext.simpleupdateprocessor telegram.ext.updater telegram.ext.handlers-tree.rst telegram.ext.persistence-tree.rst telegram.ext.acd-tree.rst telegram.ext.rate-limiting-tree.rstpython-telegram-bot-21.1.1/docs/source/telegram.ext.shippingqueryhandler.rst000066400000000000000000000002011460724040100273110ustar00rootroot00000000000000ShippingQueryHandler ==================== .. autoclass:: telegram.ext.ShippingQueryHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.simpleupdateprocessor.rst000066400000000000000000000002031460724040100275020ustar00rootroot00000000000000SimpleUpdateProcessor ===================== .. autoclass:: telegram.ext.SimpleUpdateProcessor :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.ext.stringcommandhandler.rst000066400000000000000000000002011460724040100272470ustar00rootroot00000000000000StringCommandHandler ==================== .. autoclass:: telegram.ext.StringCommandHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.stringregexhandler.rst000066400000000000000000000001731460724040100267530ustar00rootroot00000000000000StringRegexHandler ================== .. autoclass:: telegram.ext.StringRegexHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.typehandler.rst000066400000000000000000000001461460724040100253730ustar00rootroot00000000000000TypeHandler =========== .. autoclass:: telegram.ext.TypeHandler :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.ext.updater.rst000066400000000000000000000001321460724040100245130ustar00rootroot00000000000000Updater ======= .. autoclass:: telegram.ext.Updater :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.externalreplyinfo.rst000066400000000000000000000001641460724040100260270ustar00rootroot00000000000000ExternalReplyInfo ================= .. autoclass:: telegram.ExternalReplyInfo :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.file.rst000066400000000000000000000001151460724040100231700ustar00rootroot00000000000000File ==== .. autoclass:: telegram.File :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.filecredentials.rst000066400000000000000000000001561460724040100254130ustar00rootroot00000000000000FileCredentials =============== .. autoclass:: telegram.FileCredentials :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.forcereply.rst000066400000000000000000000001371460724040100244270ustar00rootroot00000000000000ForceReply ========== .. autoclass:: telegram.ForceReply :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.forumtopic.rst000066400000000000000000000001371460724040100244440ustar00rootroot00000000000000ForumTopic ========== .. autoclass:: telegram.ForumTopic :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.forumtopicclosed.rst000066400000000000000000000001611460724040100256330ustar00rootroot00000000000000ForumTopicClosed ================ .. autoclass:: telegram.ForumTopicClosed :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.forumtopiccreated.rst000066400000000000000000000001641460724040100257740ustar00rootroot00000000000000ForumTopicCreated ================= .. autoclass:: telegram.ForumTopicCreated :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.forumtopicedited.rst000066400000000000000000000001611460724040100256200ustar00rootroot00000000000000ForumTopicEdited ================ .. autoclass:: telegram.ForumTopicEdited :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.forumtopicreopened.rst000066400000000000000000000001671460724040100261710ustar00rootroot00000000000000ForumTopicReopened ================== .. autoclass:: telegram.ForumTopicReopened :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.game.rst000066400000000000000000000001151460724040100231620ustar00rootroot00000000000000Game ==== .. autoclass:: telegram.Game :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.gamehighscore.rst000066400000000000000000000001501460724040100250550ustar00rootroot00000000000000GameHighScore ============= .. autoclass:: telegram.GameHighScore :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.games-tree.rst000066400000000000000000000001631460724040100243050ustar00rootroot00000000000000Games ----- .. toctree:: :titlesonly: telegram.callbackgame telegram.game telegram.gamehighscore python-telegram-bot-21.1.1/docs/source/telegram.generalforumtopichidden.rst000066400000000000000000000002061460724040100271530ustar00rootroot00000000000000GeneralForumTopicHidden ======================= .. autoclass:: telegram.GeneralForumTopicHidden :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.generalforumtopicunhidden.rst000066400000000000000000000002141460724040100275150ustar00rootroot00000000000000GeneralForumTopicUnhidden ========================= .. autoclass:: telegram.GeneralForumTopicUnhidden :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.giveaway.rst000066400000000000000000000001301460724040100240620ustar00rootroot00000000000000Giveaway ======== .. autoclass:: telegram.Giveaway :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.giveawaycompleted.rst000066400000000000000000000001631460724040100257650ustar00rootroot00000000000000GiveawayCompleted ================= .. autoclass:: telegram.GiveawayCompleted :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.giveawaycreated.rst000066400000000000000000000001551460724040100254210ustar00rootroot00000000000000GiveawayCreated =============== .. autoclass:: telegram.GiveawayCreated :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.giveawaywinners.rst000066400000000000000000000001551460724040100254770ustar00rootroot00000000000000GiveawayWinners =============== .. autoclass:: telegram.GiveawayWinners :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.helpers.rst000066400000000000000000000001671460724040100237220ustar00rootroot00000000000000telegram.helpers Module ======================= .. automodule:: telegram.helpers :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.iddocumentdata.rst000066400000000000000000000001531460724040100252400ustar00rootroot00000000000000IdDocumentData ============== .. autoclass:: telegram.IdDocumentData :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inaccessiblemessage.rst000066400000000000000000000001721460724040100262450ustar00rootroot00000000000000InaccessibleMessage =================== .. autoclass:: telegram.InaccessibleMessage :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inline-tree.rst000066400000000000000000000022531460724040100244710ustar00rootroot00000000000000Inline Mode ----------- .. toctree:: :titlesonly: telegram.choseninlineresult telegram.inlinequery telegram.inlinequeryresult telegram.inlinequeryresultarticle telegram.inlinequeryresultaudio telegram.inlinequeryresultcachedaudio telegram.inlinequeryresultcacheddocument telegram.inlinequeryresultcachedgif telegram.inlinequeryresultcachedmpeg4gif telegram.inlinequeryresultcachedphoto telegram.inlinequeryresultcachedsticker telegram.inlinequeryresultcachedvideo telegram.inlinequeryresultcachedvoice telegram.inlinequeryresultcontact telegram.inlinequeryresultdocument telegram.inlinequeryresultgame telegram.inlinequeryresultgif telegram.inlinequeryresultlocation telegram.inlinequeryresultmpeg4gif telegram.inlinequeryresultphoto telegram.inlinequeryresultsbutton telegram.inlinequeryresultvenue telegram.inlinequeryresultvideo telegram.inlinequeryresultvoice telegram.inputmessagecontent telegram.inputtextmessagecontent telegram.inputlocationmessagecontent telegram.inputvenuemessagecontent telegram.inputcontactmessagecontent telegram.inputinvoicemessagecontent python-telegram-bot-21.1.1/docs/source/telegram.inlinekeyboardbutton.rst000066400000000000000000000001751460724040100265120ustar00rootroot00000000000000InlineKeyboardButton ==================== .. autoclass:: telegram.InlineKeyboardButton :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinekeyboardmarkup.rst000066400000000000000000000001751460724040100264760ustar00rootroot00000000000000InlineKeyboardMarkup ==================== .. autoclass:: telegram.InlineKeyboardMarkup :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequery.rst000066400000000000000000000001421460724040100246150ustar00rootroot00000000000000InlineQuery =========== .. autoclass:: telegram.InlineQuery :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresult.rst000066400000000000000000000001641460724040100260600ustar00rootroot00000000000000InlineQueryResult ================= .. autoclass:: telegram.InlineQueryResult :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultarticle.rst000066400000000000000000000002111460724040100274150ustar00rootroot00000000000000InlineQueryResultArticle ======================== .. autoclass:: telegram.InlineQueryResultArticle :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultaudio.rst000066400000000000000000000002031460724040100270740ustar00rootroot00000000000000InlineQueryResultAudio ====================== .. autoclass:: telegram.InlineQueryResultAudio :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultcachedaudio.rst000066400000000000000000000002251460724040100302300ustar00rootroot00000000000000InlineQueryResultCachedAudio ============================ .. autoclass:: telegram.InlineQueryResultCachedAudio :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultcacheddocument.rst000066400000000000000000000002361460724040100307470ustar00rootroot00000000000000InlineQueryResultCachedDocument =============================== .. autoclass:: telegram.InlineQueryResultCachedDocument :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultcachedgif.rst000066400000000000000000000002171460724040100276750ustar00rootroot00000000000000InlineQueryResultCachedGif ========================== .. autoclass:: telegram.InlineQueryResultCachedGif :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultcachedmpeg4gif.rst000066400000000000000000000002361460724040100306330ustar00rootroot00000000000000InlineQueryResultCachedMpeg4Gif =============================== .. autoclass:: telegram.InlineQueryResultCachedMpeg4Gif :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultcachedphoto.rst000066400000000000000000000002251460724040100302600ustar00rootroot00000000000000InlineQueryResultCachedPhoto ============================ .. autoclass:: telegram.InlineQueryResultCachedPhoto :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultcachedsticker.rst000066400000000000000000000002331460724040100305720ustar00rootroot00000000000000InlineQueryResultCachedSticker ============================== .. autoclass:: telegram.InlineQueryResultCachedSticker :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultcachedvideo.rst000066400000000000000000000002251460724040100302350ustar00rootroot00000000000000InlineQueryResultCachedVideo ============================ .. autoclass:: telegram.InlineQueryResultCachedVideo :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultcachedvoice.rst000066400000000000000000000002251460724040100302340ustar00rootroot00000000000000InlineQueryResultCachedVoice ============================ .. autoclass:: telegram.InlineQueryResultCachedVoice :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultcontact.rst000066400000000000000000000002111460724040100274250ustar00rootroot00000000000000InlineQueryResultContact ======================== .. autoclass:: telegram.InlineQueryResultContact :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultdocument.rst000066400000000000000000000002141460724040100276130ustar00rootroot00000000000000InlineQueryResultDocument ========================= .. autoclass:: telegram.InlineQueryResultDocument :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultgame.rst000066400000000000000000000002001460724040100267010ustar00rootroot00000000000000InlineQueryResultGame ===================== .. autoclass:: telegram.InlineQueryResultGame :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultgif.rst000066400000000000000000000001751460724040100265500ustar00rootroot00000000000000InlineQueryResultGif ==================== .. autoclass:: telegram.InlineQueryResultGif :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultlocation.rst000066400000000000000000000002141460724040100276050ustar00rootroot00000000000000InlineQueryResultLocation ========================= .. autoclass:: telegram.InlineQueryResultLocation :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultmpeg4gif.rst000066400000000000000000000002141460724040100274770ustar00rootroot00000000000000InlineQueryResultMpeg4Gif ========================= .. autoclass:: telegram.InlineQueryResultMpeg4Gif :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultphoto.rst000066400000000000000000000002031460724040100271240ustar00rootroot00000000000000InlineQueryResultPhoto ====================== .. autoclass:: telegram.InlineQueryResultPhoto :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultsbutton.rst000066400000000000000000000002101460724040100274670ustar00rootroot00000000000000InlineQueryResultsButton ======================== .. autoclass:: telegram.InlineQueryResultsButton :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultvenue.rst000066400000000000000000000002031460724040100271150ustar00rootroot00000000000000InlineQueryResultVenue ====================== .. autoclass:: telegram.InlineQueryResultVenue :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultvideo.rst000066400000000000000000000002031460724040100271010ustar00rootroot00000000000000InlineQueryResultVideo ====================== .. autoclass:: telegram.InlineQueryResultVideo :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inlinequeryresultvoice.rst000066400000000000000000000002031460724040100271000ustar00rootroot00000000000000InlineQueryResultVoice ====================== .. autoclass:: telegram.InlineQueryResultVoice :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputcontactmessagecontent.rst000066400000000000000000000002171460724040100277270ustar00rootroot00000000000000InputContactMessageContent ========================== .. autoclass:: telegram.InputContactMessageContent :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputfile.rst000066400000000000000000000001341460724040100242510ustar00rootroot00000000000000InputFile ========= .. autoclass:: telegram.InputFile :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputinvoicemessagecontent.rst000066400000000000000000000002171460724040100277300ustar00rootroot00000000000000InputInvoiceMessageContent ========================== .. autoclass:: telegram.InputInvoiceMessageContent :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputlocationmessagecontent.rst000066400000000000000000000002221460724040100301000ustar00rootroot00000000000000InputLocationMessageContent =========================== .. autoclass:: telegram.InputLocationMessageContent :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputmedia.rst000066400000000000000000000001371460724040100244140ustar00rootroot00000000000000InputMedia ========== .. autoclass:: telegram.InputMedia :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputmediaanimation.rst000066400000000000000000000001721460724040100263130ustar00rootroot00000000000000InputMediaAnimation =================== .. autoclass:: telegram.InputMediaAnimation :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputmediaaudio.rst000066400000000000000000000001561460724040100254370ustar00rootroot00000000000000InputMediaAudio =============== .. autoclass:: telegram.InputMediaAudio :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputmediadocument.rst000066400000000000000000000001671460724040100261560ustar00rootroot00000000000000InputMediaDocument ================== .. autoclass:: telegram.InputMediaDocument :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputmediaphoto.rst000066400000000000000000000001561460724040100254670ustar00rootroot00000000000000InputMediaPhoto =============== .. autoclass:: telegram.InputMediaPhoto :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputmediavideo.rst000066400000000000000000000001561460724040100254440ustar00rootroot00000000000000InputMediaVideo =============== .. autoclass:: telegram.InputMediaVideo :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputmessagecontent.rst000066400000000000000000000001721460724040100263530ustar00rootroot00000000000000InputMessageContent =================== .. autoclass:: telegram.InputMessageContent :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputsticker.rst000066400000000000000000000001451460724040100250000ustar00rootroot00000000000000InputSticker ============ .. autoclass:: telegram.InputSticker :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputtextmessagecontent.rst000066400000000000000000000002061460724040100272560ustar00rootroot00000000000000InputTextMessageContent ======================= .. autoclass:: telegram.InputTextMessageContent :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.inputvenuemessagecontent.rst000066400000000000000000000002111460724040100274100ustar00rootroot00000000000000InputVenueMessageContent ======================== .. autoclass:: telegram.InputVenueMessageContent :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.invoice.rst000066400000000000000000000001261460724040100237070ustar00rootroot00000000000000Invoice ======= .. autoclass:: telegram.Invoice :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.keyboardbutton.rst000066400000000000000000000001531460724040100253070ustar00rootroot00000000000000KeyboardButton ============== .. autoclass:: telegram.KeyboardButton :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.keyboardbuttonpolltype.rst000066400000000000000000000002031460724040100270740ustar00rootroot00000000000000KeyboardButtonPollType ====================== .. autoclass:: telegram.KeyboardButtonPollType :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.keyboardbuttonrequestchat.rst000066400000000000000000000002251460724040100275600ustar00rootroot00000000000000KeyboardButtonRequestChat ================================== .. autoclass:: telegram.KeyboardButtonRequestChat :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.keyboardbuttonrequestusers.rst000066400000000000000000000002171460724040100300030ustar00rootroot00000000000000KeyboardButtonRequestUsers ========================== .. autoclass:: telegram.KeyboardButtonRequestUsers :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.labeledprice.rst000066400000000000000000000001451460724040100246670ustar00rootroot00000000000000LabeledPrice ============ .. autoclass:: telegram.LabeledPrice :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.linkpreviewoptions.rst000066400000000000000000000001671460724040100262330ustar00rootroot00000000000000LinkPreviewOptions ================== .. autoclass:: telegram.LinkPreviewOptions :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.location.rst000066400000000000000000000001311460724040100240570ustar00rootroot00000000000000Location ======== .. autoclass:: telegram.Location :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.loginurl.rst000066400000000000000000000001311460724040100241020ustar00rootroot00000000000000LoginUrl ======== .. autoclass:: telegram.LoginUrl :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.maskposition.rst000066400000000000000000000001451460724040100247740ustar00rootroot00000000000000MaskPosition ============ .. autoclass:: telegram.MaskPosition :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.maybeinaccessiblemessage.rst000066400000000000000000000002111460724040100272550ustar00rootroot00000000000000MaybeInaccessibleMessage ======================== .. autoclass:: telegram.MaybeInaccessibleMessage :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.menubutton.rst000066400000000000000000000001361460724040100244540ustar00rootroot00000000000000MenuButton ========== .. autoclass:: telegram.MenuButton :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.menubuttoncommands.rst000066400000000000000000000001661460724040100262010ustar00rootroot00000000000000MenuButtonCommands ================== .. autoclass:: telegram.MenuButtonCommands :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.menubuttondefault.rst000066400000000000000000000001631460724040100260210ustar00rootroot00000000000000MenuButtonDefault ================= .. autoclass:: telegram.MenuButtonDefault :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.menubuttonwebapp.rst000066400000000000000000000001601460724040100256500ustar00rootroot00000000000000MenuButtonWebApp ================ .. autoclass:: telegram.MenuButtonWebApp :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.message.rst000066400000000000000000000001261460724040100236770ustar00rootroot00000000000000Message ======= .. autoclass:: telegram.Message :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.messageautodeletetimerchanged.rst000066400000000000000000000002301460724040100303220ustar00rootroot00000000000000MessageAutoDeleteTimerChanged ============================= .. autoclass:: telegram.MessageAutoDeleteTimerChanged :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.messageentity.rst000066400000000000000000000001501460724040100251310ustar00rootroot00000000000000MessageEntity ============= .. autoclass:: telegram.MessageEntity :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.messageid.rst000066400000000000000000000001341460724040100242130ustar00rootroot00000000000000MessageId ========= .. autoclass:: telegram.MessageId :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.messageorigin.rst000066400000000000000000000001501460724040100251040ustar00rootroot00000000000000MessageOrigin ============= .. autoclass:: telegram.MessageOrigin :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.messageoriginchannel.rst000066400000000000000000000001751460724040100264440ustar00rootroot00000000000000MessageOriginChannel ==================== .. autoclass:: telegram.MessageOriginChannel :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.messageoriginchat.rst000066400000000000000000000001641460724040100257510ustar00rootroot00000000000000MessageOriginChat ================= .. autoclass:: telegram.MessageOriginChat :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.messageoriginhiddenuser.rst000066400000000000000000000002071460724040100271620ustar00rootroot00000000000000MessageOriginHiddenUser ======================= .. autoclass:: telegram.MessageOriginHiddenUser :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.messageoriginuser.rst000066400000000000000000000001641460724040100260100ustar00rootroot00000000000000MessageOriginUser ================= .. autoclass:: telegram.MessageOriginUser :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.messagereactioncountupdated.rst000066400000000000000000000002221460724040100300410ustar00rootroot00000000000000MessageReactionCountUpdated =========================== .. autoclass:: telegram.MessageReactionCountUpdated :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.messagereactionupdated.rst000066400000000000000000000002031460724040100267670ustar00rootroot00000000000000MessageReactionUpdated ====================== .. autoclass:: telegram.MessageReactionUpdated :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.orderinfo.rst000066400000000000000000000001341460724040100242410ustar00rootroot00000000000000OrderInfo ========= .. autoclass:: telegram.OrderInfo :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passport-tree.rst000066400000000000000000000014651460724040100250720ustar00rootroot00000000000000Passport -------- .. toctree:: :titlesonly: telegram.credentials telegram.datacredentials telegram.encryptedcredentials telegram.encryptedpassportelement telegram.filecredentials telegram.iddocumentdata telegram.passportdata telegram.passportelementerror telegram.passportelementerrordatafield telegram.passportelementerrorfile telegram.passportelementerrorfiles telegram.passportelementerrorfrontside telegram.passportelementerrorreverseside telegram.passportelementerrorselfie telegram.passportelementerrortranslationfile telegram.passportelementerrortranslationfiles telegram.passportelementerrorunspecified telegram.passportfile telegram.personaldetails telegram.residentialaddress telegram.securedata telegram.securevalue python-telegram-bot-21.1.1/docs/source/telegram.passportdata.rst000066400000000000000000000001451460724040100247610ustar00rootroot00000000000000PassportData ============ .. autoclass:: telegram.PassportData :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passportelementerror.rst000066400000000000000000000001751460724040100265560ustar00rootroot00000000000000PassportElementError ==================== .. autoclass:: telegram.PassportElementError :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passportelementerrordatafield.rst000066400000000000000000000002301460724040100304040ustar00rootroot00000000000000PassportElementErrorDataField ============================= .. autoclass:: telegram.PassportElementErrorDataField :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passportelementerrorfile.rst000066400000000000000000000002111460724040100274050ustar00rootroot00000000000000PassportElementErrorFile ======================== .. autoclass:: telegram.PassportElementErrorFile :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passportelementerrorfiles.rst000066400000000000000000000002141460724040100275730ustar00rootroot00000000000000PassportElementErrorFiles ========================= .. autoclass:: telegram.PassportElementErrorFiles :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passportelementerrorfrontside.rst000066400000000000000000000002301460724040100304640ustar00rootroot00000000000000PassportElementErrorFrontSide ============================= .. autoclass:: telegram.PassportElementErrorFrontSide :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passportelementerrorreverseside.rst000066400000000000000000000002361460724040100310150ustar00rootroot00000000000000PassportElementErrorReverseSide =============================== .. autoclass:: telegram.PassportElementErrorReverseSide :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passportelementerrorselfie.rst000066400000000000000000000002171460724040100277430ustar00rootroot00000000000000PassportElementErrorSelfie ========================== .. autoclass:: telegram.PassportElementErrorSelfie :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passportelementerrortranslationfile.rst000066400000000000000000000002521460724040100316710ustar00rootroot00000000000000PassportElementErrorTranslationFile =================================== .. autoclass:: telegram.PassportElementErrorTranslationFile :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passportelementerrortranslationfiles.rst000066400000000000000000000002551460724040100320570ustar00rootroot00000000000000PassportElementErrorTranslationFiles ==================================== .. autoclass:: telegram.PassportElementErrorTranslationFiles :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passportelementerrorunspecified.rst000066400000000000000000000002361460724040100307730ustar00rootroot00000000000000PassportElementErrorUnspecified =============================== .. autoclass:: telegram.PassportElementErrorUnspecified :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.passportfile.rst000066400000000000000000000001451460724040100247670ustar00rootroot00000000000000PassportFile ============ .. autoclass:: telegram.PassportFile :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.payments-tree.rst000066400000000000000000000004111460724040100250450ustar00rootroot00000000000000Payments -------- .. toctree:: :titlesonly: telegram.invoice telegram.labeledprice telegram.orderinfo telegram.precheckoutquery telegram.shippingaddress telegram.shippingoption telegram.shippingquery telegram.successfulpayment python-telegram-bot-21.1.1/docs/source/telegram.personaldetails.rst000066400000000000000000000001561460724040100254470ustar00rootroot00000000000000PersonalDetails =============== .. autoclass:: telegram.PersonalDetails :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.photosize.rst000066400000000000000000000003231460724040100242760ustar00rootroot00000000000000PhotoSize ========= .. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject .. autoclass:: telegram.PhotoSize :members: :show-inheritance: :inherited-members: TelegramObject python-telegram-bot-21.1.1/docs/source/telegram.poll.rst000066400000000000000000000001151460724040100232170ustar00rootroot00000000000000Poll ==== .. autoclass:: telegram.Poll :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.pollanswer.rst000066400000000000000000000001371460724040100244430ustar00rootroot00000000000000PollAnswer ========== .. autoclass:: telegram.PollAnswer :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.polloption.rst000066400000000000000000000001371460724040100244540ustar00rootroot00000000000000PollOption ========== .. autoclass:: telegram.PollOption :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.precheckoutquery.rst000066400000000000000000000001611460724040100256540ustar00rootroot00000000000000PreCheckoutQuery ================ .. autoclass:: telegram.PreCheckoutQuery :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.proximityalerttriggered.rst000066400000000000000000000002061460724040100272430ustar00rootroot00000000000000ProximityAlertTriggered ======================= .. autoclass:: telegram.ProximityAlertTriggered :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.reactioncount.rst000066400000000000000000000001501460724040100251250ustar00rootroot00000000000000ReactionCount ============= .. autoclass:: telegram.ReactionCount :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.reactiontype.rst000066400000000000000000000001451460724040100247620ustar00rootroot00000000000000ReactionType ============ .. autoclass:: telegram.ReactionType :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.reactiontypecustomemoji.rst000066400000000000000000000002061460724040100272370ustar00rootroot00000000000000ReactionTypeCustomEmoji ======================= .. autoclass:: telegram.ReactionTypeCustomEmoji :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.reactiontypeemoji.rst000066400000000000000000000001641460724040100260070ustar00rootroot00000000000000ReactionTypeEmoji ================= .. autoclass:: telegram.ReactionTypeEmoji :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.replykeyboardmarkup.rst000066400000000000000000000001721460724040100263500ustar00rootroot00000000000000ReplyKeyboardMarkup =================== .. autoclass:: telegram.ReplyKeyboardMarkup :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.replykeyboardremove.rst000066400000000000000000000001721460724040100263460ustar00rootroot00000000000000ReplyKeyboardRemove =================== .. autoclass:: telegram.ReplyKeyboardRemove :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.replyparameters.rst000066400000000000000000000001561460724040100254750ustar00rootroot00000000000000ReplyParameters =============== .. autoclass:: telegram.ReplyParameters :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.request.baserequest.rst000066400000000000000000000001511460724040100262630ustar00rootroot00000000000000BaseRequest =========== .. autoclass:: telegram.request.BaseRequest :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.request.httpxrequest.rst000066400000000000000000000001541460724040100265230ustar00rootroot00000000000000HTTPXRequest ============ .. autoclass:: telegram.request.HTTPXRequest :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.request.requestdata.rst000066400000000000000000000001511460724040100262620ustar00rootroot00000000000000RequestData =========== .. autoclass:: telegram.request.RequestData :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.request.rst000066400000000000000000000003141460724040100237420ustar00rootroot00000000000000telegram.request Module ======================= .. versionadded:: 20.0 .. toctree:: :titlesonly: telegram.request.baserequest telegram.request.requestdata telegram.request.httpxrequest python-telegram-bot-21.1.1/docs/source/telegram.residentialaddress.rst000066400000000000000000000001671460724040100261310ustar00rootroot00000000000000ResidentialAddress ================== .. autoclass:: telegram.ResidentialAddress :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.rst000066400000000000000000000007151460724040100222600ustar00rootroot00000000000000telegram package ================ Version Constants ----------------- .. automodule:: telegram :members: __version__, __version_info__, __bot_api_version__, __bot_api_version_info__ Classes in this package ----------------------- .. toctree:: :titlesonly: telegram.bot telegram.at-tree.rst telegram.stickers-tree.rst telegram.inline-tree.rst telegram.payments-tree.rst telegram.games-tree.rst telegram.passport-tree.rst python-telegram-bot-21.1.1/docs/source/telegram.securedata.rst000066400000000000000000000001371460724040100243750ustar00rootroot00000000000000SecureData ========== .. autoclass:: telegram.SecureData :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.securevalue.rst000066400000000000000000000001421460724040100245740ustar00rootroot00000000000000SecureValue =========== .. autoclass:: telegram.SecureValue :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.sentwebappmessage.rst000066400000000000000000000001631460724040100257710ustar00rootroot00000000000000SentWebAppMessage ================= .. autoclass:: telegram.SentWebAppMessage :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.shareduser.rst000066400000000000000000000001401460724040100244140ustar00rootroot00000000000000SharedUser ========== .. autoclass:: telegram.SharedUser :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.shippingaddress.rst000066400000000000000000000001561460724040100254450ustar00rootroot00000000000000ShippingAddress =============== .. autoclass:: telegram.ShippingAddress :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.shippingoption.rst000066400000000000000000000001531460724040100253250ustar00rootroot00000000000000ShippingOption ============== .. autoclass:: telegram.ShippingOption :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.shippingquery.rst000066400000000000000000000001501460724040100251570ustar00rootroot00000000000000ShippingQuery ============= .. autoclass:: telegram.ShippingQuery :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.sticker.rst000066400000000000000000000003161460724040100237200ustar00rootroot00000000000000Sticker ======= .. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject .. autoclass:: telegram.Sticker :members: :show-inheritance: :inherited-members: TelegramObject python-telegram-bot-21.1.1/docs/source/telegram.stickers-tree.rst000066400000000000000000000001711460724040100250370ustar00rootroot00000000000000Stickers -------- .. toctree:: :titlesonly: telegram.maskposition telegram.sticker telegram.stickerset python-telegram-bot-21.1.1/docs/source/telegram.stickerset.rst000066400000000000000000000001371460724040100244350ustar00rootroot00000000000000StickerSet ========== .. autoclass:: telegram.StickerSet :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.story.rst000066400000000000000000000001201460724040100234250ustar00rootroot00000000000000Story ===== .. autoclass:: telegram.Story :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.successfulpayment.rst000066400000000000000000000001641460724040100260320ustar00rootroot00000000000000SuccessfulPayment ================= .. autoclass:: telegram.SuccessfulPayment :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.switchinlinequerychosenchat.rst000066400000000000000000000002221460724040100300760ustar00rootroot00000000000000SwitchInlineQueryChosenChat =========================== .. autoclass:: telegram.SwitchInlineQueryChosenChat :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.telegramobject.rst000066400000000000000000000001531460724040100252420ustar00rootroot00000000000000TelegramObject ============== .. autoclass:: telegram.TelegramObject :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.textquote.rst000066400000000000000000000001341460724040100243140ustar00rootroot00000000000000TextQuote ========= .. autoclass:: telegram.TextQuote :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.update.rst000066400000000000000000000001231460724040100235320ustar00rootroot00000000000000Update ====== .. autoclass:: telegram.Update :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.user.rst000066400000000000000000000001151460724040100232270ustar00rootroot00000000000000User ==== .. autoclass:: telegram.User :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.userchatboosts.rst000066400000000000000000000002021460724040100253160ustar00rootroot00000000000000UserChatBoosts ============== .. versionadded:: 20.8 .. autoclass:: telegram.UserChatBoosts :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.userprofilephotos.rst000066400000000000000000000001641460724040100260510ustar00rootroot00000000000000UserProfilePhotos ================= .. autoclass:: telegram.UserProfilePhotos :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.usersshared.rst000066400000000000000000000001421460724040100246010ustar00rootroot00000000000000UsersShared =========== .. autoclass:: telegram.UsersShared :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.venue.rst000066400000000000000000000001201460724040100233670ustar00rootroot00000000000000Venue ===== .. autoclass:: telegram.Venue :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.video.rst000066400000000000000000000003071460724040100233620ustar00rootroot00000000000000Video ===== .. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject .. autoclass:: telegram.Video :members: :show-inheritance: :inherited-members: TelegramObjectpython-telegram-bot-21.1.1/docs/source/telegram.videochatended.rst000066400000000000000000000001541460724040100252220ustar00rootroot00000000000000VideoChatEnded ============== .. autoclass:: telegram.VideoChatEnded :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.videochatparticipantsinvited.rst000066400000000000000000000002261460724040100302270ustar00rootroot00000000000000VideoChatParticipantsInvited ============================ .. autoclass:: telegram.VideoChatParticipantsInvited :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.videochatscheduled.rst000066400000000000000000000001671460724040100261070ustar00rootroot00000000000000VideoChatScheduled ================== .. autoclass:: telegram.VideoChatScheduled :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.videochatstarted.rst000066400000000000000000000001621460724040100256100ustar00rootroot00000000000000VideoChatStarted ================ .. autoclass:: telegram.VideoChatStarted :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.videonote.rst000066400000000000000000000003231460724040100242460ustar00rootroot00000000000000VideoNote ========= .. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject .. autoclass:: telegram.VideoNote :members: :show-inheritance: :inherited-members: TelegramObjectpython-telegram-bot-21.1.1/docs/source/telegram.voice.rst000066400000000000000000000003101460724040100233530ustar00rootroot00000000000000Voice ===== .. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject .. autoclass:: telegram.Voice :members: :show-inheritance: :inherited-members: TelegramObject python-telegram-bot-21.1.1/docs/source/telegram.warnings.rst000066400000000000000000000001721460724040100241040ustar00rootroot00000000000000telegram.warnings Module ======================== .. automodule:: telegram.warnings :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.webappdata.rst000066400000000000000000000001361460724040100243640ustar00rootroot00000000000000WebAppData ========== .. autoclass:: telegram.WebAppData :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.webappinfo.rst000066400000000000000000000001361460724040100244060ustar00rootroot00000000000000WebAppInfo ========== .. autoclass:: telegram.WebAppInfo :members: :show-inheritance:python-telegram-bot-21.1.1/docs/source/telegram.webhookinfo.rst000066400000000000000000000001421460724040100245630ustar00rootroot00000000000000WebhookInfo =========== .. autoclass:: telegram.WebhookInfo :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram.writeaccessallowed.rst000066400000000000000000000001671460724040100261440ustar00rootroot00000000000000WriteAccessAllowed ================== .. autoclass:: telegram.WriteAccessAllowed :members: :show-inheritance: python-telegram-bot-21.1.1/docs/source/telegram_auxil.rst000066400000000000000000000002561460724040100234620ustar00rootroot00000000000000Auxiliary modules ================= .. toctree:: :titlesonly: telegram.constants telegram.error telegram.helpers telegram.request telegram.warnings python-telegram-bot-21.1.1/docs/source/testing.rst000066400000000000000000000000431460724040100221270ustar00rootroot00000000000000.. include:: ../../tests/README.rstpython-telegram-bot-21.1.1/docs/substitutions/000077500000000000000000000000001460724040100213625ustar00rootroot00000000000000python-telegram-bot-21.1.1/docs/substitutions/global.rst000066400000000000000000000172401460724040100233600ustar00rootroot00000000000000.. |uploadinput| replace:: To upload a file, you can either pass a :term:`file object` (e.g. ``open("filename", "rb")``), the file contents as bytes or the path of the file (as string or :class:`pathlib.Path` object). In the latter case, the file contents will either be read as bytes or the file path will be passed to Telegram, depending on the :paramref:`~telegram.Bot.local_mode` setting. .. |uploadinputnopath| replace:: To upload a file, you can either pass a :term:`file object` (e.g. ``open("filename", "rb")``) or the file contents as bytes. If the bot is running in :paramref:`~telegram.Bot.local_mode`, passing the path of the file (as string or :class:`pathlib.Path` object) is supported as well. .. |fileinputbase| replace:: Pass a ``file_id`` as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one. .. |fileinput| replace:: |fileinputbase| |uploadinput| .. |fileinputnopath| replace:: |fileinputbase| |uploadinputnopath| .. |thumbdocstringbase| replace:: Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file. .. |thumbdocstring| replace:: |thumbdocstringbase| |uploadinput| .. |thumbdocstringnopath| replace:: |thumbdocstringbase| |uploadinputnopath| .. |editreplymarkup| replace:: It is currently only possible to edit messages without :attr:`telegram.Message.reply_markup` or with inline keyboards. .. |toapikwargsbase| replace:: These arguments are also considered by :meth:`~telegram.TelegramObject.to_dict` and :meth:`~telegram.TelegramObject.to_json`, i.e. when passing objects to Telegram. Passing them to Telegram is however not guaranteed to work for all kinds of objects, e.g. this will fail for objects that can not directly be JSON serialized. .. |toapikwargsarg| replace:: Arbitrary keyword arguments. Can be used to store data for which there are no dedicated attributes. |toapikwargsbase| .. |toapikwargsattr| replace:: Optional. Arbitrary keyword arguments. Used to store data for which there are no dedicated attributes. |toapikwargsbase| .. |chat_id_channel| replace:: Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). .. |chat_id_group| replace:: Unique identifier for the target chat or username of the target supergroup (in the format ``@supergroupusername``). .. |message_thread_id| replace:: Unique identifier for the target message thread of the forum topic. .. |message_thread_id_arg| replace:: Unique identifier for the target message thread (topic) of the forum; for forum supergroups only. .. |parse_mode| replace:: Mode for parsing entities. See :class:`telegram.constants.ParseMode` and `formatting options `__ for more details. .. |allow_sending_without_reply| replace:: Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. .. |caption_entities| replace:: Sequence of special entities that appear in the caption, which can be specified instead of ``parse_mode``. .. |protect_content| replace:: Protects the contents of the sent message from forwarding and saving. .. |disable_notification| replace:: Sends the message silently. Users will receive a notification with no sound. .. |reply_to_msg_id| replace:: If the message is a reply, ID of the original message. .. |sequenceclassargs| replace:: |sequenceargs| The input is converted to a tuple. .. |tupleclassattrs| replace:: This attribute is now an immutable tuple. .. |alwaystuple| replace:: This attribute is now always a tuple, that may be empty. .. |sequenceargs| replace:: Accepts any :class:`collections.abc.Sequence` as input instead of just a list. .. |captionentitiesattr| replace:: Tuple of special entities that appear in the caption, which can be specified instead of ``parse_mode``. .. |datetime_localization| replace:: The default timezone of the bot is used for localization, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used. .. |post_methods_note| replace:: If you implement custom logic that implies that you will **not** be using :class:`~telegram.ext.Application`'s methods :meth:`~telegram.ext.Application.run_polling` or :meth:`~telegram.ext.Application.run_webhook` to run your application (like it's done in `Custom Webhook Bot Example `__), the callback you set in this method **will not be called automatically**. So instead of setting a callback with this method, you have to explicitly ``await`` the function that you want to run at this stage of your application's life (in the `example mentioned above `__, that would be in ``async with application`` context manager). .. |removed_thumb_note| replace:: Removed the deprecated argument and attribute ``thumb``. .. |removed_thumb_url_note| replace:: Removed the deprecated argument and attribute ``thumb_url``. .. |removed_thumb_wildcard_note| replace:: Removed the deprecated arguments and attributes ``thumb_*``. .. |async_context_manager| replace:: Asynchronous context manager which .. |reply_parameters| replace:: Description of the message to reply to. .. |rtm_aswr_deprecated| replace:: replacing this argument. PTB will automatically convert this argument to that one, but you should update your code to use the new argument. .. |keyword_only_arg| replace:: This argument is now a keyword-only argument. .. |text_html| replace:: The return value of this property is a best-effort approach. Unfortunately, it can not be guaranteed that sending a message with the returned string will render in the same way as the original message produces the same :attr:`~telegram.Message.entities`/:attr:`~telegram.Message.caption_entities` as the original message. For example, Telegram recommends that entities of type :attr:`~telegram.MessageEntity.BLOCKQUOTE` and :attr:`~telegram.MessageEntity.PRE` *should* start and end on a new line, but does not enforce this and leaves rendering decisions up to the clients. .. |text_markdown| replace:: |text_html| Moreover, markdown formatting is inherently less expressive than HTML, so some edge cases may not be coverable at all. For example, markdown formatting can not specify two consecutive block quotes without a blank line in between, but HTML can. .. |reply_quote| replace:: If set to :obj:`True`, the reply is sent as an actual reply to this message. If ``reply_to_message_id`` is passed, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. .. |do_quote| replace:: If set to :obj:`True`, the replied message is quoted. For a dict, it must be the output of :meth:`~telegram.Message.build_reply_arguments` to specify exact ``reply_parameters``. If ``reply_to_message_id`` or ``reply_parameters`` are passed, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. .. |non_optional_story_argument| replace:: As of this version, this argument is now required. In accordance with our `stability policy `__, the signature will be kept as optional for now, though they are mandatory and an error will be raised if you don't pass it. .. |business_id_str| replace:: Unique identifier of the business connection on behalf of which the message will be sent. python-telegram-bot-21.1.1/examples/000077500000000000000000000000001460724040100173115ustar00rootroot00000000000000python-telegram-bot-21.1.1/examples/LICENSE.txt000066400000000000000000000146331460724040100211430ustar00rootroot00000000000000CC0 1.0 Universal Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. For more information, please see python-telegram-bot-21.1.1/examples/README.md000066400000000000000000000006561460724040100205770ustar00rootroot00000000000000# Examples A description of the examples in this directory can be found in the [documentation](https://docs.python-telegram-bot.org/examples.html). All examples are licensed under the [CC0 License](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/LICENSE.txt) and are therefore fully dedicated to the public domain. You can use them as the base for your own bots without worrying about copyrights.python-telegram-bot-21.1.1/examples/arbitrarycallbackdatabot.py000066400000000000000000000104451460724040100247020ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """This example showcases how PTBs "arbitrary callback data" feature can be used. For detailed info on arbitrary callback data, see the wiki page at https://github.com/python-telegram-bot/python-telegram-bot/wiki/Arbitrary-callback_data Note: To use arbitrary callback data, you must install PTB via `pip install "python-telegram-bot[callback-data]"` """ import logging from typing import List, Tuple, cast from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ( Application, CallbackQueryHandler, CommandHandler, ContextTypes, InvalidCallbackData, PicklePersistence, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Sends a message with 5 inline buttons attached.""" number_list: List[int] = [] await update.message.reply_text("Please choose:", reply_markup=build_keyboard(number_list)) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Displays info on how to use the bot.""" await update.message.reply_text( "Use /start to test this bot. Use /clear to clear the stored data so that you can see " "what happens, if the button data is not available. " ) async def clear(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Clears the callback data cache""" context.bot.callback_data_cache.clear_callback_data() context.bot.callback_data_cache.clear_callback_queries() await update.effective_message.reply_text("All clear!") def build_keyboard(current_list: List[int]) -> InlineKeyboardMarkup: """Helper function to build the next inline keyboard.""" return InlineKeyboardMarkup.from_column( [InlineKeyboardButton(str(i), callback_data=(i, current_list)) for i in range(1, 6)] ) async def list_button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Parses the CallbackQuery and updates the message text.""" query = update.callback_query await query.answer() # Get the data from the callback_data. # If you're using a type checker like MyPy, you'll have to use typing.cast # to make the checker get the expected type of the callback_data number, number_list = cast(Tuple[int, List[int]], query.data) # append the number to the list number_list.append(number) await query.edit_message_text( text=f"So far you've selected {number_list}. Choose the next item:", reply_markup=build_keyboard(number_list), ) # we can delete the data stored for the query, because we've replaced the buttons context.drop_callback_data(query) async def handle_invalid_button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Informs the user that the button is no longer available.""" await update.callback_query.answer() await update.effective_message.edit_text( "Sorry, I could not process this button click 😕 Please send /start to get a new keyboard." ) def main() -> None: """Run the bot.""" # We use persistence to demonstrate how buttons can still work after the bot was restarted persistence = PicklePersistence(filepath="arbitrarycallbackdatabot") # Create the Application and pass it your bot's token. application = ( Application.builder() .token("TOKEN") .persistence(persistence) .arbitrary_callback_data(True) .build() ) application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("help", help_command)) application.add_handler(CommandHandler("clear", clear)) application.add_handler( CallbackQueryHandler(handle_invalid_button, pattern=InvalidCallbackData) ) application.add_handler(CallbackQueryHandler(list_button)) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/chatmemberbot.py000066400000000000000000000162351460724040100225060ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ Simple Bot to handle '(my_)chat_member' updates. Greets new users & keeps track of which chats the bot is in. Usage: Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import logging from typing import Optional, Tuple from telegram import Chat, ChatMember, ChatMemberUpdated, Update from telegram.constants import ParseMode from telegram.ext import ( Application, ChatMemberHandler, CommandHandler, ContextTypes, MessageHandler, filters, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) def extract_status_change(chat_member_update: ChatMemberUpdated) -> Optional[Tuple[bool, bool]]: """Takes a ChatMemberUpdated instance and extracts whether the 'old_chat_member' was a member of the chat and whether the 'new_chat_member' is a member of the chat. Returns None, if the status didn't change. """ status_change = chat_member_update.difference().get("status") old_is_member, new_is_member = chat_member_update.difference().get("is_member", (None, None)) if status_change is None: return None old_status, new_status = status_change was_member = old_status in [ ChatMember.MEMBER, ChatMember.OWNER, ChatMember.ADMINISTRATOR, ] or (old_status == ChatMember.RESTRICTED and old_is_member is True) is_member = new_status in [ ChatMember.MEMBER, ChatMember.OWNER, ChatMember.ADMINISTRATOR, ] or (new_status == ChatMember.RESTRICTED and new_is_member is True) return was_member, is_member async def track_chats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Tracks the chats the bot is in.""" result = extract_status_change(update.my_chat_member) if result is None: return was_member, is_member = result # Let's check who is responsible for the change cause_name = update.effective_user.full_name # Handle chat types differently: chat = update.effective_chat if chat.type == Chat.PRIVATE: if not was_member and is_member: # This may not be really needed in practice because most clients will automatically # send a /start command after the user unblocks the bot, and start_private_chat() # will add the user to "user_ids". # We're including this here for the sake of the example. logger.info("%s unblocked the bot", cause_name) context.bot_data.setdefault("user_ids", set()).add(chat.id) elif was_member and not is_member: logger.info("%s blocked the bot", cause_name) context.bot_data.setdefault("user_ids", set()).discard(chat.id) elif chat.type in [Chat.GROUP, Chat.SUPERGROUP]: if not was_member and is_member: logger.info("%s added the bot to the group %s", cause_name, chat.title) context.bot_data.setdefault("group_ids", set()).add(chat.id) elif was_member and not is_member: logger.info("%s removed the bot from the group %s", cause_name, chat.title) context.bot_data.setdefault("group_ids", set()).discard(chat.id) elif not was_member and is_member: logger.info("%s added the bot to the channel %s", cause_name, chat.title) context.bot_data.setdefault("channel_ids", set()).add(chat.id) elif was_member and not is_member: logger.info("%s removed the bot from the channel %s", cause_name, chat.title) context.bot_data.setdefault("channel_ids", set()).discard(chat.id) async def show_chats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Shows which chats the bot is in""" user_ids = ", ".join(str(uid) for uid in context.bot_data.setdefault("user_ids", set())) group_ids = ", ".join(str(gid) for gid in context.bot_data.setdefault("group_ids", set())) channel_ids = ", ".join(str(cid) for cid in context.bot_data.setdefault("channel_ids", set())) text = ( f"@{context.bot.username} is currently in a conversation with the user IDs {user_ids}." f" Moreover it is a member of the groups with IDs {group_ids} " f"and administrator in the channels with IDs {channel_ids}." ) await update.effective_message.reply_text(text) async def greet_chat_members(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Greets new users in chats and announces when someone leaves""" result = extract_status_change(update.chat_member) if result is None: return was_member, is_member = result cause_name = update.chat_member.from_user.mention_html() member_name = update.chat_member.new_chat_member.user.mention_html() if not was_member and is_member: await update.effective_chat.send_message( f"{member_name} was added by {cause_name}. Welcome!", parse_mode=ParseMode.HTML, ) elif was_member and not is_member: await update.effective_chat.send_message( f"{member_name} is no longer with us. Thanks a lot, {cause_name} ...", parse_mode=ParseMode.HTML, ) async def start_private_chat(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Greets the user and records that they started a chat with the bot if it's a private chat. Since no `my_chat_member` update is issued when a user starts a private chat with the bot for the first time, we have to track it explicitly here. """ user_name = update.effective_user.full_name chat = update.effective_chat if chat.type != Chat.PRIVATE or chat.id in context.bot_data.get("user_ids", set()): return logger.info("%s started a private chat with the bot", user_name) context.bot_data.setdefault("user_ids", set()).add(chat.id) await update.effective_message.reply_text( f"Welcome {user_name}. Use /show_chats to see what chats I'm in." ) def main() -> None: """Start the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() # Keep track of which chats the bot is in application.add_handler(ChatMemberHandler(track_chats, ChatMemberHandler.MY_CHAT_MEMBER)) application.add_handler(CommandHandler("show_chats", show_chats)) # Handle members joining/leaving chats. application.add_handler(ChatMemberHandler(greet_chat_members, ChatMemberHandler.CHAT_MEMBER)) # Interpret any other command or text message as a start of a private chat. # This will record the user as being in a private chat with bot. application.add_handler(MessageHandler(filters.ALL, start_private_chat)) # Run the bot until the user presses Ctrl-C # We pass 'allowed_updates' handle *all* updates including `chat_member` updates # To reset this, simply pass `allowed_updates=[]` application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/contexttypesbot.py000066400000000000000000000114521460724040100231440ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ Simple Bot to showcase `telegram.ext.ContextTypes`. Usage: Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import logging from collections import defaultdict from typing import DefaultDict, Optional, Set from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.constants import ParseMode from telegram.ext import ( Application, CallbackContext, CallbackQueryHandler, CommandHandler, ContextTypes, ExtBot, TypeHandler, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) class ChatData: """Custom class for chat_data. Here we store data per message.""" def __init__(self) -> None: self.clicks_per_message: DefaultDict[int, int] = defaultdict(int) # The [ExtBot, dict, ChatData, dict] is for type checkers like mypy class CustomContext(CallbackContext[ExtBot, dict, ChatData, dict]): """Custom class for context.""" def __init__( self, application: Application, chat_id: Optional[int] = None, user_id: Optional[int] = None, ): super().__init__(application=application, chat_id=chat_id, user_id=user_id) self._message_id: Optional[int] = None @property def bot_user_ids(self) -> Set[int]: """Custom shortcut to access a value stored in the bot_data dict""" return self.bot_data.setdefault("user_ids", set()) @property def message_clicks(self) -> Optional[int]: """Access the number of clicks for the message this context object was built for.""" if self._message_id: return self.chat_data.clicks_per_message[self._message_id] return None @message_clicks.setter def message_clicks(self, value: int) -> None: """Allow to change the count""" if not self._message_id: raise RuntimeError("There is no message associated with this context object.") self.chat_data.clicks_per_message[self._message_id] = value @classmethod def from_update(cls, update: object, application: "Application") -> "CustomContext": """Override from_update to set _message_id.""" # Make sure to call super() context = super().from_update(update, application) if context.chat_data and isinstance(update, Update) and update.effective_message: # pylint: disable=protected-access context._message_id = update.effective_message.message_id # Remember to return the object return context async def start(update: Update, context: CustomContext) -> None: """Display a message with a button.""" await update.message.reply_html( "This button was clicked 0 times.", reply_markup=InlineKeyboardMarkup.from_button( InlineKeyboardButton(text="Click me!", callback_data="button") ), ) async def count_click(update: Update, context: CustomContext) -> None: """Update the click count for the message.""" context.message_clicks += 1 await update.callback_query.answer() await update.effective_message.edit_text( f"This button was clicked {context.message_clicks} times.", reply_markup=InlineKeyboardMarkup.from_button( InlineKeyboardButton(text="Click me!", callback_data="button") ), parse_mode=ParseMode.HTML, ) async def print_users(update: Update, context: CustomContext) -> None: """Show which users have been using this bot.""" await update.message.reply_text( f"The following user IDs have used this bot: {', '.join(map(str, context.bot_user_ids))}" ) async def track_users(update: Update, context: CustomContext) -> None: """Store the user id of the incoming update, if any.""" if update.effective_user: context.bot_user_ids.add(update.effective_user.id) def main() -> None: """Run the bot.""" context_types = ContextTypes(context=CustomContext, chat_data=ChatData) application = Application.builder().token("TOKEN").context_types(context_types).build() # run track_users in its own group to not interfere with the user handlers application.add_handler(TypeHandler(Update, track_users), group=-1) application.add_handler(CommandHandler("start", start)) application.add_handler(CallbackQueryHandler(count_click)) application.add_handler(CommandHandler("print_users", print_users)) application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/conversationbot.mmd000066400000000000000000000021251460724040100232270ustar00rootroot00000000000000flowchart TB %% Documentation: https://mermaid-js.github.io/mermaid/#/flowchart A(("/start")):::entryPoint -->|Hi! My name is Professor Bot...| B((GENDER)):::state B --> |"- Boy
- Girl
- Other"|C("(choice)"):::userInput C --> |I see! Please send me a photo...| D((PHOTO)):::state D --> E("/skip"):::userInput D --> F("(photo)"):::userInput E --> |I bet you look great!| G[\ /]:::userInput F --> |Gorgeous!| G[\ /] G --> |"Now, send me your location .."| H((LOCATION)):::state H --> I("/skip"):::userInput H --> J("(location)"):::userInput I --> |You seem a bit paranoid!| K[\" "/]:::userInput J --> |Maybe I can visit...| K K --> |"Tell me about yourself..."| L(("BIO")):::state L --> M("(text)"):::userInput M --> |"Thanks and bye!"| End(("END")):::termination classDef userInput fill:#2a5279, color:#ffffff, stroke:#ffffff classDef state fill:#222222, color:#ffffff, stroke:#ffffff classDef entryPoint fill:#009c11, stroke:#42FF57, color:#ffffff classDef termination fill:#bb0007, stroke:#E60109, color:#ffffff python-telegram-bot-21.1.1/examples/conversationbot.py000066400000000000000000000131641460724040100231070ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ First, a few callback functions are defined. Then, those functions are passed to the Application and registered at their respective places. Then, the bot is started and runs until we press Ctrl-C on the command line. Usage: Example of a bot-user conversation using ConversationHandler. Send /start to initiate the conversation. Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import logging from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update from telegram.ext import ( Application, CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) GENDER, PHOTO, LOCATION, BIO = range(4) async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Starts the conversation and asks the user about their gender.""" reply_keyboard = [["Boy", "Girl", "Other"]] await update.message.reply_text( "Hi! My name is Professor Bot. I will hold a conversation with you. " "Send /cancel to stop talking to me.\n\n" "Are you a boy or a girl?", reply_markup=ReplyKeyboardMarkup( reply_keyboard, one_time_keyboard=True, input_field_placeholder="Boy or Girl?" ), ) return GENDER async def gender(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Stores the selected gender and asks for a photo.""" user = update.message.from_user logger.info("Gender of %s: %s", user.first_name, update.message.text) await update.message.reply_text( "I see! Please send me a photo of yourself, " "so I know what you look like, or send /skip if you don't want to.", reply_markup=ReplyKeyboardRemove(), ) return PHOTO async def photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Stores the photo and asks for a location.""" user = update.message.from_user photo_file = await update.message.photo[-1].get_file() await photo_file.download_to_drive("user_photo.jpg") logger.info("Photo of %s: %s", user.first_name, "user_photo.jpg") await update.message.reply_text( "Gorgeous! Now, send me your location please, or send /skip if you don't want to." ) return LOCATION async def skip_photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Skips the photo and asks for a location.""" user = update.message.from_user logger.info("User %s did not send a photo.", user.first_name) await update.message.reply_text( "I bet you look great! Now, send me your location please, or send /skip." ) return LOCATION async def location(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Stores the location and asks for some info about the user.""" user = update.message.from_user user_location = update.message.location logger.info( "Location of %s: %f / %f", user.first_name, user_location.latitude, user_location.longitude ) await update.message.reply_text( "Maybe I can visit you sometime! At last, tell me something about yourself." ) return BIO async def skip_location(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Skips the location and asks for info about the user.""" user = update.message.from_user logger.info("User %s did not send a location.", user.first_name) await update.message.reply_text( "You seem a bit paranoid! At last, tell me something about yourself." ) return BIO async def bio(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Stores the info about the user and ends the conversation.""" user = update.message.from_user logger.info("Bio of %s: %s", user.first_name, update.message.text) await update.message.reply_text("Thank you! I hope we can talk again some day.") return ConversationHandler.END async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Cancels and ends the conversation.""" user = update.message.from_user logger.info("User %s canceled the conversation.", user.first_name) await update.message.reply_text( "Bye! I hope we can talk again some day.", reply_markup=ReplyKeyboardRemove() ) return ConversationHandler.END def main() -> None: """Run the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() # Add conversation handler with the states GENDER, PHOTO, LOCATION and BIO conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], states={ GENDER: [MessageHandler(filters.Regex("^(Boy|Girl|Other)$"), gender)], PHOTO: [MessageHandler(filters.PHOTO, photo), CommandHandler("skip", skip_photo)], LOCATION: [ MessageHandler(filters.LOCATION, location), CommandHandler("skip", skip_location), ], BIO: [MessageHandler(filters.TEXT & ~filters.COMMAND, bio)], }, fallbacks=[CommandHandler("cancel", cancel)], ) application.add_handler(conv_handler) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/conversationbot2.mmd000066400000000000000000000020371460724040100233130ustar00rootroot00000000000000flowchart TB %% Documentation: https://mermaid-js.github.io/mermaid/#/flowchart A(("/start")):::entryPoint -->|Hi! My name is Doctor Botter...| B((CHOOSING)):::state B --> C("Something else..."):::userInput C --> |What category?| D((TYPING_CHOICE)):::state D --> E("(text)"):::userInput E --> |"[save choice]
I'd love to hear about that!"| F((TYPING_REPLY)):::state F --> G("(text)"):::userInput G --> |"[save choice: text]
Neat!
(List of facts)
More?"| B B --> H("- Age
- Favourite colour
- Number of siblings"):::userInput H --> |"[save choice]
I'd love to hear about that!"| F B --> I("Done"):::userInput I --> |"I learned these facts about you:
..."| End(("END")):::termination classDef userInput fill:#2a5279, color:#ffffff, stroke:#ffffff classDef state fill:#222222, color:#ffffff, stroke:#ffffff classDef entryPoint fill:#009c11, stroke:#42FF57, color:#ffffff classDef termination fill:#bb0007, stroke:#E60109, color:#ffffff python-telegram-bot-21.1.1/examples/conversationbot2.py000066400000000000000000000116031460724040100231650ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ First, a few callback functions are defined. Then, those functions are passed to the Application and registered at their respective places. Then, the bot is started and runs until we press Ctrl-C on the command line. Usage: Example of a bot-user conversation using ConversationHandler. Send /start to initiate the conversation. Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import logging from typing import Dict from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update from telegram.ext import ( Application, CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) CHOOSING, TYPING_REPLY, TYPING_CHOICE = range(3) reply_keyboard = [ ["Age", "Favourite colour"], ["Number of siblings", "Something else..."], ["Done"], ] markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) def facts_to_str(user_data: Dict[str, str]) -> str: """Helper function for formatting the gathered user info.""" facts = [f"{key} - {value}" for key, value in user_data.items()] return "\n".join(facts).join(["\n", "\n"]) async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Start the conversation and ask user for input.""" await update.message.reply_text( "Hi! My name is Doctor Botter. I will hold a more complex conversation with you. " "Why don't you tell me something about yourself?", reply_markup=markup, ) return CHOOSING async def regular_choice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Ask the user for info about the selected predefined choice.""" text = update.message.text context.user_data["choice"] = text await update.message.reply_text(f"Your {text.lower()}? Yes, I would love to hear about that!") return TYPING_REPLY async def custom_choice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Ask the user for a description of a custom category.""" await update.message.reply_text( 'Alright, please send me the category first, for example "Most impressive skill"' ) return TYPING_CHOICE async def received_information(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Store info provided by user and ask for the next category.""" user_data = context.user_data text = update.message.text category = user_data["choice"] user_data[category] = text del user_data["choice"] await update.message.reply_text( "Neat! Just so you know, this is what you already told me:" f"{facts_to_str(user_data)}You can tell me more, or change your opinion" " on something.", reply_markup=markup, ) return CHOOSING async def done(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Display the gathered info and end the conversation.""" user_data = context.user_data if "choice" in user_data: del user_data["choice"] await update.message.reply_text( f"I learned these facts about you: {facts_to_str(user_data)}Until next time!", reply_markup=ReplyKeyboardRemove(), ) user_data.clear() return ConversationHandler.END def main() -> None: """Run the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() # Add conversation handler with the states CHOOSING, TYPING_CHOICE and TYPING_REPLY conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], states={ CHOOSING: [ MessageHandler( filters.Regex("^(Age|Favourite colour|Number of siblings)$"), regular_choice ), MessageHandler(filters.Regex("^Something else...$"), custom_choice), ], TYPING_CHOICE: [ MessageHandler( filters.TEXT & ~(filters.COMMAND | filters.Regex("^Done$")), regular_choice ) ], TYPING_REPLY: [ MessageHandler( filters.TEXT & ~(filters.COMMAND | filters.Regex("^Done$")), received_information, ) ], }, fallbacks=[MessageHandler(filters.Regex("^Done$"), done)], ) application.add_handler(conv_handler) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/customwebhookbot/000077500000000000000000000000001460724040100227075ustar00rootroot00000000000000python-telegram-bot-21.1.1/examples/customwebhookbot/djangobot.py000066400000000000000000000137001460724040100252310ustar00rootroot00000000000000#!/usr/bin/env python # This program is dedicated to the public domain under the CC0 license. # pylint: disable=import-error,unused-argument """ Simple example of a bot that uses a custom webhook setup and handles custom updates. For the custom webhook setup, the libraries `Django` and `uvicorn` are used. Please install them as `pip install Django~=4.2.4 uvicorn~=0.23.2`. Note that any other `asyncio` based web server framework can be used for a custom webhook setup just as well. Usage: Set bot Token, URL, admin CHAT_ID and PORT after the imports. You may also need to change the `listen` value in the uvicorn configuration to match your setup. Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import asyncio import html import json import logging from dataclasses import dataclass from uuid import uuid4 import uvicorn from django.conf import settings from django.core.asgi import get_asgi_application from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest from django.urls import path from telegram import Update from telegram.constants import ParseMode from telegram.ext import ( Application, CallbackContext, CommandHandler, ContextTypes, ExtBot, TypeHandler, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # Define configuration constants URL = "https://domain.tld" ADMIN_CHAT_ID = 123456 PORT = 8000 TOKEN = "123:ABC" # nosec B105 @dataclass class WebhookUpdate: """Simple dataclass to wrap a custom update type""" user_id: int payload: str class CustomContext(CallbackContext[ExtBot, dict, dict, dict]): """ Custom CallbackContext class that makes `user_data` available for updates of type `WebhookUpdate`. """ @classmethod def from_update( cls, update: object, application: "Application", ) -> "CustomContext": if isinstance(update, WebhookUpdate): return cls(application=application, user_id=update.user_id) return super().from_update(update, application) async def start(update: Update, context: CustomContext) -> None: """Display a message with instructions on how to use this bot.""" payload_url = html.escape(f"{URL}/submitpayload?user_id=&payload=") text = ( f"To check if the bot is still running, call {URL}/healthcheck.\n\n" f"To post a custom update, call {payload_url}." ) await update.message.reply_html(text=text) async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None: """Handle custom updates.""" chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id) payloads = context.user_data.setdefault("payloads", []) payloads.append(update.payload) combined_payloads = "\n• ".join(payloads) text = ( f"The user {chat_member.user.mention_html()} has sent a new payload. " f"So far they have sent the following payloads: \n\n• {combined_payloads}" ) await context.bot.send_message(chat_id=ADMIN_CHAT_ID, text=text, parse_mode=ParseMode.HTML) async def telegram(request: HttpRequest) -> HttpResponse: """Handle incoming Telegram updates by putting them into the `update_queue`""" await ptb_application.update_queue.put( Update.de_json(data=json.loads(request.body), bot=ptb_application.bot) ) return HttpResponse() async def custom_updates(request: HttpRequest) -> HttpResponse: """ Handle incoming webhook updates by also putting them into the `update_queue` if the required parameters were passed correctly. """ try: user_id = int(request.GET["user_id"]) payload = request.GET["payload"] except KeyError: return HttpResponseBadRequest( "Please pass both `user_id` and `payload` as query parameters.", ) except ValueError: return HttpResponseBadRequest("The `user_id` must be a string!") await ptb_application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload)) return HttpResponse() async def health(_: HttpRequest) -> HttpResponse: """For the health endpoint, reply with a simple plain text message.""" return HttpResponse("The bot is still running fine :)") # Set up PTB application and a web application for handling the incoming requests. context_types = ContextTypes(context=CustomContext) # Here we set updater to None because we want our custom webhook server to handle the updates # and hence we don't need an Updater instance ptb_application = ( Application.builder().token(TOKEN).updater(None).context_types(context_types).build() ) # register handlers ptb_application.add_handler(CommandHandler("start", start)) ptb_application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update)) urlpatterns = [ path("telegram", telegram, name="Telegram updates"), path("submitpayload", custom_updates, name="custom updates"), path("healthcheck", health, name="health check"), ] settings.configure(ROOT_URLCONF=__name__, SECRET_KEY=uuid4().hex) async def main() -> None: """Finalize configuration and run the applications.""" webserver = uvicorn.Server( config=uvicorn.Config( app=get_asgi_application(), port=PORT, use_colors=False, host="127.0.0.1", ) ) # Pass webhook settings to telegram await ptb_application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES) # Run application and webserver together async with ptb_application: await ptb_application.start() await webserver.serve() await ptb_application.stop() if __name__ == "__main__": asyncio.run(main()) python-telegram-bot-21.1.1/examples/customwebhookbot/flaskbot.py000066400000000000000000000137051460724040100250740ustar00rootroot00000000000000#!/usr/bin/env python # This program is dedicated to the public domain under the CC0 license. # pylint: disable=import-error,unused-argument """ Simple example of a bot that uses a custom webhook setup and handles custom updates. For the custom webhook setup, the libraries `flask`, `asgiref` and `uvicorn` are used. Please install them as `pip install flask[async]~=2.3.2 uvicorn~=0.23.2 asgiref~=3.7.2`. Note that any other `asyncio` based web server framework can be used for a custom webhook setup just as well. Usage: Set bot Token, URL, admin CHAT_ID and PORT after the imports. You may also need to change the `listen` value in the uvicorn configuration to match your setup. Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import asyncio import html import logging from dataclasses import dataclass from http import HTTPStatus import uvicorn from asgiref.wsgi import WsgiToAsgi from flask import Flask, Response, abort, make_response, request from telegram import Update from telegram.constants import ParseMode from telegram.ext import ( Application, CallbackContext, CommandHandler, ContextTypes, ExtBot, TypeHandler, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # Define configuration constants URL = "https://domain.tld" ADMIN_CHAT_ID = 123456 PORT = 8000 TOKEN = "123:ABC" # nosec B105 @dataclass class WebhookUpdate: """Simple dataclass to wrap a custom update type""" user_id: int payload: str class CustomContext(CallbackContext[ExtBot, dict, dict, dict]): """ Custom CallbackContext class that makes `user_data` available for updates of type `WebhookUpdate`. """ @classmethod def from_update( cls, update: object, application: "Application", ) -> "CustomContext": if isinstance(update, WebhookUpdate): return cls(application=application, user_id=update.user_id) return super().from_update(update, application) async def start(update: Update, context: CustomContext) -> None: """Display a message with instructions on how to use this bot.""" payload_url = html.escape(f"{URL}/submitpayload?user_id=&payload=") text = ( f"To check if the bot is still running, call {URL}/healthcheck.\n\n" f"To post a custom update, call {payload_url}." ) await update.message.reply_html(text=text) async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None: """Handle custom updates.""" chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id) payloads = context.user_data.setdefault("payloads", []) payloads.append(update.payload) combined_payloads = "\n• ".join(payloads) text = ( f"The user {chat_member.user.mention_html()} has sent a new payload. " f"So far they have sent the following payloads: \n\n• {combined_payloads}" ) await context.bot.send_message(chat_id=ADMIN_CHAT_ID, text=text, parse_mode=ParseMode.HTML) async def main() -> None: """Set up PTB application and a web application for handling the incoming requests.""" context_types = ContextTypes(context=CustomContext) # Here we set updater to None because we want our custom webhook server to handle the updates # and hence we don't need an Updater instance application = ( Application.builder().token(TOKEN).updater(None).context_types(context_types).build() ) # register handlers application.add_handler(CommandHandler("start", start)) application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update)) # Pass webhook settings to telegram await application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES) # Set up webserver flask_app = Flask(__name__) @flask_app.post("/telegram") # type: ignore[misc] async def telegram() -> Response: """Handle incoming Telegram updates by putting them into the `update_queue`""" await application.update_queue.put(Update.de_json(data=request.json, bot=application.bot)) return Response(status=HTTPStatus.OK) @flask_app.route("/submitpayload", methods=["GET", "POST"]) # type: ignore[misc] async def custom_updates() -> Response: """ Handle incoming webhook updates by also putting them into the `update_queue` if the required parameters were passed correctly. """ try: user_id = int(request.args["user_id"]) payload = request.args["payload"] except KeyError: abort( HTTPStatus.BAD_REQUEST, "Please pass both `user_id` and `payload` as query parameters.", ) except ValueError: abort(HTTPStatus.BAD_REQUEST, "The `user_id` must be a string!") await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload)) return Response(status=HTTPStatus.OK) @flask_app.get("/healthcheck") # type: ignore[misc] async def health() -> Response: """For the health endpoint, reply with a simple plain text message.""" response = make_response("The bot is still running fine :)", HTTPStatus.OK) response.mimetype = "text/plain" return response webserver = uvicorn.Server( config=uvicorn.Config( app=WsgiToAsgi(flask_app), port=PORT, use_colors=False, host="127.0.0.1", ) ) # Run application and webserver together async with application: await application.start() await webserver.serve() await application.stop() if __name__ == "__main__": asyncio.run(main()) python-telegram-bot-21.1.1/examples/customwebhookbot/quartbot.py000066400000000000000000000136351460724040100251320ustar00rootroot00000000000000#!/usr/bin/env python # This program is dedicated to the public domain under the CC0 license. # pylint: disable=import-error,unused-argument """ Simple example of a bot that uses a custom webhook setup and handles custom updates. For the custom webhook setup, the libraries `quart` and `uvicorn` are used. Please install them as `pip install quart~=0.18.4 uvicorn~=0.23.2`. Note that any other `asyncio` based web server framework can be used for a custom webhook setup just as well. Usage: Set bot Token, URL, admin CHAT_ID and PORT after the imports. You may also need to change the `listen` value in the uvicorn configuration to match your setup. Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import asyncio import html import logging from dataclasses import dataclass from http import HTTPStatus import uvicorn from quart import Quart, Response, abort, make_response, request from telegram import Update from telegram.constants import ParseMode from telegram.ext import ( Application, CallbackContext, CommandHandler, ContextTypes, ExtBot, TypeHandler, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # Define configuration constants URL = "https://domain.tld" ADMIN_CHAT_ID = 123456 PORT = 8000 TOKEN = "123:ABC" # nosec B105 @dataclass class WebhookUpdate: """Simple dataclass to wrap a custom update type""" user_id: int payload: str class CustomContext(CallbackContext[ExtBot, dict, dict, dict]): """ Custom CallbackContext class that makes `user_data` available for updates of type `WebhookUpdate`. """ @classmethod def from_update( cls, update: object, application: "Application", ) -> "CustomContext": if isinstance(update, WebhookUpdate): return cls(application=application, user_id=update.user_id) return super().from_update(update, application) async def start(update: Update, context: CustomContext) -> None: """Display a message with instructions on how to use this bot.""" payload_url = html.escape(f"{URL}/submitpayload?user_id=&payload=") text = ( f"To check if the bot is still running, call {URL}/healthcheck.\n\n" f"To post a custom update, call {payload_url}." ) await update.message.reply_html(text=text) async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None: """Handle custom updates.""" chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id) payloads = context.user_data.setdefault("payloads", []) payloads.append(update.payload) combined_payloads = "\n• ".join(payloads) text = ( f"The user {chat_member.user.mention_html()} has sent a new payload. " f"So far they have sent the following payloads: \n\n• {combined_payloads}" ) await context.bot.send_message(chat_id=ADMIN_CHAT_ID, text=text, parse_mode=ParseMode.HTML) async def main() -> None: """Set up PTB application and a web application for handling the incoming requests.""" context_types = ContextTypes(context=CustomContext) # Here we set updater to None because we want our custom webhook server to handle the updates # and hence we don't need an Updater instance application = ( Application.builder().token(TOKEN).updater(None).context_types(context_types).build() ) # register handlers application.add_handler(CommandHandler("start", start)) application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update)) # Pass webhook settings to telegram await application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES) # Set up webserver quart_app = Quart(__name__) @quart_app.post("/telegram") # type: ignore[misc] async def telegram() -> Response: """Handle incoming Telegram updates by putting them into the `update_queue`""" await application.update_queue.put( Update.de_json(data=await request.get_json(), bot=application.bot) ) return Response(status=HTTPStatus.OK) @quart_app.route("/submitpayload", methods=["GET", "POST"]) # type: ignore[misc] async def custom_updates() -> Response: """ Handle incoming webhook updates by also putting them into the `update_queue` if the required parameters were passed correctly. """ try: user_id = int(request.args["user_id"]) payload = request.args["payload"] except KeyError: abort( HTTPStatus.BAD_REQUEST, "Please pass both `user_id` and `payload` as query parameters.", ) except ValueError: abort(HTTPStatus.BAD_REQUEST, "The `user_id` must be a string!") await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload)) return Response(status=HTTPStatus.OK) @quart_app.get("/healthcheck") # type: ignore[misc] async def health() -> Response: """For the health endpoint, reply with a simple plain text message.""" response = await make_response("The bot is still running fine :)", HTTPStatus.OK) response.mimetype = "text/plain" return response webserver = uvicorn.Server( config=uvicorn.Config( app=quart_app, port=PORT, use_colors=False, host="127.0.0.1", ) ) # Run application and webserver together async with application: await application.start() await webserver.serve() await application.stop() if __name__ == "__main__": asyncio.run(main()) python-telegram-bot-21.1.1/examples/customwebhookbot/starlettebot.py000066400000000000000000000143101460724040100257740ustar00rootroot00000000000000#!/usr/bin/env python # This program is dedicated to the public domain under the CC0 license. # pylint: disable=import-error,unused-argument """ Simple example of a bot that uses a custom webhook setup and handles custom updates. For the custom webhook setup, the libraries `starlette` and `uvicorn` are used. Please install them as `pip install starlette~=0.20.0 uvicorn~=0.23.2`. Note that any other `asyncio` based web server framework can be used for a custom webhook setup just as well. Usage: Set bot Token, URL, admin CHAT_ID and PORT after the imports. You may also need to change the `listen` value in the uvicorn configuration to match your setup. Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import asyncio import html import logging from dataclasses import dataclass from http import HTTPStatus import uvicorn from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import PlainTextResponse, Response from starlette.routing import Route from telegram import Update from telegram.constants import ParseMode from telegram.ext import ( Application, CallbackContext, CommandHandler, ContextTypes, ExtBot, TypeHandler, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # Define configuration constants URL = "https://domain.tld" ADMIN_CHAT_ID = 123456 PORT = 8000 TOKEN = "123:ABC" # nosec B105 @dataclass class WebhookUpdate: """Simple dataclass to wrap a custom update type""" user_id: int payload: str class CustomContext(CallbackContext[ExtBot, dict, dict, dict]): """ Custom CallbackContext class that makes `user_data` available for updates of type `WebhookUpdate`. """ @classmethod def from_update( cls, update: object, application: "Application", ) -> "CustomContext": if isinstance(update, WebhookUpdate): return cls(application=application, user_id=update.user_id) return super().from_update(update, application) async def start(update: Update, context: CustomContext) -> None: """Display a message with instructions on how to use this bot.""" payload_url = html.escape(f"{URL}/submitpayload?user_id=&payload=") text = ( f"To check if the bot is still running, call {URL}/healthcheck.\n\n" f"To post a custom update, call {payload_url}." ) await update.message.reply_html(text=text) async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None: """Handle custom updates.""" chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id) payloads = context.user_data.setdefault("payloads", []) payloads.append(update.payload) combined_payloads = "\n• ".join(payloads) text = ( f"The user {chat_member.user.mention_html()} has sent a new payload. " f"So far they have sent the following payloads: \n\n• {combined_payloads}" ) await context.bot.send_message(chat_id=ADMIN_CHAT_ID, text=text, parse_mode=ParseMode.HTML) async def main() -> None: """Set up PTB application and a web application for handling the incoming requests.""" context_types = ContextTypes(context=CustomContext) # Here we set updater to None because we want our custom webhook server to handle the updates # and hence we don't need an Updater instance application = ( Application.builder().token(TOKEN).updater(None).context_types(context_types).build() ) # register handlers application.add_handler(CommandHandler("start", start)) application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update)) # Pass webhook settings to telegram await application.bot.set_webhook(url=f"{URL}/telegram", allowed_updates=Update.ALL_TYPES) # Set up webserver async def telegram(request: Request) -> Response: """Handle incoming Telegram updates by putting them into the `update_queue`""" await application.update_queue.put( Update.de_json(data=await request.json(), bot=application.bot) ) return Response() async def custom_updates(request: Request) -> PlainTextResponse: """ Handle incoming webhook updates by also putting them into the `update_queue` if the required parameters were passed correctly. """ try: user_id = int(request.query_params["user_id"]) payload = request.query_params["payload"] except KeyError: return PlainTextResponse( status_code=HTTPStatus.BAD_REQUEST, content="Please pass both `user_id` and `payload` as query parameters.", ) except ValueError: return PlainTextResponse( status_code=HTTPStatus.BAD_REQUEST, content="The `user_id` must be a string!", ) await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload)) return PlainTextResponse("Thank you for the submission! It's being forwarded.") async def health(_: Request) -> PlainTextResponse: """For the health endpoint, reply with a simple plain text message.""" return PlainTextResponse(content="The bot is still running fine :)") starlette_app = Starlette( routes=[ Route("/telegram", telegram, methods=["POST"]), Route("/healthcheck", health, methods=["GET"]), Route("/submitpayload", custom_updates, methods=["POST", "GET"]), ] ) webserver = uvicorn.Server( config=uvicorn.Config( app=starlette_app, port=PORT, use_colors=False, host="127.0.0.1", ) ) # Run application and webserver together async with application: await application.start() await webserver.serve() await application.stop() if __name__ == "__main__": asyncio.run(main()) python-telegram-bot-21.1.1/examples/deeplinking.py000066400000000000000000000124671460724040100221660ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """Bot that explains Telegram's "Deep Linking Parameters" functionality. This program is dedicated to the public domain under the CC0 license. This Bot uses the Application class to handle the bot. First, a few handler functions are defined. Then, those functions are passed to the Application and registered at their respective places. Then, the bot is started and runs until we press Ctrl-C on the command line. Usage: Deep Linking example. Send /start to get the link. Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import logging from telegram import ( InlineKeyboardButton, InlineKeyboardMarkup, LinkPreviewOptions, Update, helpers, ) from telegram.constants import ParseMode from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, filters # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # Define constants that will allow us to reuse the deep-linking parameters. CHECK_THIS_OUT = "check-this-out" USING_ENTITIES = "using-entities-here" USING_KEYBOARD = "using-keyboard-here" SO_COOL = "so-cool" # Callback data to pass in 3rd level deep-linking KEYBOARD_CALLBACKDATA = "keyboard-callback-data" async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Send a deep-linked URL when the command /start is issued.""" bot = context.bot url = helpers.create_deep_linked_url(bot.username, CHECK_THIS_OUT, group=True) text = "Feel free to tell your friends about it:\n\n" + url await update.message.reply_text(text) async def deep_linked_level_1(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Reached through the CHECK_THIS_OUT payload""" bot = context.bot url = helpers.create_deep_linked_url(bot.username, SO_COOL) text = ( "Awesome, you just accessed hidden functionality! Now let's get back to the private chat." ) keyboard = InlineKeyboardMarkup.from_button( InlineKeyboardButton(text="Continue here!", url=url) ) await update.message.reply_text(text, reply_markup=keyboard) async def deep_linked_level_2(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Reached through the SO_COOL payload""" bot = context.bot url = helpers.create_deep_linked_url(bot.username, USING_ENTITIES) text = f'You can also mask the deep-linked URLs as links: ▶️ CLICK HERE.' await update.message.reply_text( text, parse_mode=ParseMode.HTML, link_preview_options=LinkPreviewOptions(is_disabled=True) ) async def deep_linked_level_3(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Reached through the USING_ENTITIES payload""" await update.message.reply_text( "It is also possible to make deep-linking using InlineKeyboardButtons.", reply_markup=InlineKeyboardMarkup( [[InlineKeyboardButton(text="Like this!", callback_data=KEYBOARD_CALLBACKDATA)]] ), ) async def deep_link_level_3_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Answers CallbackQuery with deeplinking url.""" bot = context.bot url = helpers.create_deep_linked_url(bot.username, USING_KEYBOARD) await update.callback_query.answer(url=url) async def deep_linked_level_4(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Reached through the USING_KEYBOARD payload""" payload = context.args await update.message.reply_text( f"Congratulations! This is as deep as it gets 👏🏻\n\nThe payload was: {payload}" ) def main() -> None: """Start the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() # More info on what deep linking actually is (read this first if it's unclear to you): # https://core.telegram.org/bots/features#deep-linking # Register a deep-linking handler application.add_handler( CommandHandler("start", deep_linked_level_1, filters.Regex(CHECK_THIS_OUT)) ) # This one works with a textual link instead of an URL application.add_handler(CommandHandler("start", deep_linked_level_2, filters.Regex(SO_COOL))) # We can also pass on the deep-linking payload application.add_handler( CommandHandler("start", deep_linked_level_3, filters.Regex(USING_ENTITIES)) ) # Possible with inline keyboard buttons as well application.add_handler( CommandHandler("start", deep_linked_level_4, filters.Regex(USING_KEYBOARD)) ) # register callback handler for inline keyboard button application.add_handler( CallbackQueryHandler(deep_link_level_3_callback, pattern=KEYBOARD_CALLBACKDATA) ) # Make sure the deep-linking handlers occur *before* the normal /start handler. application.add_handler(CommandHandler("start", start)) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/echobot.py000066400000000000000000000045101460724040100213060ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ Simple Bot to reply to Telegram messages. First, a few handler functions are defined. Then, those functions are passed to the Application and registered at their respective places. Then, the bot is started and runs until we press Ctrl-C on the command line. Usage: Basic Echobot example, repeats messages. Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import logging from telegram import ForceReply, Update from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # Define a few command handlers. These usually take the two arguments update and # context. async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Send a message when the command /start is issued.""" user = update.effective_user await update.message.reply_html( rf"Hi {user.mention_html()}!", reply_markup=ForceReply(selective=True), ) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Send a message when the command /help is issued.""" await update.message.reply_text("Help!") async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Echo the user message.""" await update.message.reply_text(update.message.text) def main() -> None: """Start the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() # on different commands - answer in Telegram application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("help", help_command)) # on non command i.e message - echo the message on Telegram application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo)) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/errorhandlerbot.py000066400000000000000000000065351460724040100230700ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """This is a very simple example on how one could implement a custom error handler.""" import html import json import logging import traceback from telegram import Update from telegram.constants import ParseMode from telegram.ext import Application, CommandHandler, ContextTypes # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # This can be your own ID, or one for a developer group/channel. # You can use the /start command of this bot to see your chat id. DEVELOPER_CHAT_ID = 123456789 async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None: """Log the error and send a telegram message to notify the developer.""" # Log the error before we do anything else, so we can see it even if something breaks. logger.error("Exception while handling an update:", exc_info=context.error) # traceback.format_exception returns the usual python message about an exception, but as a # list of strings rather than a single string, so we have to join them together. tb_list = traceback.format_exception(None, context.error, context.error.__traceback__) tb_string = "".join(tb_list) # Build the message with some markup and additional information about what happened. # You might need to add some logic to deal with messages longer than the 4096 character limit. update_str = update.to_dict() if isinstance(update, Update) else str(update) message = ( "An exception was raised while handling an update\n" f"

update = {html.escape(json.dumps(update_str, indent=2, ensure_ascii=False))}"
        "
\n\n" f"
context.chat_data = {html.escape(str(context.chat_data))}
\n\n" f"
context.user_data = {html.escape(str(context.user_data))}
\n\n" f"
{html.escape(tb_string)}
" ) # Finally, send the message await context.bot.send_message( chat_id=DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML ) async def bad_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Raise an error to trigger the error handler.""" await context.bot.wrong_method_name() # type: ignore[attr-defined] async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Displays info on how to trigger an error.""" await update.effective_message.reply_html( "Use /bad_command to cause an error.\n" f"Your chat id is {update.effective_chat.id}." ) def main() -> None: """Run the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() # Register the commands... application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("bad_command", bad_command)) # ...and the error handler application.add_error_handler(error_handler) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/inlinebot.py000066400000000000000000000062311460724040100216500ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ Don't forget to enable inline mode with @BotFather First, a few handler functions are defined. Then, those functions are passed to the Application and registered at their respective places. Then, the bot is started and runs until we press Ctrl-C on the command line. Usage: Basic inline bot example. Applies different text transformations. Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import logging from html import escape from uuid import uuid4 from telegram import InlineQueryResultArticle, InputTextMessageContent, Update from telegram.constants import ParseMode from telegram.ext import Application, CommandHandler, ContextTypes, InlineQueryHandler # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # Define a few command handlers. These usually take the two arguments update and # context. async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Send a message when the command /start is issued.""" await update.message.reply_text("Hi!") async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Send a message when the command /help is issued.""" await update.message.reply_text("Help!") async def inline_query(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the inline query. This is run when you type: @botusername """ query = update.inline_query.query if not query: # empty query should not be handled return results = [ InlineQueryResultArticle( id=str(uuid4()), title="Caps", input_message_content=InputTextMessageContent(query.upper()), ), InlineQueryResultArticle( id=str(uuid4()), title="Bold", input_message_content=InputTextMessageContent( f"{escape(query)}", parse_mode=ParseMode.HTML ), ), InlineQueryResultArticle( id=str(uuid4()), title="Italic", input_message_content=InputTextMessageContent( f"{escape(query)}", parse_mode=ParseMode.HTML ), ), ] await update.inline_query.answer(results) def main() -> None: """Run the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() # on different commands - answer in Telegram application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("help", help_command)) # on inline queries - show corresponding inline results application.add_handler(InlineQueryHandler(inline_query)) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/inlinekeyboard.py000066400000000000000000000046601460724040100226700ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ Basic example for a bot that uses inline keyboards. For an in-depth explanation, check out https://github.com/python-telegram-bot/python-telegram-bot/wiki/InlineKeyboard-Example. """ import logging from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Sends a message with three inline buttons attached.""" keyboard = [ [ InlineKeyboardButton("Option 1", callback_data="1"), InlineKeyboardButton("Option 2", callback_data="2"), ], [InlineKeyboardButton("Option 3", callback_data="3")], ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text("Please choose:", reply_markup=reply_markup) async def button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Parses the CallbackQuery and updates the message text.""" query = update.callback_query # CallbackQueries need to be answered, even if no notification to the user is needed # Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery await query.answer() await query.edit_message_text(text=f"Selected option: {query.data}") async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Displays info on how to use the bot.""" await update.message.reply_text("Use /start to test this bot.") def main() -> None: """Run the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() application.add_handler(CommandHandler("start", start)) application.add_handler(CallbackQueryHandler(button)) application.add_handler(CommandHandler("help", help_command)) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/inlinekeyboard2.py000066400000000000000000000162451460724040100227540ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """Simple inline keyboard bot with multiple CallbackQueryHandlers. This Bot uses the Application class to handle the bot. First, a few callback functions are defined as callback query handler. Then, those functions are passed to the Application and registered at their respective places. Then, the bot is started and runs until we press Ctrl-C on the command line. Usage: Example of a bot that uses inline keyboard that has multiple CallbackQueryHandlers arranged in a ConversationHandler. Send /start to initiate the conversation. Press Ctrl-C on the command line to stop the bot. """ import logging from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ( Application, CallbackQueryHandler, CommandHandler, ContextTypes, ConversationHandler, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # Stages START_ROUTES, END_ROUTES = range(2) # Callback data ONE, TWO, THREE, FOUR = range(4) async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Send message on `/start`.""" # Get user that sent /start and log his name user = update.message.from_user logger.info("User %s started the conversation.", user.first_name) # Build InlineKeyboard where each button has a displayed text # and a string as callback_data # The keyboard is a list of button rows, where each row is in turn # a list (hence `[[...]]`). keyboard = [ [ InlineKeyboardButton("1", callback_data=str(ONE)), InlineKeyboardButton("2", callback_data=str(TWO)), ] ] reply_markup = InlineKeyboardMarkup(keyboard) # Send message with text and appended InlineKeyboard await update.message.reply_text("Start handler, Choose a route", reply_markup=reply_markup) # Tell ConversationHandler that we're in state `FIRST` now return START_ROUTES async def start_over(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Prompt same text & keyboard as `start` does but not as new message""" # Get CallbackQuery from Update query = update.callback_query # CallbackQueries need to be answered, even if no notification to the user is needed # Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery await query.answer() keyboard = [ [ InlineKeyboardButton("1", callback_data=str(ONE)), InlineKeyboardButton("2", callback_data=str(TWO)), ] ] reply_markup = InlineKeyboardMarkup(keyboard) # Instead of sending a new message, edit the message that # originated the CallbackQuery. This gives the feeling of an # interactive menu. await query.edit_message_text(text="Start handler, Choose a route", reply_markup=reply_markup) return START_ROUTES async def one(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Show new choice of buttons""" query = update.callback_query await query.answer() keyboard = [ [ InlineKeyboardButton("3", callback_data=str(THREE)), InlineKeyboardButton("4", callback_data=str(FOUR)), ] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text( text="First CallbackQueryHandler, Choose a route", reply_markup=reply_markup ) return START_ROUTES async def two(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Show new choice of buttons""" query = update.callback_query await query.answer() keyboard = [ [ InlineKeyboardButton("1", callback_data=str(ONE)), InlineKeyboardButton("3", callback_data=str(THREE)), ] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text( text="Second CallbackQueryHandler, Choose a route", reply_markup=reply_markup ) return START_ROUTES async def three(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Show new choice of buttons. This is the end point of the conversation.""" query = update.callback_query await query.answer() keyboard = [ [ InlineKeyboardButton("Yes, let's do it again!", callback_data=str(ONE)), InlineKeyboardButton("Nah, I've had enough ...", callback_data=str(TWO)), ] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text( text="Third CallbackQueryHandler. Do want to start over?", reply_markup=reply_markup ) # Transfer to conversation state `SECOND` return END_ROUTES async def four(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Show new choice of buttons""" query = update.callback_query await query.answer() keyboard = [ [ InlineKeyboardButton("2", callback_data=str(TWO)), InlineKeyboardButton("3", callback_data=str(THREE)), ] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text( text="Fourth CallbackQueryHandler, Choose a route", reply_markup=reply_markup ) return START_ROUTES async def end(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Returns `ConversationHandler.END`, which tells the ConversationHandler that the conversation is over. """ query = update.callback_query await query.answer() await query.edit_message_text(text="See you next time!") return ConversationHandler.END def main() -> None: """Run the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() # Setup conversation handler with the states FIRST and SECOND # Use the pattern parameter to pass CallbackQueries with specific # data pattern to the corresponding handlers. # ^ means "start of line/string" # $ means "end of line/string" # So ^ABC$ will only allow 'ABC' conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], states={ START_ROUTES: [ CallbackQueryHandler(one, pattern="^" + str(ONE) + "$"), CallbackQueryHandler(two, pattern="^" + str(TWO) + "$"), CallbackQueryHandler(three, pattern="^" + str(THREE) + "$"), CallbackQueryHandler(four, pattern="^" + str(FOUR) + "$"), ], END_ROUTES: [ CallbackQueryHandler(start_over, pattern="^" + str(ONE) + "$"), CallbackQueryHandler(end, pattern="^" + str(TWO) + "$"), ], }, fallbacks=[CommandHandler("start", start)], ) # Add ConversationHandler to application that will be used for handling updates application.add_handler(conv_handler) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/nestedconversationbot.mmd000066400000000000000000000036041460724040100244350ustar00rootroot00000000000000flowchart TB %% Documentation: https://mermaid-js.github.io/mermaid/#/flowchart A(("/start")):::entryPoint -->|Hi! I'm FamilyBot...| B((SELECTING_ACTION)):::state B --> C("Show Data"):::userInput C --> |"(List of gathered data)"| D((SHOWING)):::state D --> E("Back"):::userInput E --> B B --> F("Add Yourself"):::userInput F --> G(("DESCRIBING_SELF")):::state G --> H("Add info"):::userInput H --> I((SELECT_FEATURE)):::state I --> |"Please select a feature to update.
- Name
- Age
- Done"|J("(choice)"):::userInput J --> |"Okay, tell me."| K((TYPING)):::state K --> L("(text)"):::userInput L --> |"[saving]"|I I --> M("Done"):::userInput M --> B B --> N("Add family member"):::userInput R --> I W --> |"See you around!"|End(("END")):::termination Y(("ANY STATE")):::state --> Z("/stop"):::userInput Z -->|"Okay, bye."| End B --> W("Done"):::userInput subgraph nestedConversation[Nested Conversation: Add Family Member] direction BT N --> O(("SELECT_LEVEL")):::state O --> |"Add...
- Add Parent
- Add Child
"|P("(choice)"):::userInput P --> Q(("SELECT_GENDER")):::state Q --> |"- Mother
- Father
/
- Sister
- Brother"| R("(choice)"):::userInput Q --> V("Show Data"):::userInput Q --> T(("SELECTING_ACTION")):::state Q --> U("Back"):::userInput U --> T O --> U O --> V V --> S(("SHOWING")):::state V --> T end classDef userInput fill:#2a5279, color:#ffffff, stroke:#ffffff classDef state fill:#222222, color:#ffffff, stroke:#ffffff classDef entryPoint fill:#009c11, stroke:#42FF57, color:#ffffff classDef termination fill:#bb0007, stroke:#E60109, color:#ffffff style nestedConversation fill:#999999, stroke-width:2px, stroke:#333333 python-telegram-bot-21.1.1/examples/nestedconversationbot.py000066400000000000000000000317741460724040100243210ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ First, a few callback functions are defined. Then, those functions are passed to the Application and registered at their respective places. Then, the bot is started and runs until we press Ctrl-C on the command line. Usage: Example of a bot-user conversation using nested ConversationHandlers. Send /start to initiate the conversation. Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import logging from typing import Any, Dict, Tuple from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ( Application, CallbackQueryHandler, CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # State definitions for top level conversation SELECTING_ACTION, ADDING_MEMBER, ADDING_SELF, DESCRIBING_SELF = map(chr, range(4)) # State definitions for second level conversation SELECTING_LEVEL, SELECTING_GENDER = map(chr, range(4, 6)) # State definitions for descriptions conversation SELECTING_FEATURE, TYPING = map(chr, range(6, 8)) # Meta states STOPPING, SHOWING = map(chr, range(8, 10)) # Shortcut for ConversationHandler.END END = ConversationHandler.END # Different constants for this example ( PARENTS, CHILDREN, SELF, GENDER, MALE, FEMALE, AGE, NAME, START_OVER, FEATURES, CURRENT_FEATURE, CURRENT_LEVEL, ) = map(chr, range(10, 22)) # Helper def _name_switcher(level: str) -> Tuple[str, str]: if level == PARENTS: return "Father", "Mother" return "Brother", "Sister" # Top level conversation callbacks async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Select an action: Adding parent/child or show data.""" text = ( "You may choose to add a family member, yourself, show the gathered data, or end the " "conversation. To abort, simply type /stop." ) buttons = [ [ InlineKeyboardButton(text="Add family member", callback_data=str(ADDING_MEMBER)), InlineKeyboardButton(text="Add yourself", callback_data=str(ADDING_SELF)), ], [ InlineKeyboardButton(text="Show data", callback_data=str(SHOWING)), InlineKeyboardButton(text="Done", callback_data=str(END)), ], ] keyboard = InlineKeyboardMarkup(buttons) # If we're starting over we don't need to send a new message if context.user_data.get(START_OVER): await update.callback_query.answer() await update.callback_query.edit_message_text(text=text, reply_markup=keyboard) else: await update.message.reply_text( "Hi, I'm Family Bot and I'm here to help you gather information about your family." ) await update.message.reply_text(text=text, reply_markup=keyboard) context.user_data[START_OVER] = False return SELECTING_ACTION async def adding_self(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Add information about yourself.""" context.user_data[CURRENT_LEVEL] = SELF text = "Okay, please tell me about yourself." button = InlineKeyboardButton(text="Add info", callback_data=str(MALE)) keyboard = InlineKeyboardMarkup.from_button(button) await update.callback_query.answer() await update.callback_query.edit_message_text(text=text, reply_markup=keyboard) return DESCRIBING_SELF async def show_data(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Pretty print gathered data.""" def pretty_print(data: Dict[str, Any], level: str) -> str: people = data.get(level) if not people: return "\nNo information yet." return_str = "" if level == SELF: for person in data[level]: return_str += f"\nName: {person.get(NAME, '-')}, Age: {person.get(AGE, '-')}" else: male, female = _name_switcher(level) for person in data[level]: gender = female if person[GENDER] == FEMALE else male return_str += ( f"\n{gender}: Name: {person.get(NAME, '-')}, Age: {person.get(AGE, '-')}" ) return return_str user_data = context.user_data text = f"Yourself:{pretty_print(user_data, SELF)}" text += f"\n\nParents:{pretty_print(user_data, PARENTS)}" text += f"\n\nChildren:{pretty_print(user_data, CHILDREN)}" buttons = [[InlineKeyboardButton(text="Back", callback_data=str(END))]] keyboard = InlineKeyboardMarkup(buttons) await update.callback_query.answer() await update.callback_query.edit_message_text(text=text, reply_markup=keyboard) user_data[START_OVER] = True return SHOWING async def stop(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """End Conversation by command.""" await update.message.reply_text("Okay, bye.") return END async def end(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """End conversation from InlineKeyboardButton.""" await update.callback_query.answer() text = "See you around!" await update.callback_query.edit_message_text(text=text) return END # Second level conversation callbacks async def select_level(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Choose to add a parent or a child.""" text = "You may add a parent or a child. Also you can show the gathered data or go back." buttons = [ [ InlineKeyboardButton(text="Add parent", callback_data=str(PARENTS)), InlineKeyboardButton(text="Add child", callback_data=str(CHILDREN)), ], [ InlineKeyboardButton(text="Show data", callback_data=str(SHOWING)), InlineKeyboardButton(text="Back", callback_data=str(END)), ], ] keyboard = InlineKeyboardMarkup(buttons) await update.callback_query.answer() await update.callback_query.edit_message_text(text=text, reply_markup=keyboard) return SELECTING_LEVEL async def select_gender(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Choose to add mother or father.""" level = update.callback_query.data context.user_data[CURRENT_LEVEL] = level text = "Please choose, whom to add." male, female = _name_switcher(level) buttons = [ [ InlineKeyboardButton(text=f"Add {male}", callback_data=str(MALE)), InlineKeyboardButton(text=f"Add {female}", callback_data=str(FEMALE)), ], [ InlineKeyboardButton(text="Show data", callback_data=str(SHOWING)), InlineKeyboardButton(text="Back", callback_data=str(END)), ], ] keyboard = InlineKeyboardMarkup(buttons) await update.callback_query.answer() await update.callback_query.edit_message_text(text=text, reply_markup=keyboard) return SELECTING_GENDER async def end_second_level(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Return to top level conversation.""" context.user_data[START_OVER] = True await start(update, context) return END # Third level callbacks async def select_feature(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Select a feature to update for the person.""" buttons = [ [ InlineKeyboardButton(text="Name", callback_data=str(NAME)), InlineKeyboardButton(text="Age", callback_data=str(AGE)), InlineKeyboardButton(text="Done", callback_data=str(END)), ] ] keyboard = InlineKeyboardMarkup(buttons) # If we collect features for a new person, clear the cache and save the gender if not context.user_data.get(START_OVER): context.user_data[FEATURES] = {GENDER: update.callback_query.data} text = "Please select a feature to update." await update.callback_query.answer() await update.callback_query.edit_message_text(text=text, reply_markup=keyboard) # But after we do that, we need to send a new message else: text = "Got it! Please select a feature to update." await update.message.reply_text(text=text, reply_markup=keyboard) context.user_data[START_OVER] = False return SELECTING_FEATURE async def ask_for_input(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Prompt user to input data for selected feature.""" context.user_data[CURRENT_FEATURE] = update.callback_query.data text = "Okay, tell me." await update.callback_query.answer() await update.callback_query.edit_message_text(text=text) return TYPING async def save_input(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Save input for feature and return to feature selection.""" user_data = context.user_data user_data[FEATURES][user_data[CURRENT_FEATURE]] = update.message.text user_data[START_OVER] = True return await select_feature(update, context) async def end_describing(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """End gathering of features and return to parent conversation.""" user_data = context.user_data level = user_data[CURRENT_LEVEL] if not user_data.get(level): user_data[level] = [] user_data[level].append(user_data[FEATURES]) # Print upper level menu if level == SELF: user_data[START_OVER] = True await start(update, context) else: await select_level(update, context) return END async def stop_nested(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str: """Completely end conversation from within nested conversation.""" await update.message.reply_text("Okay, bye.") return STOPPING def main() -> None: """Run the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() # Set up third level ConversationHandler (collecting features) description_conv = ConversationHandler( entry_points=[ CallbackQueryHandler( select_feature, pattern="^" + str(MALE) + "$|^" + str(FEMALE) + "$" ) ], states={ SELECTING_FEATURE: [ CallbackQueryHandler(ask_for_input, pattern="^(?!" + str(END) + ").*$") ], TYPING: [MessageHandler(filters.TEXT & ~filters.COMMAND, save_input)], }, fallbacks=[ CallbackQueryHandler(end_describing, pattern="^" + str(END) + "$"), CommandHandler("stop", stop_nested), ], map_to_parent={ # Return to second level menu END: SELECTING_LEVEL, # End conversation altogether STOPPING: STOPPING, }, ) # Set up second level ConversationHandler (adding a person) add_member_conv = ConversationHandler( entry_points=[CallbackQueryHandler(select_level, pattern="^" + str(ADDING_MEMBER) + "$")], states={ SELECTING_LEVEL: [ CallbackQueryHandler(select_gender, pattern=f"^{PARENTS}$|^{CHILDREN}$") ], SELECTING_GENDER: [description_conv], }, fallbacks=[ CallbackQueryHandler(show_data, pattern="^" + str(SHOWING) + "$"), CallbackQueryHandler(end_second_level, pattern="^" + str(END) + "$"), CommandHandler("stop", stop_nested), ], map_to_parent={ # After showing data return to top level menu SHOWING: SHOWING, # Return to top level menu END: SELECTING_ACTION, # End conversation altogether STOPPING: END, }, ) # Set up top level ConversationHandler (selecting action) # Because the states of the third level conversation map to the ones of the second level # conversation, we need to make sure the top level conversation can also handle them selection_handlers = [ add_member_conv, CallbackQueryHandler(show_data, pattern="^" + str(SHOWING) + "$"), CallbackQueryHandler(adding_self, pattern="^" + str(ADDING_SELF) + "$"), CallbackQueryHandler(end, pattern="^" + str(END) + "$"), ] conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], states={ SHOWING: [CallbackQueryHandler(start, pattern="^" + str(END) + "$")], SELECTING_ACTION: selection_handlers, SELECTING_LEVEL: selection_handlers, DESCRIBING_SELF: [description_conv], STOPPING: [CommandHandler("start", start)], }, fallbacks=[CommandHandler("stop", stop)], ) application.add_handler(conv_handler) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/passportbot.html000066400000000000000000000021131460724040100225540ustar00rootroot00000000000000 Telegram passport test!

Telegram passport test

python-telegram-bot-21.1.1/examples/passportbot.py000066400000000000000000000105771460724040100222550ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ Simple Bot to print/download all incoming passport data See https://telegram.org/blog/passport for info about what telegram passport is. See https://github.com/python-telegram-bot/python-telegram-bot/wiki/Telegram-Passport for how to use Telegram Passport properly with python-telegram-bot. Note: To use Telegram Passport, you must install PTB via `pip install "python-telegram-bot[passport]"` """ import logging from pathlib import Path from telegram import Update from telegram.ext import Application, ContextTypes, MessageHandler, filters # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) async def msg(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Downloads and prints the received passport data.""" # Retrieve passport data passport_data = update.message.passport_data # If our nonce doesn't match what we think, this Update did not originate from us # Ideally you would randomize the nonce on the server if passport_data.decrypted_credentials.nonce != "thisisatest": return # Print the decrypted credential data # For all elements # Print their decrypted data # Files will be downloaded to current directory for data in passport_data.decrypted_data: # This is where the data gets decrypted if data.type == "phone_number": print("Phone: ", data.phone_number) elif data.type == "email": print("Email: ", data.email) if data.type in ( "personal_details", "passport", "driver_license", "identity_card", "internal_passport", "address", ): print(data.type, data.data) if data.type in ( "utility_bill", "bank_statement", "rental_agreement", "passport_registration", "temporary_registration", ): print(data.type, len(data.files), "files") for file in data.files: actual_file = await file.get_file() print(actual_file) await actual_file.download_to_drive() if ( data.type in ("passport", "driver_license", "identity_card", "internal_passport") and data.front_side ): front_file = await data.front_side.get_file() print(data.type, front_file) await front_file.download_to_drive() if data.type in ("driver_license" and "identity_card") and data.reverse_side: reverse_file = await data.reverse_side.get_file() print(data.type, reverse_file) await reverse_file.download_to_drive() if ( data.type in ("passport", "driver_license", "identity_card", "internal_passport") and data.selfie ): selfie_file = await data.selfie.get_file() print(data.type, selfie_file) await selfie_file.download_to_drive() if data.translation and data.type in ( "passport", "driver_license", "identity_card", "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration", "temporary_registration", ): print(data.type, len(data.translation), "translation") for file in data.translation: actual_file = await file.get_file() print(actual_file) await actual_file.download_to_drive() def main() -> None: """Start the bot.""" # Create the Application and pass it your token and private key private_key = Path("private.key") application = ( Application.builder().token("TOKEN").private_key(private_key.read_bytes()).build() ) # On messages that include passport data call msg application.add_handler(MessageHandler(filters.PASSPORT_DATA, msg)) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/paymentbot.py000066400000000000000000000133671460724040100220570ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """Basic example for a bot that can receive payment from user.""" import logging from telegram import LabeledPrice, ShippingOption, Update from telegram.ext import ( Application, CommandHandler, ContextTypes, MessageHandler, PreCheckoutQueryHandler, ShippingQueryHandler, filters, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) PAYMENT_PROVIDER_TOKEN = "PAYMENT_PROVIDER_TOKEN" async def start_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Displays info on how to use the bot.""" msg = ( "Use /shipping to get an invoice for shipping-payment, or /noshipping for an " "invoice without shipping." ) await update.message.reply_text(msg) async def start_with_shipping_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Sends an invoice with shipping-payment.""" chat_id = update.message.chat_id title = "Payment Example" description = "Payment Example using python-telegram-bot" # select a payload just for you to recognize its the donation from your bot payload = "Custom-Payload" # In order to get a provider_token see https://core.telegram.org/bots/payments#getting-a-token currency = "USD" # price in dollars price = 1 # price * 100 so as to include 2 decimal points # check https://core.telegram.org/bots/payments#supported-currencies for more details prices = [LabeledPrice("Test", price * 100)] # optionally pass need_name=True, need_phone_number=True, # need_email=True, need_shipping_address=True, is_flexible=True await context.bot.send_invoice( chat_id, title, description, payload, PAYMENT_PROVIDER_TOKEN, currency, prices, need_name=True, need_phone_number=True, need_email=True, need_shipping_address=True, is_flexible=True, ) async def start_without_shipping_callback( update: Update, context: ContextTypes.DEFAULT_TYPE ) -> None: """Sends an invoice without shipping-payment.""" chat_id = update.message.chat_id title = "Payment Example" description = "Payment Example using python-telegram-bot" # select a payload just for you to recognize its the donation from your bot payload = "Custom-Payload" # In order to get a provider_token see https://core.telegram.org/bots/payments#getting-a-token currency = "USD" # price in dollars price = 1 # price * 100 so as to include 2 decimal points prices = [LabeledPrice("Test", price * 100)] # optionally pass need_name=True, need_phone_number=True, # need_email=True, need_shipping_address=True, is_flexible=True await context.bot.send_invoice( chat_id, title, description, payload, PAYMENT_PROVIDER_TOKEN, currency, prices ) async def shipping_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Answers the ShippingQuery with ShippingOptions""" query = update.shipping_query # check the payload, is this from your bot? if query.invoice_payload != "Custom-Payload": # answer False pre_checkout_query await query.answer(ok=False, error_message="Something went wrong...") return # First option has a single LabeledPrice options = [ShippingOption("1", "Shipping Option A", [LabeledPrice("A", 100)])] # second option has an array of LabeledPrice objects price_list = [LabeledPrice("B1", 150), LabeledPrice("B2", 200)] options.append(ShippingOption("2", "Shipping Option B", price_list)) await query.answer(ok=True, shipping_options=options) # after (optional) shipping, it's the pre-checkout async def precheckout_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Answers the PreQecheckoutQuery""" query = update.pre_checkout_query # check the payload, is this from your bot? if query.invoice_payload != "Custom-Payload": # answer False pre_checkout_query await query.answer(ok=False, error_message="Something went wrong...") else: await query.answer(ok=True) # finally, after contacting the payment provider... async def successful_payment_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Confirms the successful payment.""" # do something after successfully receiving payment? await update.message.reply_text("Thank you for your payment!") def main() -> None: """Run the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() # simple start function application.add_handler(CommandHandler("start", start_callback)) # Add command handler to start the payment invoice application.add_handler(CommandHandler("shipping", start_with_shipping_callback)) application.add_handler(CommandHandler("noshipping", start_without_shipping_callback)) # Optional handler if your product requires shipping application.add_handler(ShippingQueryHandler(shipping_callback)) # Pre-checkout handler to final check application.add_handler(PreCheckoutQueryHandler(precheckout_callback)) # Success! Notify your user! application.add_handler( MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_payment_callback) ) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/persistentconversationbot.py000066400000000000000000000136201460724040100252250ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ First, a few callback functions are defined. Then, those functions are passed to the Application and registered at their respective places. Then, the bot is started and runs until we press Ctrl-C on the command line. Usage: Example of a bot-user conversation using ConversationHandler. Send /start to initiate the conversation. Press Ctrl-C on the command line or send a signal to the process to stop the bot. """ import logging from typing import Dict from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update from telegram.ext import ( Application, CommandHandler, ContextTypes, ConversationHandler, MessageHandler, PicklePersistence, filters, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) CHOOSING, TYPING_REPLY, TYPING_CHOICE = range(3) reply_keyboard = [ ["Age", "Favourite colour"], ["Number of siblings", "Something else..."], ["Done"], ] markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) def facts_to_str(user_data: Dict[str, str]) -> str: """Helper function for formatting the gathered user info.""" facts = [f"{key} - {value}" for key, value in user_data.items()] return "\n".join(facts).join(["\n", "\n"]) async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Start the conversation, display any stored data and ask user for input.""" reply_text = "Hi! My name is Doctor Botter." if context.user_data: reply_text += ( f" You already told me your {', '.join(context.user_data.keys())}. Why don't you " "tell me something more about yourself? Or change anything I already know." ) else: reply_text += ( " I will hold a more complex conversation with you. Why don't you tell me " "something about yourself?" ) await update.message.reply_text(reply_text, reply_markup=markup) return CHOOSING async def regular_choice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Ask the user for info about the selected predefined choice.""" text = update.message.text.lower() context.user_data["choice"] = text if context.user_data.get(text): reply_text = ( f"Your {text}? I already know the following about that: {context.user_data[text]}" ) else: reply_text = f"Your {text}? Yes, I would love to hear about that!" await update.message.reply_text(reply_text) return TYPING_REPLY async def custom_choice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Ask the user for a description of a custom category.""" await update.message.reply_text( 'Alright, please send me the category first, for example "Most impressive skill"' ) return TYPING_CHOICE async def received_information(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Store info provided by user and ask for the next category.""" text = update.message.text category = context.user_data["choice"] context.user_data[category] = text.lower() del context.user_data["choice"] await update.message.reply_text( "Neat! Just so you know, this is what you already told me:" f"{facts_to_str(context.user_data)}" "You can tell me more, or change your opinion on something.", reply_markup=markup, ) return CHOOSING async def show_data(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Display the gathered info.""" await update.message.reply_text( f"This is what you already told me: {facts_to_str(context.user_data)}" ) async def done(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Display the gathered info and end the conversation.""" if "choice" in context.user_data: del context.user_data["choice"] await update.message.reply_text( f"I learned these facts about you: {facts_to_str(context.user_data)}Until next time!", reply_markup=ReplyKeyboardRemove(), ) return ConversationHandler.END def main() -> None: """Run the bot.""" # Create the Application and pass it your bot's token. persistence = PicklePersistence(filepath="conversationbot") application = Application.builder().token("TOKEN").persistence(persistence).build() # Add conversation handler with the states CHOOSING, TYPING_CHOICE and TYPING_REPLY conv_handler = ConversationHandler( entry_points=[CommandHandler("start", start)], states={ CHOOSING: [ MessageHandler( filters.Regex("^(Age|Favourite colour|Number of siblings)$"), regular_choice ), MessageHandler(filters.Regex("^Something else...$"), custom_choice), ], TYPING_CHOICE: [ MessageHandler( filters.TEXT & ~(filters.COMMAND | filters.Regex("^Done$")), regular_choice ) ], TYPING_REPLY: [ MessageHandler( filters.TEXT & ~(filters.COMMAND | filters.Regex("^Done$")), received_information, ) ], }, fallbacks=[MessageHandler(filters.Regex("^Done$"), done)], name="my_conversation", persistent=True, ) application.add_handler(conv_handler) show_data_handler = CommandHandler("show_data", show_data) application.add_handler(show_data_handler) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/pollbot.py000066400000000000000000000145531460724040100213460ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ Basic example for a bot that works with polls. Only 3 people are allowed to interact with each poll/quiz the bot generates. The preview command generates a closed poll/quiz, exactly like the one the user sends the bot """ import logging from telegram import ( KeyboardButton, KeyboardButtonPollType, Poll, ReplyKeyboardMarkup, ReplyKeyboardRemove, Update, ) from telegram.constants import ParseMode from telegram.ext import ( Application, CommandHandler, ContextTypes, MessageHandler, PollAnswerHandler, PollHandler, filters, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) TOTAL_VOTER_COUNT = 3 async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Inform user about what this bot can do""" await update.message.reply_text( "Please select /poll to get a Poll, /quiz to get a Quiz or /preview" " to generate a preview for your poll" ) async def poll(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Sends a predefined poll""" questions = ["Good", "Really good", "Fantastic", "Great"] message = await context.bot.send_poll( update.effective_chat.id, "How are you?", questions, is_anonymous=False, allows_multiple_answers=True, ) # Save some info about the poll the bot_data for later use in receive_poll_answer payload = { message.poll.id: { "questions": questions, "message_id": message.message_id, "chat_id": update.effective_chat.id, "answers": 0, } } context.bot_data.update(payload) async def receive_poll_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Summarize a users poll vote""" answer = update.poll_answer answered_poll = context.bot_data[answer.poll_id] try: questions = answered_poll["questions"] # this means this poll answer update is from an old poll, we can't do our answering then except KeyError: return selected_options = answer.option_ids answer_string = "" for question_id in selected_options: if question_id != selected_options[-1]: answer_string += questions[question_id] + " and " else: answer_string += questions[question_id] await context.bot.send_message( answered_poll["chat_id"], f"{update.effective_user.mention_html()} feels {answer_string}!", parse_mode=ParseMode.HTML, ) answered_poll["answers"] += 1 # Close poll after three participants voted if answered_poll["answers"] == TOTAL_VOTER_COUNT: await context.bot.stop_poll(answered_poll["chat_id"], answered_poll["message_id"]) async def quiz(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Send a predefined poll""" questions = ["1", "2", "4", "20"] message = await update.effective_message.reply_poll( "How many eggs do you need for a cake?", questions, type=Poll.QUIZ, correct_option_id=2 ) # Save some info about the poll the bot_data for later use in receive_quiz_answer payload = { message.poll.id: {"chat_id": update.effective_chat.id, "message_id": message.message_id} } context.bot_data.update(payload) async def receive_quiz_answer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Close quiz after three participants took it""" # the bot can receive closed poll updates we don't care about if update.poll.is_closed: return if update.poll.total_voter_count == TOTAL_VOTER_COUNT: try: quiz_data = context.bot_data[update.poll.id] # this means this poll answer update is from an old poll, we can't stop it then except KeyError: return await context.bot.stop_poll(quiz_data["chat_id"], quiz_data["message_id"]) async def preview(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Ask user to create a poll and display a preview of it""" # using this without a type lets the user chooses what he wants (quiz or poll) button = [[KeyboardButton("Press me!", request_poll=KeyboardButtonPollType())]] message = "Press the button to let the bot generate a preview for your poll" # using one_time_keyboard to hide the keyboard await update.effective_message.reply_text( message, reply_markup=ReplyKeyboardMarkup(button, one_time_keyboard=True) ) async def receive_poll(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """On receiving polls, reply to it by a closed poll copying the received poll""" actual_poll = update.effective_message.poll # Only need to set the question and options, since all other parameters don't matter for # a closed poll await update.effective_message.reply_poll( question=actual_poll.question, options=[o.text for o in actual_poll.options], # with is_closed true, the poll/quiz is immediately closed is_closed=True, reply_markup=ReplyKeyboardRemove(), ) async def help_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Display a help message""" await update.message.reply_text("Use /quiz, /poll or /preview to test this bot.") def main() -> None: """Run bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("poll", poll)) application.add_handler(CommandHandler("quiz", quiz)) application.add_handler(CommandHandler("preview", preview)) application.add_handler(CommandHandler("help", help_handler)) application.add_handler(MessageHandler(filters.POLL, receive_poll)) application.add_handler(PollAnswerHandler(receive_poll_answer)) application.add_handler(PollHandler(receive_quiz_answer)) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/rawapibot.py000066400000000000000000000044041460724040100216550ustar00rootroot00000000000000#!/usr/bin/env python """Simple Bot to reply to Telegram messages. This is built on the API wrapper, see echobot.py to see the same example built on the telegram.ext bot framework. This program is dedicated to the public domain under the CC0 license. """ import asyncio import contextlib import logging from typing import NoReturn from telegram import Bot, Update from telegram.error import Forbidden, NetworkError logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) async def main() -> NoReturn: """Run the bot.""" # Here we use the `async with` syntax to properly initialize and shutdown resources. async with Bot("TOKEN") as bot: # get the first pending update_id, this is so we can skip over it in case # we get a "Forbidden" exception. try: update_id = (await bot.get_updates())[0].update_id except IndexError: update_id = None logger.info("listening for new messages...") while True: try: update_id = await echo(bot, update_id) except NetworkError: await asyncio.sleep(1) except Forbidden: # The user has removed or blocked the bot. update_id += 1 async def echo(bot: Bot, update_id: int) -> int: """Echo the message the user sent.""" # Request updates after the last update_id updates = await bot.get_updates(offset=update_id, timeout=10, allowed_updates=Update.ALL_TYPES) for update in updates: next_update_id = update.update_id + 1 # your bot can receive updates without messages # and not all messages contain text if update.message and update.message.text: # Reply to the message logger.info("Found message %s!", update.message.text) await update.message.reply_text(update.message.text) return next_update_id return update_id if __name__ == "__main__": with contextlib.suppress(KeyboardInterrupt): # Ignore exception when Ctrl-C is pressed asyncio.run(main()) python-telegram-bot-21.1.1/examples/timerbot.py000066400000000000000000000073651460724040100215230ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ Simple Bot to send timed Telegram messages. This Bot uses the Application class to handle the bot and the JobQueue to send timed messages. First, a few handler functions are defined. Then, those functions are passed to the Application and registered at their respective places. Then, the bot is started and runs until we press Ctrl-C on the command line. Usage: Basic Alarm Bot example, sends a message after a set time. Press Ctrl-C on the command line or send a signal to the process to stop the bot. Note: To use the JobQueue, you must install PTB via `pip install "python-telegram-bot[job-queue]"` """ import logging from telegram import Update from telegram.ext import Application, CommandHandler, ContextTypes # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # Define a few command handlers. These usually take the two arguments update and # context. # Best practice would be to replace context with an underscore, # since context is an unused local variable. # This being an example and not having context present confusing beginners, # we decided to have it present as context. async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Sends explanation on how to use the bot.""" await update.message.reply_text("Hi! Use /set to set a timer") async def alarm(context: ContextTypes.DEFAULT_TYPE) -> None: """Send the alarm message.""" job = context.job await context.bot.send_message(job.chat_id, text=f"Beep! {job.data} seconds are over!") def remove_job_if_exists(name: str, context: ContextTypes.DEFAULT_TYPE) -> bool: """Remove job with given name. Returns whether job was removed.""" current_jobs = context.job_queue.get_jobs_by_name(name) if not current_jobs: return False for job in current_jobs: job.schedule_removal() return True async def set_timer(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Add a job to the queue.""" chat_id = update.effective_message.chat_id try: # args[0] should contain the time for the timer in seconds due = float(context.args[0]) if due < 0: await update.effective_message.reply_text("Sorry we can not go back to future!") return job_removed = remove_job_if_exists(str(chat_id), context) context.job_queue.run_once(alarm, due, chat_id=chat_id, name=str(chat_id), data=due) text = "Timer successfully set!" if job_removed: text += " Old one was removed." await update.effective_message.reply_text(text) except (IndexError, ValueError): await update.effective_message.reply_text("Usage: /set ") async def unset(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Remove the job if the user changed their mind.""" chat_id = update.message.chat_id job_removed = remove_job_if_exists(str(chat_id), context) text = "Timer successfully cancelled!" if job_removed else "You have no active timer." await update.message.reply_text(text) def main() -> None: """Run bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() # on different commands - answer in Telegram application.add_handler(CommandHandler(["start", "help"], start)) application.add_handler(CommandHandler("set", set_timer)) application.add_handler(CommandHandler("unset", unset)) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/examples/webappbot.html000066400000000000000000000027321460724040100221660ustar00rootroot00000000000000 python-telegram-bot Example WebApp
python-telegram-bot-21.1.1/examples/webappbot.py000066400000000000000000000050301460724040100216440ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=unused-argument # This program is dedicated to the public domain under the CC0 license. """ Simple example of a Telegram WebApp which displays a color picker. The static website for this website is hosted by the PTB team for your convenience. Currently only showcases starting the WebApp via a KeyboardButton, as all other methods would require a bot token. """ import json import logging from telegram import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, Update, WebAppInfo from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # set higher logging level for httpx to avoid all GET and POST requests being logged logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # Define a `/start` command handler. async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Send a message with a button that opens a the web app.""" await update.message.reply_text( "Please press the button below to choose a color via the WebApp.", reply_markup=ReplyKeyboardMarkup.from_button( KeyboardButton( text="Open the color picker!", web_app=WebAppInfo(url="https://python-telegram-bot.org/static/webappbot"), ) ), ) # Handle incoming WebAppData async def web_app_data(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Print the received data and remove the button.""" # Here we use `json.loads`, since the WebApp sends the data JSON serialized string # (see webappbot.html) data = json.loads(update.effective_message.web_app_data.data) await update.message.reply_html( text=( f"You selected the color with the HEX value {data['hex']}. The " f"corresponding RGB value is {tuple(data['rgb'].values())}." ), reply_markup=ReplyKeyboardRemove(), ) def main() -> None: """Start the bot.""" # Create the Application and pass it your bot's token. application = Application.builder().token("TOKEN").build() application.add_handler(CommandHandler("start", start)) application.add_handler(MessageHandler(filters.StatusUpdate.WEB_APP_DATA, web_app_data)) # Run the bot until the user presses Ctrl-C application.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() python-telegram-bot-21.1.1/public_keys/000077500000000000000000000000001460724040100200045ustar00rootroot00000000000000python-telegram-bot-21.1.1/public_keys/v12.5-v20.0b0.gpg000066400000000000000000000077001460724040100222470ustar00rootroot00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- mQINBF6AWOIBEAClMRtGdbm2yKAo1GdeybmasMpsCBmGI952/z5VvjSgDlu+ktPJ QauWkARWrq4Eab2uZEIQrHUw01+9/Ugfu7qHu4safZjc177Tq4Fp0hJDHFRFVdqb 68Rv5mH0xjr7At0eNys2Rx8m7H1dBYQCz4aroUQ0q0TB4qSeKU9FzUzOZZ7/pYri KFyNdaIjhrBeY/WXbn+7L2/cFJtafJHqkZlyfIgQzpmONfIrtJLnG0nxdBPIxg3m CYxzrJKCkPPrl8b4TNV0LCO9c4r8zXGiZ6ZOQMAGjgb2Do67AvHTLI3YYd8+iY25 112noIFASabxbW9jBb+nlyMc8nz5kW19JWq6sBfMJYzWaVpI6ofNpiZtKWupIt2V 4GcViZmE/Kh4hBQjrBIAeqLUz6ABidERrhWu6/wP3huNe+R6eMQTfEdYlYeD3LXG 5Upl46/wnodWQeW4llMVBhjkS+5ExtvlVBD9Yz8iQMUsXUSu8+UPRgjy6YIRsvtF bzGKFXkn4tFWCyQsXJDf8f+lQCtB1XF/DN2IVZF9OjQOvrpRwJk3h3CiQboN5lf7 Mj7I11MusPKzn34vg+3f6loOOQ++GUa+WH2B6Dx6AH3C0BI0V2sh6axu6HYJNo3o stOXzhhWysPKp9pj41fmaFTcF8bYYlnHoLA17IapttsYo0WuFaBF5UW90QARAQAB tCpIaW5yaWNoIE1haGxlciA8aGlucmljaC5tYWhsZXJAZnJlZW5ldC5kZT6JAk4E EwEKADgWIQRlW7T1bNsODkUAz1cung4SfvPygwUCXoBY4gIbAwULCQgHAgYVCgkI CwIEFgIDAQIeAQIXgAAKCRAung4SfvPyg3fXD/9IMuDgYGl+T98JvN/VHLcrA9rT idIWr5vDSxjniQaYe8Kc3oTqje1xmRcIR7z3WFKgxErx8yEf3AOUCPqQh53MjS0q rXNDCoRWqlljpeY2EHZh2xPSvin2pUgOd334BRFeBqyvIfb/IQmwUNDoUp1FxKEq 4veT4XPDnKZlZs/OwRMD1f0v64mtTy60fQzJyM9ICLWzZdCKZULGTKCBLms63l0h wqznU6RA7qEpKeLEwwZs454maPdDraOebyhBYqf2EbATo1Rf1faqNzjL2T7KtyZg PB1RDah/tCrsRi2ueFbOakyDms+98Km94lC8cZ0Wp7wlXaBLQHbRgylt49dN5KLP cgjUxnBDOxM/4iCrwLQ3GxqZksOkwuzkZE3v1BkRy51XPRiaEov+57zhua686BzY /rQjG6xWEawmodydUPd7qZ8LbO0bGpw+euqVd+FDEmHi4ytB1vyZMQ3Zt7gZjbBm Wxd0x4TSKz0CQ4XuGxXvVx0ZXv3zbOXE2g06UmxijHbCwNZQazAzOw5EVaZiZPvK 4HzxssBskp49xh+8T8Pgmc085ujstW2+b1XiZcqnVhIkOqEynxKhPghP/5ODtY7/ 13dkqbVgAfTZ8JU6Q+jTo5bhaoFohnb8XeEg7O8aLf06WpKvorJ+O8XGKICn1tnD odL5XTa9xX8WMwiI3bQ9SGlucmljaCBNYWhsZXIgPDIyMzY2NTU3K0JpYm8tSm9z aGlAdXNlcnMubm9yZXBseS5naXRodWIuY29tPokCTgQTAQoAOBYhBGVbtPVs2w4O RQDPVy6eDhJ+8/KDBQJjm4maAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJ EC6eDhJ+8/KDKTcP/ja9/nVePimncHGvrT70Ek53qBheSTujurDtNE6mfUOxOgr8 7m/fcZo3j/3FUssN81ijoNthS6HuhE0kyc+jbL1q4cv9oOeqgniV0P2GxlCOLwrJ k3BjLNA04P8qSXfS1eefyRgr+dtNaOw2ARetOZm3yja34EW8II6p7Bd7DTRoGOAQ cwi0Xz0AGyz9drLS0vDZ3VJfB0xIUToS/I/PBjUsIg0eD9M2bEOznFHK422CQMJV r4pdHJgMfzX7XmwqMQP31oz8zNljsLWYZqpMR9Lw3B2XKziaZlptXgwC9uulcojG lZv1OTXFKudjKmuOgzd9tW0nwMX/XnNGC7rzCpJCi5//8LeqbLxFqHoK8fXGs/9k U7EBd3srEnGZHp3ROCsEGmNERTTIIN/rWS3lywnQCQ5kyk8HjT3WztfyLuAIiU+q fiGQvvkehFF31ONa6tA5Xsm/0ch8rFx9ON8K72KY0fHLJ4FMz0rIdUa47GDYKDAr n8q+drVCGBVSOWqHYhSZ5C/NCtRr6TebGCsxmhsVuqcXpJSM8CqG8f2xYeQeRw54 bGSrCGMB5TlcMUKQ9wmQWHDHa+khobvYrGBtZaHMt/exnBYqwPna88rp1pWyhIqw bBiCzKPYOUvzW+nUL0ogCVTfq5XKMJAQh1d1FPPbSLIb/0LYSHC4FavvrmJ3uQIN BF6AWOIBEADUaTuRBlOcwBBZAFYHgXNM22+TdU+JcYVbdn96A0NPJy0z91SLHotM w8fr9Sbd/g+xhG8byeFDpdEwTz+sH3GN3SjaSRV3CYIueWApiAFC4ybIoR46kcri DQJWaTL8YtofBQs7GTtmZ/IbKaxRC/nKXalBnUh3nPgWCLUcIu8axmloBfHP9zFz +Vn37JrClM66V2TLeJp5/C3Sw2lR2Dj13G65LkSAOX+naoL//PWSSmnfiDef2DA6 Ucd3vh2BoTVkmZoDeLkIP93MfDM23I7NRq9rQSptcvYmtdxo3f3IU25sfjYBi+3g HIWzbFq2SBeGAbWBufcFkj+kvoP4LzxJ63hVhpA0ql3WWgi5eKOeuhtnBSNaejhC Axq8ktdAFUpyLMjA53bMUnD7Xkb09tMwW0OcUFPxVz3LG7TT8aQSwA46u/nBFIDN 4D5R14OdAysLHcVp536NSApfvzc/E5OCIx+0TPihq+8EnSEyHMvNXA0QLeGcSn8a CoIjRK9p7IpKknGNCBx/H54rHGWi0XdPZPNbGHGFypualEB2uIRELFFkCquO2DTi rYnrYM2Xf/u1ahp1vLNGgM1F6L9u+mC6gUUMGmyBklsz1jTGkTB4HmiMDLBDhAQM /5UPJQhAdxuZwdfyveGxWYBJSSMtGbR+yOfT7NuTEhtBj0WG4h6tPwARAQABiQI2 BBgBCgAgFiEEZVu09WzbDg5FAM9XLp4OEn7z8oMFAl6AWOICGwwACgkQLp4OEn7z 8oOZ1g/+NE9vZSoxB9yw8uLRthjUUScES2wpA4VBVgpcA0QNEqtq5xE82xXUMhIb 1td6E00+lESoXGo66xoIFQdWEMyr0Df6kfFSTgh2FL4DA5NSSQgiM9u4O8yayu2x AmBQzRz42V3AquVSH7gu/WhwPcYROq+gYaQT7D/l6Mg/mRUfMS/zvkFIsTJktp+q yvcuf8ybqoCetDbOw6Uo12IjfxqT1pj8gUVrN/ePlI3HXDzOS6bCiYCgRjCU/Ujj zcXOupKjA4mvN6LCslgwREciZgDE5W31Ghnjd3tT4OL9TSixugRFD67jr7ZsXpcl ZReF+nbEsTZlKMXKVDnvwgvrVD4BolSj9rBpPcyftcZtFeuBPSVUE1c6pd5Vo7HJ I+QDHQA32/hk8dDdqrdVHCn24+yZUkpkuzvNovVJINqt/4GgIRUZot0e5MupFuj9 D+Nn90ffKvFe1YjmRkJ1hblHG9Lh2I4LQOjF2qGKRL+CmtgA3NbO4BKLuTEAJnJz PeR4FOX756R40p9Z7e0XhUqF4D/GvRyfxYhxkMKbIGEXbfgPjcNvVCDJxElBBtMz mu6lniNOz/rViivD28dWHIro3ScaGsB4vAo2Eq+4VWTUtvoPo0OHbfX8PDbpcTuU 3a3DN3/RD8DQe+Pm5clbVCpDHzKThq9TpHFC0Iozh6d7QY+hQac= =VeuH -----END PGP PUBLIC KEY BLOCK----- python-telegram-bot-21.1.1/public_keys/v20.0-current.gpg000066400000000000000000000062441460724040100227360ustar00rootroot00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- mQINBGOgNtUBEAC9CnrQYcKeHOVQatup9batI7lOSoXhqnS4v41LmWKt2GgVpiSn CpWre04UMg+s/oCFto6af97vGxSsGbb2b23mTVHECvUoUBsiM2CMHopvNpMiPG3s 85gIaqQPOyVorET+MgPw96N+Ik/8sevOTb7u6+tUP4wdZcplVXBo5q+3EeK5GDb3 g+WNYbcwZ+x+3jwgphQmryHAXAis4PrdAfuaAXxclN/prJ23245IaAQxWID1Mcxi ZaT/vZSqDEKbjj3wyzlJBfe5DuAG2IqAqEbWZCYUjjdUrsrGVdjh6Al1CqA+V8px PYCF+qK6c3M7ilbQSXv9dDjiH1shM7DPrXop657zFandVrmaFVcf7/WLLNSX0KwM kuYdFXYQXXoptpLsB6edB8Od39l6uETObDDVXiKQdeBypFlkWaqDv2soBY04+o0h NvQBWT3EPS2zUXGwo7j2pd1Q4j1qC0ceKW64rxF8UqtZXOG2mJrVKeKvbprhq3kF 3+vXHqTgr6ZgjplHkVZmcv31UOBlwGnN+xBi5ooD/brF0rxZ9Pcr0BXdtBhG80V+ q1vxVh9D2K0RFJ7pLaxFdExZoevOwHdtwT411I9VfWdICc3w9KYnH4xSwjoR+Cm2 Jy9aTlft+TkQXfOxWD83QUtu/zE/MYwp2xhHLsJoOTzSLHcRHMOWC4yfrQARAQAB tG9IaW5yaWNoIE1haGxlciAoS2V5IGZvciBzaWduaW5nIHJlbGVhc2VzIG9mIHB5 dGhvbi10ZWxlZ3JhbS1ib3QpIDwyMjM2NjU1NytCaWJvLUpvc2hpQHVzZXJzLm5v cmVwbHkuZ2l0aHViLmNvbT6JAk4EEwEKADgWIQRMulGIRwROKJVIvZ+iuYSpBzAi sgUCY6A21QIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCiuYSpBzAisgG+ EACXEqRSzuMI/Ca4vs9Q0teaUskjDbmb3tpt/vlPtgtWVBxIwixIrBXXt6VF58pc 4PjnmWd0TQVWHXysVtV1JBdFHJPf7CXD1OXAzDYLS3s0gZJ8Y32oG+94FKVtdcjp M9XV2ip0DZFd0k8ln0bVultFPR441Q7DVpJFctAvICvccC//TkgD0vMWAV+zKD/9 xctkIa1t0ZQ2xHA+NPDTLEGzs9rADwJTK/OOE528mo8ki37RTM7Ou6kGF9Xb+A5P iaO2FBDp3b6QyprMvaOF2Xgt18h+wembzsXwVwep3OpjZHYsaImoWn6821HmsS7H MTjR71aG6BrGZTjFtsWNd9Anu2aYfyZgpZZmeTMC6o81x428YEA9Mr7+yAgFAfd7 BTyShNsyP1LcbMpPoYciFPiL9ieMqfNPOpdluV56Xb4W27/EJ39wx+PQPEs/5A2n RUfKdtJtqxrGGOWtoLORS0pr+s7JS6WP1dfR/VWeTMiVNG506crAvCkN7VvTlpI0 ASqnRvFeu82cgOiP8toZJ/seRDq5JdRy/dV6EMD442mE6si7RUFJ3ShQsAMdukGw lCDdUfLATJ/rrMCsoaInIbqWBa49OqEKcpOxyR2qR/91wxS1LDhfkmtumGgIxkjj utt2XKLSaLNaXIPjX19RMqmFntzMrU2Mk+0HD40dd30bTbkCDQRjoDbVARAA1tN/ R2PhQ++bbLCtqeIjmkx/k5nmLK5P52lOu1zR142XVDC1ByZ8FEMzgnU+n660HN2g 62/p7lrMi1I12RuPZJBvalsRfGVx2DtbX06AEEg8bjABXB48uF1xU35gLOt1nhDp bEinY0Kx4uBQcXlKme83ceyG2vmZvW0C+2JyLFFjwpUvOwzlzjSMkBFdbDzMBL93 A8gTt36WxcA+IbZlEwYte6w8FE5HcEZgGXsYXCvKEEN2wn3scXg+1R8bXOyQeRCo KqblMDRO6Qsb/QL3a86so7MBWX4VmW+7714kaQYhMC+DKZq5u7yO//UqnEElHrYw W8T5KGCYTizXcrM+4giAecdeukdPpeLwOmLhKQ1YfwWkHolsruwjg15vW4QE85dV LRSTdgH1Is6NCqweOMIozH6uIAYz3b1u1r3C9hlgMqFN1oujI12MjOURLtLUtrN/ LeosSZEUgL3T7a1bmO4ykcVvhWvGrcU0HfR/CX5nXMymhkjGk2x47elottK/O3BP evinQWPSiF+ABH3605KZQB2cWcr0oH1rLMAIwcYtMdCmGJY9we1fRF2lf/BTcoui nDqsuuIJJpHg9FIIR167fW1tQH0DYbQBvsDCG6jv5pZ7V4Rl8zJ30Am5h1XZzpIk /3wPvouskdfsEF1CahpzKF/rdtIB5aaynWO0xHkAEQEAAYkCNgQYAQoAIBYhBEy6 UYhHBE4olUi9n6K5hKkHMCKyBQJjoDbVAhsMAAoJEKK5hKkHMCKypCcP/3pvvpP/ axelSKUgdsmU+mcUK4AHWtPXZxJcLoIGhMEOiNOdeX64XhHYGO0gKDvPn1Bv/ygI ef49+NGAXWZmaSOqXSKfSWaWwQqlGLuqL76Hes5EqTbAlo4qEEbZoWx/x52zbXz6 1fbcH+rU+3dYVFIzvq2UG2t3L6v+PQXoZL20CWxIAWLGjVmnUPV/7/XJJATg2x1J IEb7iG9h8XLvC4/+whRWG5J48kPRRPE3vy1kgeUtvHCIsvujoFCosAxRhLR62+5x F4rREUyY2WKAsuymVCJPEKE/g3LoTtn+f78ucECm6MJNhJpZqdjlqib0mGhjWGeR XPNHL3iEL4wFlU/DRt25GE7YKTHoPFcjet4wWOUdj9OHdV2DHgsgETEAVxaFwH7W hnPBtyc9TDLsTtJ1NphxofSiehsesNrfX5sneQVBpUclb5HS8vYqJxLC0KLOulNU emDwYaQdM4chy1QfKHJhj9uSbqRcoUcms59mLINUlTmdwez2Vbmnh8sLYfuz2S0L 7OANYTUOOmwlw9cQJjF2hgtgvn7lRK/zD38nVesHSBU4dmlYISV5/jnByhIDaIdo Afq07AF3OUKAI2FfAquDjV2P66GS1K2cWEQAAd3wgze3Qlt7xvifGTxHEXXesfP3 toI3+U6jDIfsuoL4ynzcPdz0IsFwu+yX6bxw =gtS+ -----END PGP PUBLIC KEY BLOCK----- python-telegram-bot-21.1.1/pyproject.toml000066400000000000000000000071461460724040100204170ustar00rootroot00000000000000# BLACK: [tool.black] line-length = 99 target-version = ['py38', 'py39', 'py310', 'py311'] # ISORT: [tool.isort] # black config profile = "black" line_length = 99 # RUFF: [tool.ruff] line-length = 99 target-version = "py38" show-fixes = true [tool.ruff.lint] preview = true explicit-preview-rules = true 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"] # Add "FURB" after it's out of preview [tool.ruff.lint.per-file-ignores] "tests/*.py" = ["B018"] "tests/**.py" = ["RUF012", "ASYNC101"] "docs/**.py" = ["INP001"] # 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 -rsxX" 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:" ] python-telegram-bot-21.1.1/requirements-all.txt000066400000000000000000000001421460724040100215220ustar00rootroot00000000000000-r requirements.txt -r requirements-dev.txt -r requirements-opts.txt -r docs/requirements-docs.txtpython-telegram-bot-21.1.1/requirements-dev.txt000066400000000000000000000006511460724040100215350ustar00rootroot00000000000000pre-commit # needed for pre-commit hooks in the git commit command # For the test suite pytest==7.4.4 pytest-asyncio==0.21.1 # needed because pytest doesn't come with native support for coroutines as tests pytest-xdist==3.5.0 # xdist runs tests in parallel flaky # Used for flaky tests (flaky decorator) beautifulsoup4 # used in test_official for parsing tg docs wheel # required for building the wheels for releases python-telegram-bot-21.1.1/requirements-opts.txt000066400000000000000000000017551460724040100217520ustar00rootroot00000000000000# Format: # package_name==version # req-1, req-2, req-3!ext # `pip install ptb-raw[req-1/2]` will install `package_name` # `pip install ptb[req-1/2/3]` will also install `package_name` # 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 httpx[socks] # socks httpx[http2] # http2 cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=39.0.1 # passport aiolimiter~=1.1.0 # rate-limiter!ext # tornado is rather stable, but let's not allow the next mayor release without prior testing tornado~=6.4 # webhooks!ext # Cachetools and APS don't have a strict stability policy. # Let's be cautious for now. cachetools~=5.3.3 # callback-data!ext APScheduler~=3.10.4 # job-queue!ext # pytz is required by APS and just needs the lower bound due to #2120 pytz>=2018.6 # job-queue!ext python-telegram-bot-21.1.1/requirements.txt000066400000000000000000000007441460724040100207640ustar00rootroot00000000000000# 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 # httpx has no stable release yet, but we've had no stability problems since v20.0a0 either # Since there have been requests to relax the bound a bit, we allow versions < 1.0.0 httpx ~= 0.27 python-telegram-bot-21.1.1/setup.cfg000066400000000000000000000003041460724040100173110ustar00rootroot00000000000000[metadata] license_files = LICENSE, LICENSE.dual, LICENSE.lesser [flake8] max-line-length = 99 ignore = W503, W605 extend-ignore = E203, E704 exclude = setup.py, setup_raw.py docs/source/conf.py python-telegram-bot-21.1.1/setup.py000066400000000000000000000117211460724040100172070ustar00rootroot00000000000000#!/usr/bin/env python """The setup and build script for the python-telegram-bot library.""" import subprocess import sys from collections import defaultdict from pathlib import Path from typing import Any, Dict, List, Tuple from setuptools import find_packages, setup def get_requirements() -> List[str]: """Build the requirements list for this project""" requirements_list = [] with Path("requirements.txt").open(encoding="utf-8") as reqs: for install in reqs: if install.startswith("#"): continue requirements_list.append(install.strip()) return requirements_list def get_packages_requirements(raw: bool = False) -> Tuple[List[str], List[str]]: """Build the package & requirements list for this project""" reqs = get_requirements() exclude = ["tests*", "docs*"] if raw: exclude.append("telegram.ext*") packs = find_packages(exclude=exclude) return packs, reqs def get_optional_requirements(raw: bool = False) -> Dict[str, List[str]]: """Build the optional dependencies""" requirements = defaultdict(list) with Path("requirements-opts.txt").open(encoding="utf-8") as reqs: for line in reqs: effective_line = line.strip() if not effective_line or effective_line.startswith("#"): continue dependency, names = effective_line.split("#") dependency = dependency.strip() for name in names.split(","): effective_name = name.strip() if effective_name.endswith("!ext"): if raw: continue effective_name = effective_name[:-4] requirements["ext"].append(dependency) requirements[effective_name].append(dependency) requirements["all"].append(dependency) return requirements def get_setup_kwargs(raw: bool = False) -> Dict[str, Any]: """Builds a dictionary of kwargs for the setup function""" packages, requirements = get_packages_requirements(raw=raw) raw_ext = "-raw" if raw else "" readme = Path(f'README{"_RAW" if raw else ""}.rst') version_file = Path("telegram/_version.py").read_text(encoding="utf-8") first_part = version_file.split("# SETUP.PY MARKER")[0] exec(first_part) # pylint: disable=exec-used return { "script_name": f"setup{raw_ext}.py", "name": f"python-telegram-bot{raw_ext}", "version": locals()["__version__"], "author": "Leandro Toledo", "author_email": "devs@python-telegram-bot.org", "license": "LGPLv3", "url": "https://python-telegram-bot.org/", # Keywords supported by PyPI can be found at # https://github.com/pypa/warehouse/blob/aafc5185e57e67d43487ce4faa95913dd4573e14/ # warehouse/templates/packaging/detail.html#L20-L58 "project_urls": { "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", }, "download_url": f"https://pypi.org/project/python-telegram-bot{raw_ext}/", "keywords": "python telegram bot api wrapper", "description": "We have made you a wrapper you can't refuse", "long_description": readme.read_text(encoding="utf-8"), "long_description_content_type": "text/x-rst", "packages": packages, "install_requires": requirements, "extras_require": get_optional_requirements(raw=raw), "include_package_data": True, "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", ], "python_requires": ">=3.8", } def main() -> None: # If we're building, build ptb-raw as well if set(sys.argv[1:]) in [{"bdist_wheel"}, {"sdist"}, {"sdist", "bdist_wheel"}]: args = ["python", "setup_raw.py"] args.extend(sys.argv[1:]) subprocess.run(args, check=True, capture_output=True) setup(**get_setup_kwargs(raw=False)) if __name__ == "__main__": main() python-telegram-bot-21.1.1/setup_raw.py000066400000000000000000000003071460724040100200560ustar00rootroot00000000000000#!/usr/bin/env python """The setup and build script for the python-telegram-bot-raw library.""" from setuptools import setup from setup import get_setup_kwargs setup(**get_setup_kwargs(raw=True)) python-telegram-bot-21.1.1/telegram/000077500000000000000000000000001460724040100172735ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/__init__.py000066400000000000000000000356651460724040100214230ustar00rootroot00000000000000#!/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/]. """A library that provides a Python interface to the Telegram Bot API""" __author__ = "devs@python-telegram-bot.org" __all__ = ( "Animation", "Audio", "Birthdate", "Bot", "BotCommand", "BotCommandScope", "BotCommandScopeAllChatAdministrators", "BotCommandScopeAllGroupChats", "BotCommandScopeAllPrivateChats", "BotCommandScopeChat", "BotCommandScopeChatAdministrators", "BotCommandScopeChatMember", "BotCommandScopeDefault", "BotDescription", "BotName", "BotShortDescription", "BusinessConnection", "BusinessIntro", "BusinessLocation", "BusinessMessagesDeleted", "BusinessOpeningHours", "BusinessOpeningHoursInterval", "CallbackGame", "CallbackQuery", "Chat", "ChatAdministratorRights", "ChatBoost", "ChatBoostAdded", "ChatBoostRemoved", "ChatBoostSource", "ChatBoostSourceGiftCode", "ChatBoostSourceGiveaway", "ChatBoostSourcePremium", "ChatBoostUpdated", "ChatInviteLink", "ChatJoinRequest", "ChatLocation", "ChatMember", "ChatMemberAdministrator", "ChatMemberBanned", "ChatMemberLeft", "ChatMemberMember", "ChatMemberOwner", "ChatMemberRestricted", "ChatMemberUpdated", "ChatPermissions", "ChatPhoto", "ChatShared", "ChosenInlineResult", "Contact", "Credentials", "DataCredentials", "Dice", "Document", "EncryptedCredentials", "EncryptedPassportElement", "ExternalReplyInfo", "File", "FileCredentials", "ForceReply", "ForumTopic", "ForumTopicClosed", "ForumTopicCreated", "ForumTopicEdited", "ForumTopicReopened", "Game", "GameHighScore", "GeneralForumTopicHidden", "GeneralForumTopicUnhidden", "Giveaway", "GiveawayCompleted", "GiveawayCreated", "GiveawayWinners", "IdDocumentData", "InaccessibleMessage", "InlineKeyboardButton", "InlineKeyboardMarkup", "InlineQuery", "InlineQueryResult", "InlineQueryResultArticle", "InlineQueryResultAudio", "InlineQueryResultCachedAudio", "InlineQueryResultCachedDocument", "InlineQueryResultCachedGif", "InlineQueryResultCachedMpeg4Gif", "InlineQueryResultCachedPhoto", "InlineQueryResultCachedSticker", "InlineQueryResultCachedVideo", "InlineQueryResultCachedVoice", "InlineQueryResultContact", "InlineQueryResultDocument", "InlineQueryResultGame", "InlineQueryResultGif", "InlineQueryResultLocation", "InlineQueryResultMpeg4Gif", "InlineQueryResultPhoto", "InlineQueryResultVenue", "InlineQueryResultVideo", "InlineQueryResultVoice", "InlineQueryResultsButton", "InputContactMessageContent", "InputFile", "InputInvoiceMessageContent", "InputLocationMessageContent", "InputMedia", "InputMediaAnimation", "InputMediaAudio", "InputMediaDocument", "InputMediaPhoto", "InputMediaVideo", "InputMessageContent", "InputSticker", "InputTextMessageContent", "InputVenueMessageContent", "Invoice", "KeyboardButton", "KeyboardButtonPollType", "KeyboardButtonRequestChat", "KeyboardButtonRequestUsers", "LabeledPrice", "LinkPreviewOptions", "Location", "LoginUrl", "MaskPosition", "MaybeInaccessibleMessage", "MenuButton", "MenuButtonCommands", "MenuButtonDefault", "MenuButtonWebApp", "Message", "MessageAutoDeleteTimerChanged", "MessageEntity", "MessageId", "MessageOrigin", "MessageOriginChannel", "MessageOriginChat", "MessageOriginHiddenUser", "MessageOriginUser", "MessageReactionCountUpdated", "MessageReactionUpdated", "OrderInfo", "PassportData", "PassportElementError", "PassportElementErrorDataField", "PassportElementErrorFile", "PassportElementErrorFiles", "PassportElementErrorFrontSide", "PassportElementErrorReverseSide", "PassportElementErrorSelfie", "PassportElementErrorTranslationFile", "PassportElementErrorTranslationFiles", "PassportElementErrorUnspecified", "PassportFile", "PersonalDetails", "PhotoSize", "Poll", "PollAnswer", "PollOption", "PreCheckoutQuery", "ProximityAlertTriggered", "ReactionCount", "ReactionType", "ReactionTypeCustomEmoji", "ReactionTypeEmoji", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", "ReplyParameters", "ResidentialAddress", "SecureData", "SecureValue", "SentWebAppMessage", "SharedUser", "ShippingAddress", "ShippingOption", "ShippingQuery", "Sticker", "StickerSet", "Story", "SuccessfulPayment", "SwitchInlineQueryChosenChat", "TelegramObject", "TextQuote", "Update", "User", "UserChatBoosts", "UserProfilePhotos", "UsersShared", "Venue", "Video", "VideoChatEnded", "VideoChatParticipantsInvited", "VideoChatScheduled", "VideoChatStarted", "VideoNote", "Voice", "WebAppData", "WebAppInfo", "WebhookInfo", "WriteAccessAllowed", "__bot_api_version__", "__bot_api_version_info__", "__version__", "__version_info__", "constants", "error", "helpers", "request", "warnings", ) from . import _version, constants, error, helpers, request, warnings from ._birthdate import Birthdate from ._bot import Bot from ._botcommand import BotCommand from ._botcommandscope import ( BotCommandScope, BotCommandScopeAllChatAdministrators, BotCommandScopeAllGroupChats, BotCommandScopeAllPrivateChats, BotCommandScopeChat, BotCommandScopeChatAdministrators, BotCommandScopeChatMember, BotCommandScopeDefault, ) from ._botdescription import BotDescription, BotShortDescription from ._botname import BotName from ._business import ( BusinessConnection, BusinessIntro, BusinessLocation, BusinessMessagesDeleted, BusinessOpeningHours, BusinessOpeningHoursInterval, ) from ._callbackquery import CallbackQuery from ._chat import Chat from ._chatadministratorrights import ChatAdministratorRights from ._chatboost import ( ChatBoost, ChatBoostAdded, ChatBoostRemoved, ChatBoostSource, ChatBoostSourceGiftCode, ChatBoostSourceGiveaway, ChatBoostSourcePremium, ChatBoostUpdated, UserChatBoosts, ) from ._chatinvitelink import ChatInviteLink from ._chatjoinrequest import ChatJoinRequest from ._chatlocation import ChatLocation from ._chatmember import ( ChatMember, ChatMemberAdministrator, ChatMemberBanned, ChatMemberLeft, ChatMemberMember, ChatMemberOwner, ChatMemberRestricted, ) from ._chatmemberupdated import ChatMemberUpdated from ._chatpermissions import ChatPermissions from ._choseninlineresult import ChosenInlineResult from ._dice import Dice from ._files.animation import Animation from ._files.audio import Audio from ._files.chatphoto import ChatPhoto from ._files.contact import Contact from ._files.document import Document from ._files.file import File from ._files.inputfile import InputFile from ._files.inputmedia import ( InputMedia, InputMediaAnimation, InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo, ) from ._files.inputsticker import InputSticker from ._files.location import Location from ._files.photosize import PhotoSize from ._files.sticker import MaskPosition, Sticker, StickerSet from ._files.venue import Venue from ._files.video import Video from ._files.videonote import VideoNote from ._files.voice import Voice from ._forcereply import ForceReply from ._forumtopic import ( ForumTopic, ForumTopicClosed, ForumTopicCreated, ForumTopicEdited, ForumTopicReopened, GeneralForumTopicHidden, GeneralForumTopicUnhidden, ) from ._games.callbackgame import CallbackGame from ._games.game import Game from ._games.gamehighscore import GameHighScore from ._giveaway import Giveaway, GiveawayCompleted, GiveawayCreated, GiveawayWinners from ._inline.inlinekeyboardbutton import InlineKeyboardButton from ._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from ._inline.inlinequery import InlineQuery from ._inline.inlinequeryresult import InlineQueryResult from ._inline.inlinequeryresultarticle import InlineQueryResultArticle from ._inline.inlinequeryresultaudio import InlineQueryResultAudio from ._inline.inlinequeryresultcachedaudio import InlineQueryResultCachedAudio from ._inline.inlinequeryresultcacheddocument import InlineQueryResultCachedDocument from ._inline.inlinequeryresultcachedgif import InlineQueryResultCachedGif from ._inline.inlinequeryresultcachedmpeg4gif import InlineQueryResultCachedMpeg4Gif from ._inline.inlinequeryresultcachedphoto import InlineQueryResultCachedPhoto from ._inline.inlinequeryresultcachedsticker import InlineQueryResultCachedSticker from ._inline.inlinequeryresultcachedvideo import InlineQueryResultCachedVideo from ._inline.inlinequeryresultcachedvoice import InlineQueryResultCachedVoice from ._inline.inlinequeryresultcontact import InlineQueryResultContact from ._inline.inlinequeryresultdocument import InlineQueryResultDocument from ._inline.inlinequeryresultgame import InlineQueryResultGame from ._inline.inlinequeryresultgif import InlineQueryResultGif from ._inline.inlinequeryresultlocation import InlineQueryResultLocation from ._inline.inlinequeryresultmpeg4gif import InlineQueryResultMpeg4Gif from ._inline.inlinequeryresultphoto import InlineQueryResultPhoto from ._inline.inlinequeryresultsbutton import InlineQueryResultsButton from ._inline.inlinequeryresultvenue import InlineQueryResultVenue from ._inline.inlinequeryresultvideo import InlineQueryResultVideo from ._inline.inlinequeryresultvoice import InlineQueryResultVoice from ._inline.inputcontactmessagecontent import InputContactMessageContent from ._inline.inputinvoicemessagecontent import InputInvoiceMessageContent from ._inline.inputlocationmessagecontent import InputLocationMessageContent from ._inline.inputmessagecontent import InputMessageContent from ._inline.inputtextmessagecontent import InputTextMessageContent from ._inline.inputvenuemessagecontent import InputVenueMessageContent from ._keyboardbutton import KeyboardButton from ._keyboardbuttonpolltype import KeyboardButtonPollType from ._keyboardbuttonrequest import KeyboardButtonRequestChat, KeyboardButtonRequestUsers from ._linkpreviewoptions import LinkPreviewOptions from ._loginurl import LoginUrl from ._menubutton import MenuButton, MenuButtonCommands, MenuButtonDefault, MenuButtonWebApp from ._message import InaccessibleMessage, MaybeInaccessibleMessage, Message from ._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged from ._messageentity import MessageEntity from ._messageid import MessageId from ._messageorigin import ( MessageOrigin, MessageOriginChannel, MessageOriginChat, MessageOriginHiddenUser, MessageOriginUser, ) from ._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated from ._passport.credentials import ( Credentials, DataCredentials, EncryptedCredentials, FileCredentials, SecureData, SecureValue, ) from ._passport.data import IdDocumentData, PersonalDetails, ResidentialAddress from ._passport.encryptedpassportelement import EncryptedPassportElement from ._passport.passportdata import PassportData from ._passport.passportelementerrors import ( PassportElementError, PassportElementErrorDataField, PassportElementErrorFile, PassportElementErrorFiles, PassportElementErrorFrontSide, PassportElementErrorReverseSide, PassportElementErrorSelfie, PassportElementErrorTranslationFile, PassportElementErrorTranslationFiles, PassportElementErrorUnspecified, ) from ._passport.passportfile import PassportFile from ._payment.invoice import Invoice from ._payment.labeledprice import LabeledPrice from ._payment.orderinfo import OrderInfo from ._payment.precheckoutquery import PreCheckoutQuery from ._payment.shippingaddress import ShippingAddress from ._payment.shippingoption import ShippingOption from ._payment.shippingquery import ShippingQuery from ._payment.successfulpayment import SuccessfulPayment from ._poll import Poll, PollAnswer, PollOption from ._proximityalerttriggered import ProximityAlertTriggered from ._reaction import ReactionCount, ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote from ._replykeyboardmarkup import ReplyKeyboardMarkup from ._replykeyboardremove import ReplyKeyboardRemove from ._sentwebappmessage import SentWebAppMessage from ._shared import ChatShared, SharedUser, UsersShared from ._story import Story from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from ._telegramobject import TelegramObject from ._update import Update from ._user import User from ._userprofilephotos import UserProfilePhotos from ._videochat import ( VideoChatEnded, VideoChatParticipantsInvited, VideoChatScheduled, VideoChatStarted, ) from ._webappdata import WebAppData from ._webappinfo import WebAppInfo from ._webhookinfo import WebhookInfo from ._writeaccessallowed import WriteAccessAllowed #: :obj:`str`: The version of the `python-telegram-bot` library as string. #: To get detailed information about the version number, please use :data:`__version_info__` #: instead. __version__: str = _version.__version__ #: :class:`typing.NamedTuple`: A tuple containing the five components of the version number: #: `major`, `minor`, `micro`, `releaselevel`, and `serial`. #: All values except `releaselevel` are integers. #: The release level is ``'alpha'``, ``'beta'``, ``'candidate'``, or ``'final'``. #: The components can also be accessed by name, so ``__version_info__[0]`` is equivalent to #: ``__version_info__.major`` and so on. #: #: .. versionadded:: 20.0 __version_info__: _version.Version = _version.__version_info__ #: :obj:`str`: Shortcut for :const:`telegram.constants.BOT_API_VERSION`. #: #: .. versionchanged:: 20.0 #: This constant was previously named ``bot_api_version``. __bot_api_version__: str = _version.__bot_api_version__ #: :class:`typing.NamedTuple`: Shortcut for :const:`telegram.constants.BOT_API_VERSION_INFO`. #: #: .. versionadded:: 20.0 __bot_api_version_info__: constants._BotAPIVersion = _version.__bot_api_version_info__ python-telegram-bot-21.1.1/telegram/__main__.py000066400000000000000000000034341460724040100213710ustar00rootroot00000000000000# !/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 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() python-telegram-bot-21.1.1/telegram/_birthdate.py000066400000000000000000000054551460724040100217630ustar00rootroot00000000000000#!/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 datetime from typing import Optional from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict class Birthdate(TelegramObject): """ This object represents a user's birthday. 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) -> datetime: """Return the birthdate as a datetime object. Args: year (:obj:`int`, optional): The year to use. Required, if the :attr:`year` was not present. Returns: :obj:`datetime.datetime`: The birthdate as a datetime 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 datetime(year or self.year, self.month, self.day) # type: ignore[arg-type] python-telegram-bot-21.1.1/telegram/_bot.py000066400000000000000000013442711460724040100206040ustar00rootroot00000000000000#!/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._chat import Chat from telegram._chatadministratorrights import ChatAdministratorRights from telegram._chatboost import UserChatBoosts 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 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._poll import 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() return self except Exception as exc: await self.shutdown() raise exc 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: str, 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): # 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, *, 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 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 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}\", ...)'" ), PTBDeprecationWarning, 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, *, 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 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, 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.Chat.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, *, 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` | :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 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, } 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, ) 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, *, 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` | :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 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, ) 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, *, 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` | :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 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, ) 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, *, 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` | :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 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, ) 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, *, 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` | :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 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, } 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, ) 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, *, 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` | :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 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, ) 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, *, 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` | :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 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, } 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, ) 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, *, 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 (other formats may be sent as Audio or 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` | :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 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, ) 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, *, 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 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, } 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, *, 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`. 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 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, ) 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, *, 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. 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, } return await self._send_message( "editMessageLiveLocation", data, 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 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, *, read_timeout: ODVInput[float] = DEFAULT_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. 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, 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, *, 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 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, ) 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, *, 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 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, ) 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, *, 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 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, ) 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, *, 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|. .. 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. 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, 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, *, read_timeout: ODVInput[float] = DEFAULT_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| 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. 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( "editMessageCaption", data, reply_markup=reply_markup, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, 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, *, read_timeout: ODVInput[float] = DEFAULT_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| .. 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. 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, 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, *, read_timeout: ODVInput[float] = DEFAULT_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| 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. 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, 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, 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( 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.", PTBDeprecationWarning, 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.") return Update.de_list(result, self) 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`_. Note: 1. You will not be able to receive updates using :meth:`get_updates` for long as an outgoing webhook is set up. 2. To use a self-signed certificate, you need to upload your public key certificate using certificate parameter. Please upload as InputFile, sending a String will not work. 3. Ports currently supported for Webhooks: :attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS`. If you're having any trouble setting up webhooks, please check out this `guide to Webhooks`_. .. seealso:: :meth:`telegram.ext.Application.run_webhook`, :meth:`telegram.ext.Updater.start_webhook` 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, ) -> Chat: """ 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.). Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| Returns: :class:`telegram.Chat` 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 Chat.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.Chat.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.Chat.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: 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, *, 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 for your internal processes. provider_token (:obj:`str`): Payments provider token, obtained via `@BotFather `_. currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies `_. prices (Sequence[:class:`telegram.LabeledPrice`]): Price breakdown, a sequence of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.). .. 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``. .. 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. need_phone_number (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's phone number to complete the order. need_email (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's email to complete the order. need_shipping_address (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's shipping address to complete the order. send_phone_number_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's phone number should be sent to provider. send_email_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's email address should be sent to provider. is_flexible (:obj:`bool`, optional): Pass :obj:`True`, if the final price depends on the shipping method. 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 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, ) 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. .. 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` | :obj:`telegram.ChatInviteLink`): The invite link to edit. .. versionchanged:: 20.0 Now also accepts :obj:`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` | :obj:`telegram.ChatInviteLink`): The invite link to revoke. .. versionchanged:: 20.0 Now also accepts :obj:`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, *, read_timeout: ODVInput[float] = DEFAULT_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. 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, } 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, *, read_timeout: ODVInput[float] = DEFAULT_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 a message to unpin. If not specified, the most recent pinned message (by sending date) will be unpinned. 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( "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` | :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_format: Optional[str] = None, 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. 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_format (:obj:`str`): Format of stickers in the set, must be one of :attr:`~telegram.constants.StickerFormat.STATIC`, :attr:`~telegram.constants.StickerFormat.ANIMATED` or :attr:`~telegram.constants.StickerFormat.VIDEO`. .. versionadded:: 20.2 .. deprecated:: 21.1 Use :paramref:`telegram.InputSticker.format` instead. 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` """ if sticker_format is not None: warn( "The parameter `sticker_format` is deprecated. Use the parameter" " `InputSticker.format` in the parameter `stickers` instead.", stacklevel=2, category=PTBDeprecationWarning, ) data: JSONDict = { "user_id": user_id, "name": name, "title": title, "stickers": stickers, "sticker_format": sticker_format, "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` | :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[str], 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, *, 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`]): Sequence of answer options, :tg-const:`telegram.Poll.MIN_OPTION_NUMBER`- :tg-const:`telegram.Poll.MAX_OPTION_NUMBER` strings :tg-const:`telegram.Poll.MIN_OPTION_LENGTH`- :tg-const:`telegram.Poll.MAX_OPTION_LENGTH` characters each. .. versionchanged:: 20.0 |sequenceargs| 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 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": 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, } 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, ) async def stop_poll( self, chat_id: Union[int, str], message_id: int, 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: """ 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. 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, } 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, *, 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 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, ) 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 (:obj:`telegram.ChatAdministratorRights`, optional): A :obj:`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: :obj:`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 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 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, *, 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 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 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 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, } 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, 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 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: str, 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 for your internal processes. provider_token (:obj:`str`): Payments provider token, obtained via `@BotFather `_. currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies `_. prices (Sequence[:class:`telegram.LabeledPrice`)]: Price breakdown, a sequence of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.). .. 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``. 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. need_phone_number (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's phone number to complete the order. need_email (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's email address to complete the order. need_shipping_address (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's shipping address to complete the order. send_phone_number_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's phone number should be sent to provider. send_email_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's email address should be sent to provider. is_flexible (:obj:`bool`, optional): Pass :obj:`True`, if the final price depends on the shipping method. 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 :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 :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. .. 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. 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 (:obj:`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, ) def to_dict(self, recursive: bool = True) -> JSONDict: """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`""" python-telegram-bot-21.1.1/telegram/_botcommand.py000066400000000000000000000063111460724040100221300ustar00rootroot00000000000000#!/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 """ python-telegram-bot-21.1.1/telegram/_botcommandscope.py000066400000000000000000000242141460724040100231640ustar00rootroot00000000000000#!/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: "Bot") -> Optional["BotCommandScope"]: """Converts JSON data to the appropriate :class:`BotCommandScope` object, i.e. takes care of selecting the correct subclass. Args: data (Dict[:obj:`str`, ...]): The JSON data. bot (:class:`telegram.Bot`): The bot associated with this object. Returns: The Telegram object. """ data = cls._parse_data(data) if not data: return None _class_mapping: Dict[str, Type[BotCommandScope]] = { cls.DEFAULT: BotCommandScopeDefault, cls.ALL_PRIVATE_CHATS: BotCommandScopeAllPrivateChats, cls.ALL_GROUP_CHATS: BotCommandScopeAllGroupChats, cls.ALL_CHAT_ADMINISTRATORS: BotCommandScopeAllChatAdministrators, cls.CHAT: BotCommandScopeChat, cls.CHAT_ADMINISTRATORS: BotCommandScopeChatAdministrators, cls.CHAT_MEMBER: BotCommandScopeChatMember, } if cls is BotCommandScope 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) python-telegram-bot-21.1.1/telegram/_botdescription.py000066400000000000000000000047171460724040100230450ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_botname.py000066400000000000000000000034151460724040100214340ustar00rootroot00000000000000#!/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`""" python-telegram-bot-21.1.1/telegram/_business.py000066400000000000000000000366011460724040100216450ustar00rootroot00000000000000#!/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: "Bot") -> 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: "Bot") -> 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 represents the intro of a 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: "Bot") -> 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 represents the location of a 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: "Bot") -> 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 represents the time intervals describing business opening hours. 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 represents the opening hours of a business account. 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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_callbackquery.py000066400000000000000000001004021460724040100226230ustar00rootroot00000000000000#!/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 maybe 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: "Bot") -> 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, ) 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, *, read_timeout: ODVInput[float] = DEFAULT_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, ) 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, ) 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, ) 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, ) 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, *, 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, chat_id=None, message_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, ) 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, ) 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, *, 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, ) 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 """ python-telegram-bot-21.1.1/telegram/_chat.py000066400000000000000000004374651460724040100207460ustar00rootroot00000000000000#!/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._birthdate import Birthdate from telegram._chatlocation import ChatLocation from telegram._chatpermissions import ChatPermissions from telegram._files.chatphoto import ChatPhoto 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.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.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, Bot, BusinessIntro, BusinessLocation, BusinessOpeningHours, ChatInviteLink, ChatMember, Contact, Document, InlineKeyboardMarkup, InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo, LabeledPrice, LinkPreviewOptions, Location, Message, MessageEntity, MessageId, PhotoSize, ReplyParameters, Sticker, UserChatBoosts, Venue, Video, VideoNote, Voice, ) class Chat(TelegramObject): """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`. Args: id (:obj:`int`): Unique identifier for this chat. 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. 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. photo (:class:`telegram.ChatPhoto`, optional): Chat photo. Returned only in :meth:`telegram.Bot.get_chat`. bio (:obj:`str`, optional): Bio of the other party in a private chat. Returned only in :meth:`telegram.Bot.get_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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 13.9 description (:obj:`str`, optional): Description, for groups, supergroups and channel chats. Returned only in :meth:`telegram.Bot.get_chat`. invite_link (:obj:`str`, optional): Primary invite link, for groups, supergroups and channel. Returned only in :meth:`telegram.Bot.get_chat`. pinned_message (:class:`telegram.Message`, optional): The most recent pinned message (by sending date). Returned only in :meth:`telegram.Bot.get_chat`. permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`. slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between consecutive messages sent by each unprivileged user. Returned only in :meth:`telegram.Bot.get_chat`. message_auto_delete_time (:obj:`int`, optional): The time after which all messages sent to the chat will be automatically deleted; in seconds. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 13.4 has_protected_content (:obj:`bool`, optional): :obj:`True`, if messages from the chat can't be forwarded to other chats. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.8 sticker_set_name (:obj:`str`, optional): For supergroups, name of group sticker set. Returned only in :meth:`telegram.Bot.get_chat`. can_set_sticker_set (:obj:`bool`, optional): :obj:`True`, if the bot can change group the sticker set. Returned only in :meth:`telegram.Bot.get_chat`. 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. Returned only in :meth:`telegram.Bot.get_chat`. location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`. join_to_send_messages (:obj:`bool`, optional): :obj:`True`, if users need to join the supergroup before they can send messages. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 join_by_request (:obj:`bool`, optional): :obj:`True`, if all users directly joining the supergroup need to be approved by supergroup administrators. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 has_restricted_voice_and_video_messages (:obj:`bool`, optional): :obj:`True`, if the privacy settings of the other party restrict sending voice and video note messages in the private chat. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 is_forum (:obj:`bool`, optional): :obj:`True`, if the supergroup chat is a forum (has topics_ enabled). .. versionadded:: 20.0 active_usernames (Sequence[:obj:`str`], optional): If set, the list of all `active chat usernames `_; for private chats, supergroups and channels. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 business_intro (:class:`telegram.BusinessIntro`, optional): For private chats with business accounts, the intro of the business. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.1 business_location (:class:`telegram.BusinessLocation`, optional): For private chats with business accounts, the location of the business. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.1 business_opening_hours (:class:`telegram.BusinessOpeningHours`, optional): For private chats with business accounts, the opening hours of the business. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.8 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. Returned only in :meth:`telegram.Bot.get_chat`. Always returned in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_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, in seconds. Returned only in :meth:`telegram.Bot.get_chat`. |datetime_localization| .. versionadded:: 20.5 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. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.0 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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.0 birthdate (:obj:`telegram.Birthdate`, optional): For private chats, the date of birth of the user. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.1 personal_chat (:obj:`telegram.Chat`, optional): For private chats, the personal channel of the user. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.1 Attributes: id (:obj:`int`): Unique identifier for this chat. 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. 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. photo (:class:`telegram.ChatPhoto`): Optional. Chat photo. Returned only in :meth:`telegram.Bot.get_chat`. bio (:obj:`str`): Optional. Bio of the other party in a private chat. Returned only in :meth:`telegram.Bot.get_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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 13.9 description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats. Returned only in :meth:`telegram.Bot.get_chat`. invite_link (:obj:`str`): Optional. Primary invite link, for groups, supergroups and channel. Returned only in :meth:`telegram.Bot.get_chat`. pinned_message (:class:`telegram.Message`): Optional. The most recent pinned message (by sending date). Returned only in :meth:`telegram.Bot.get_chat`. permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`. slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between consecutive messages sent by each unprivileged user. Returned only in :meth:`telegram.Bot.get_chat`. message_auto_delete_time (:obj:`int`): Optional. The time after which all messages sent to the chat will be automatically deleted; in seconds. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 13.4 has_protected_content (:obj:`bool`): Optional. :obj:`True`, if messages from the chat can't be forwarded to other chats. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.8 sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set. Returned only in :meth:`telegram.Bot.get_chat`. can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the sticker set. Returned only in :meth:`telegram.Bot.get_chat`. 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. Returned only in :meth:`telegram.Bot.get_chat`. location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`. join_to_send_messages (:obj:`bool`): Optional. :obj:`True`, if users need to join the supergroup before they can send messages. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 join_by_request (:obj:`bool`): Optional. :obj:`True`, if all users directly joining the supergroup need to be approved by supergroup administrators. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 has_restricted_voice_and_video_messages (:obj:`bool`): Optional. :obj:`True`, if the privacy settings of the other party restrict sending voice and video note messages in the private chat. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 is_forum (:obj:`bool`): Optional. :obj:`True`, if the supergroup chat is a forum (has topics_ enabled). .. versionadded:: 20.0 active_usernames (Tuple[:obj:`str`]): Optional. If set, the list of all `active chat usernames `_; for private chats, supergroups and channels. Returned only in :meth:`telegram.Bot.get_chat`. 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 business_intro (:class:`telegram.BusinessIntro`): Optional. For private chats with business accounts, the intro of the business. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.1 business_location (:class:`telegram.BusinessLocation`): Optional. For private chats with business accounts, the location of the business. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.1 business_opening_hours (:class:`telegram.BusinessOpeningHours`): Optional. For private chats with business accounts, the opening hours of the business. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.8 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. Returned only in :meth:`telegram.Bot.get_chat`. Always returned in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_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, in seconds. Returned only in :meth:`telegram.Bot.get_chat`. |datetime_localization| .. versionadded:: 20.5 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. Returned only in :meth:`telegram.Bot.get_chat`. .. 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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.0 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. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.0 birthdate (:obj:`telegram.Birthdate`): Optional. For private chats, the date of birth of the user. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.1 personal_chat (:obj:`telegram.Chat`): Optional. For private chats, the personal channel of the user. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 21.1 .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups .. _accent colors: https://core.telegram.org/bots/api#accent-colors """ __slots__ = ( "accent_color_id", "active_usernames", "available_reactions", "background_custom_emoji_id", "bio", "birthdate", "business_intro", "business_location", "business_opening_hours", "can_set_sticker_set", "custom_emoji_sticker_set_name", "description", "emoji_status_custom_emoji_id", "emoji_status_expiration_date", "first_name", "has_aggressive_anti_spam_enabled", "has_hidden_members", "has_private_forwards", "has_protected_content", "has_restricted_voice_and_video_messages", "has_visible_history", "id", "invite_link", "is_forum", "join_by_request", "join_to_send_messages", "last_name", "linked_chat_id", "location", "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", "title", "type", "unrestrict_boost_count", "username", ) 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`""" 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, photo: Optional[ChatPhoto] = None, description: Optional[str] = None, invite_link: Optional[str] = None, pinned_message: Optional["Message"] = None, permissions: Optional[ChatPermissions] = None, sticker_set_name: Optional[str] = None, can_set_sticker_set: Optional[bool] = None, slow_mode_delay: Optional[int] = None, bio: Optional[str] = None, linked_chat_id: Optional[int] = None, location: Optional[ChatLocation] = None, message_auto_delete_time: Optional[int] = None, has_private_forwards: Optional[bool] = None, has_protected_content: Optional[bool] = None, join_to_send_messages: Optional[bool] = None, join_by_request: Optional[bool] = None, has_restricted_voice_and_video_messages: Optional[bool] = None, is_forum: Optional[bool] = None, active_usernames: Optional[Sequence[str]] = None, emoji_status_custom_emoji_id: Optional[str] = None, emoji_status_expiration_date: Optional[datetime] = None, has_aggressive_anti_spam_enabled: Optional[bool] = None, has_hidden_members: Optional[bool] = None, available_reactions: Optional[Sequence[ReactionType]] = None, accent_color_id: Optional[int] = None, background_custom_emoji_id: Optional[str] = None, profile_accent_color_id: Optional[int] = None, profile_background_custom_emoji_id: Optional[str] = None, has_visible_history: Optional[bool] = None, unrestrict_boost_count: Optional[int] = None, custom_emoji_sticker_set_name: Optional[str] = None, birthdate: Optional[Birthdate] = None, personal_chat: Optional["Chat"] = None, business_intro: Optional["BusinessIntro"] = None, business_location: Optional["BusinessLocation"] = None, business_opening_hours: Optional["BusinessOpeningHours"] = 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.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.is_forum: Optional[bool] = is_forum 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._id_attrs = (self.id,) self._freeze() @property def effective_name(self) -> Optional[str]: """ :obj:`str`: Convenience property. Gives :attr:`title` if not :obj:`None`, else :attr:`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:`first_name` is not :obj:`None`, gives :attr:`first_name` followed by (if available) :attr:`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:`username`, returns a t.me link of the chat. """ if self.username: return f"https://t.me/{self.username}" return None @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Chat"]: """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"] = cls.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 ) 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 "all_members_are_administrators" in data: api_kwargs["all_members_are_administrators"] = data.pop( "all_members_are_administrators" ) return super()._de_json(data=data, bot=bot, 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. .. 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 markdown (version 1). Raises: :exc:`TypeError`: If the chat is a private chat and neither the :paramref:`name` nor the :attr:`first_name` is set, then throw an :exc:`TypeError`. If the chat is a public chat and neither the :paramref:`name` nor the :attr:`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:`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:`first_name` is set, then throw an :exc:`TypeError`. If the chat is a public chat and neither the :paramref:`name` nor the :attr:`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:`first_name` is set, then throw an :exc:`TypeError`. If the chat is a public chat and neither the :paramref:`name` nor the :attr:`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, *, read_timeout: ODVInput[float] = DEFAULT_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, ) async def unpin_message( self, 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, ) -> 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) async def send_invoice( self, title: str, description: str, payload: str, provider_token: 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) async def send_poll( self, question: str, options: Sequence[str], 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, *, 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, 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, ) 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, *, 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, ) 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, *, 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, ) 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 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, ) python-telegram-bot-21.1.1/telegram/_chatadministratorrights.py000066400000000000000000000261461460724040100247560ustar00rootroot00000000000000#!/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. .. 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. .. 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__)) python-telegram-bot-21.1.1/telegram/_chatboost.py000066400000000000000000000361661460724040100220060ustar00rootroot00000000000000#!/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: "Bot") -> 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: "Bot") -> 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: "Bot") -> 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: "Bot") -> 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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_chatinvitelink.py000066400000000000000000000151541460724040100230260ustar00rootroot00000000000000#!/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 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 """ __slots__ = ( "creates_join_request", "creator", "expire_date", "invite_link", "is_primary", "is_revoked", "member_limit", "name", "pending_join_request_count", ) 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, *, 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._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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_chatjoinrequest.py000066400000000000000000000210521460724040100232140ustar00rootroot00000000000000#!/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: "Bot") -> 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, ) python-telegram-bot-21.1.1/telegram/_chatlocation.py000066400000000000000000000063401460724040100224570ustar00rootroot00000000000000#!/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: "Bot") -> 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 """ python-telegram-bot-21.1.1/telegram/_chatmember.py000066400000000000000000000642441460724040100221250ustar00rootroot00000000000000#!/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: "Bot") -> 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. .. 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. .. 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. 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. """ __slots__ = () def __init__( self, user: User, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(status=ChatMember.MEMBER, user=user, api_kwargs=api_kwargs) self._freeze() 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 python-telegram-bot-21.1.1/telegram/_chatmemberupdated.py000066400000000000000000000174671460724040100235010ustar00rootroot00000000000000#!/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 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 """ __slots__ = ( "chat", "date", "from_user", "invite_link", "new_chat_member", "old_chat_member", "via_chat_folder_invite_link", ) 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, *, 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._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: "Bot") -> 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] python-telegram-bot-21.1.1/telegram/_chatpermissions.py000066400000000000000000000252451460724040100232270ustar00rootroot00000000000000#!/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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_choseninlineresult.py000066400000000000000000000102621460724040100237220ustar00rootroot00000000000000#!/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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_dice.py000066400000000000000000000154541460724040100207210ustar00rootroot00000000000000#!/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 """ python-telegram-bot-21.1.1/telegram/_files/000077500000000000000000000000001460724040100205345ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_files/__init__.py000066400000000000000000000000001460724040100226330ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_files/_basemedium.py000066400000000000000000000067201460724040100233650ustar00rootroot00000000000000#!/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/]. """Common base class for media objects""" from typing import TYPE_CHECKING, Optional from telegram._telegramobject import TelegramObject from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import File class _BaseMedium(TelegramObject): """Base class for objects representing the various media file types. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`, optional): File size. Attributes: file_id (:obj:`str`): File identifier. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`): Optional. File size. """ __slots__ = ("file_id", "file_size", "file_unique_id") def __init__( self, file_id: str, file_unique_id: str, file_size: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.file_id: str = str(file_id) self.file_unique_id: str = str(file_unique_id) # Optionals self.file_size: Optional[int] = file_size self._id_attrs = (self.file_unique_id,) async def get_file( 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, ) -> "File": """Convenience wrapper over :meth:`telegram.Bot.get_file` For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: :class:`telegram.error.TelegramError` """ return await self.get_bot().get_file( file_id=self.file_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) python-telegram-bot-21.1.1/telegram/_files/_basethumbedmedium.py000066400000000000000000000073741460724040100247440ustar00rootroot00000000000000#!/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/]. """Common base class for media objects with thumbnails""" from typing import TYPE_CHECKING, Optional, Type, TypeVar from telegram._files._basemedium import _BaseMedium from telegram._files.photosize import PhotoSize from telegram._utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot # pylint: disable=invalid-name ThumbedMT_co = TypeVar("ThumbedMT_co", bound="_BaseThumbedMedium", covariant=True) class _BaseThumbedMedium(_BaseMedium): """ Base class for objects representing the various media file types that may include a thumbnail. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`, optional): File size. thumbnail (:class:`telegram.PhotoSize`, optional): Thumbnail as defined by sender. .. versionadded:: 20.2 Attributes: file_id (:obj:`str`): File identifier. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`): Optional. File size. thumbnail (:class:`telegram.PhotoSize`): Optional. Thumbnail as defined by sender. .. versionadded:: 20.2 """ __slots__ = ("thumbnail",) def __init__( self, file_id: str, file_unique_id: str, file_size: Optional[int] = None, thumbnail: Optional[PhotoSize] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__( file_id=file_id, file_unique_id=file_unique_id, file_size=file_size, api_kwargs=api_kwargs, ) self.thumbnail: Optional[PhotoSize] = thumbnail @classmethod def de_json( cls: Type[ThumbedMT_co], data: Optional[JSONDict], bot: "Bot" ) -> Optional[ThumbedMT_co]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None # In case this wasn't already done by the subclass if not isinstance(data.get("thumbnail"), PhotoSize): data["thumbnail"] = PhotoSize.de_json(data.get("thumbnail"), bot) 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("thumb") is not None: api_kwargs["thumb"] = data.pop("thumb") return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) python-telegram-bot-21.1.1/telegram/_files/animation.py000066400000000000000000000102601460724040100230640ustar00rootroot00000000000000#!/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 Animation.""" from typing import Optional from telegram._files._basethumbedmedium import _BaseThumbedMedium from telegram._files.photosize import PhotoSize from telegram._utils.types import JSONDict class Animation(_BaseThumbedMedium): """This object represents an animation file (GIF or H.264/MPEG-4 AVC video without sound). Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. .. versionchanged:: 20.5 |removed_thumb_note| Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. width (:obj:`int`): Video width as defined by sender. height (:obj:`int`): Video height as defined by sender. duration (:obj:`int`): Duration of the video in seconds as defined by sender. file_name (:obj:`str`, optional): Original animation filename as defined by sender. mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Animation thumbnail as defined by sender. .. versionadded:: 20.2 Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. width (:obj:`int`): Video width as defined by sender. height (:obj:`int`): Video height as defined by sender. duration (:obj:`int`): Duration of the video in seconds as defined by sender. file_name (:obj:`str`): Optional. Original animation filename as defined by sender. mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined by sender. .. versionadded:: 20.2 """ __slots__ = ("duration", "file_name", "height", "mime_type", "width") def __init__( self, file_id: str, file_unique_id: str, width: int, height: int, duration: int, file_name: Optional[str] = None, mime_type: Optional[str] = None, file_size: Optional[int] = None, thumbnail: Optional[PhotoSize] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__( file_id=file_id, file_unique_id=file_unique_id, file_size=file_size, api_kwargs=api_kwargs, thumbnail=thumbnail, ) with self._unfrozen(): # Required self.width: int = width self.height: int = height self.duration: int = duration # Optional self.mime_type: Optional[str] = mime_type self.file_name: Optional[str] = file_name python-telegram-bot-21.1.1/telegram/_files/audio.py000066400000000000000000000106211460724040100222070ustar00rootroot00000000000000#!/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 Audio.""" from typing import Optional from telegram._files._basethumbedmedium import _BaseThumbedMedium from telegram._files.photosize import PhotoSize from telegram._utils.types import JSONDict class Audio(_BaseThumbedMedium): """This object represents an audio file to be treated as music by the Telegram clients. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. .. versionchanged:: 20.5 |removed_thumb_note| Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. duration (:obj:`int`): Duration of the audio in seconds as defined by sender. performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio tags. title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags. file_name (:obj:`str`, optional): Original filename as defined by sender. mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Thumbnail of the album cover to which the music file belongs. .. versionadded:: 20.2 Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. duration (:obj:`int`): Duration of the audio in seconds as defined by sender. performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio tags. title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags. file_name (:obj:`str`): Optional. Original filename as defined by sender. mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Thumbnail of the album cover to which the music file belongs. .. versionadded:: 20.2 """ __slots__ = ("duration", "file_name", "mime_type", "performer", "title") def __init__( self, file_id: str, file_unique_id: str, duration: int, performer: Optional[str] = None, title: Optional[str] = None, mime_type: Optional[str] = None, file_size: Optional[int] = None, file_name: Optional[str] = None, thumbnail: Optional[PhotoSize] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__( file_id=file_id, file_unique_id=file_unique_id, file_size=file_size, thumbnail=thumbnail, api_kwargs=api_kwargs, ) with self._unfrozen(): # Required self.duration: int = duration # Optional self.performer: Optional[str] = performer self.title: Optional[str] = title self.mime_type: Optional[str] = mime_type self.file_name: Optional[str] = file_name python-telegram-bot-21.1.1/telegram/_files/chatphoto.py000066400000000000000000000156701460724040100231100ustar00rootroot00000000000000#!/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 ChatPhoto.""" from typing import TYPE_CHECKING, Final, Optional from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import File class ChatPhoto(TelegramObject): """This object represents a chat photo. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`small_file_unique_id` and :attr:`big_file_unique_id` are equal. Args: small_file_id (:obj:`str`): File identifier of small (:tg-const:`telegram.ChatPhoto.SIZE_SMALL` x :tg-const:`telegram.ChatPhoto.SIZE_SMALL`) chat photo. This file_id can be used only for photo download and only for as long as the photo is not changed. small_file_unique_id (:obj:`str`): Unique file identifier of small (:tg-const:`telegram.ChatPhoto.SIZE_SMALL` x :tg-const:`telegram.ChatPhoto.SIZE_SMALL`) chat photo, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. big_file_id (:obj:`str`): File identifier of big (:tg-const:`telegram.ChatPhoto.SIZE_BIG` x :tg-const:`telegram.ChatPhoto.SIZE_BIG`) chat photo. This file_id can be used only for photo download and only for as long as the photo is not changed. big_file_unique_id (:obj:`str`): Unique file identifier of big (:tg-const:`telegram.ChatPhoto.SIZE_BIG` x :tg-const:`telegram.ChatPhoto.SIZE_BIG`) chat photo, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. Attributes: small_file_id (:obj:`str`): File identifier of small (:tg-const:`telegram.ChatPhoto.SIZE_SMALL` x :tg-const:`telegram.ChatPhoto.SIZE_SMALL`) chat photo. This file_id can be used only for photo download and only for as long as the photo is not changed. small_file_unique_id (:obj:`str`): Unique file identifier of small (:tg-const:`telegram.ChatPhoto.SIZE_SMALL` x :tg-const:`telegram.ChatPhoto.SIZE_SMALL`) chat photo, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. big_file_id (:obj:`str`): File identifier of big (:tg-const:`telegram.ChatPhoto.SIZE_BIG` x :tg-const:`telegram.ChatPhoto.SIZE_BIG`) chat photo. This file_id can be used only for photo download and only for as long as the photo is not changed. big_file_unique_id (:obj:`str`): Unique file identifier of big (:tg-const:`telegram.ChatPhoto.SIZE_BIG` x :tg-const:`telegram.ChatPhoto.SIZE_BIG`) chat photo, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. """ __slots__ = ( "big_file_id", "big_file_unique_id", "small_file_id", "small_file_unique_id", ) def __init__( self, small_file_id: str, small_file_unique_id: str, big_file_id: str, big_file_unique_id: str, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.small_file_id: str = small_file_id self.small_file_unique_id: str = small_file_unique_id self.big_file_id: str = big_file_id self.big_file_unique_id: str = big_file_unique_id self._id_attrs = ( self.small_file_unique_id, self.big_file_unique_id, ) self._freeze() async def get_small_file( 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, ) -> "File": """Convenience wrapper over :meth:`telegram.Bot.get_file` for getting the small (:tg-const:`telegram.ChatPhoto.SIZE_SMALL` x :tg-const:`telegram.ChatPhoto.SIZE_SMALL`) chat photo For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: :class:`telegram.error.TelegramError` """ return await self.get_bot().get_file( file_id=self.small_file_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) async def get_big_file( 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, ) -> "File": """Convenience wrapper over :meth:`telegram.Bot.get_file` for getting the big (:tg-const:`telegram.ChatPhoto.SIZE_BIG` x :tg-const:`telegram.ChatPhoto.SIZE_BIG`) chat photo For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: :class:`telegram.error.TelegramError` """ return await self.get_bot().get_file( file_id=self.big_file_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) SIZE_SMALL: Final[int] = constants.ChatPhotoSize.SMALL """:const:`telegram.constants.ChatPhotoSize.SMALL` .. versionadded:: 20.0 """ SIZE_BIG: Final[int] = constants.ChatPhotoSize.BIG """:const:`telegram.constants.ChatPhotoSize.BIG` .. versionadded:: 20.0 """ python-telegram-bot-21.1.1/telegram/_files/contact.py000066400000000000000000000052401460724040100225420ustar00rootroot00000000000000#!/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 Contact.""" from typing import Optional from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict class Contact(TelegramObject): """This object represents a phone contact. 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. user_id (:obj:`int`, optional): Contact's user identifier in Telegram. vcard (:obj:`str`, optional): Additional data about the contact in the form of a vCard. 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. user_id (:obj:`int`): Optional. Contact's user identifier in Telegram. vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard. """ __slots__ = ("first_name", "last_name", "phone_number", "user_id", "vcard") def __init__( self, phone_number: str, first_name: str, last_name: Optional[str] = None, user_id: Optional[int] = None, vcard: Optional[str] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.phone_number: str = str(phone_number) self.first_name: str = first_name # Optionals self.last_name: Optional[str] = last_name self.user_id: Optional[int] = user_id self.vcard: Optional[str] = vcard self._id_attrs = (self.phone_number,) self._freeze() python-telegram-bot-21.1.1/telegram/_files/document.py000066400000000000000000000067401460724040100227330ustar00rootroot00000000000000#!/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 Document.""" from typing import Optional from telegram._files._basethumbedmedium import _BaseThumbedMedium from telegram._files.photosize import PhotoSize from telegram._utils.types import JSONDict class Document(_BaseThumbedMedium): """This object represents a general file (as opposed to photos, voice messages and audio files). Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. .. versionchanged:: 20.5 |removed_thumb_note| Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_name (:obj:`str`, optional): Original filename as defined by sender. mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Document thumbnail as defined by sender. .. versionadded:: 20.2 Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_name (:obj:`str`): Optional. Original filename as defined by sender. mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Document thumbnail as defined by sender. .. versionadded:: 20.2 """ __slots__ = ("file_name", "mime_type") def __init__( self, file_id: str, file_unique_id: str, file_name: Optional[str] = None, mime_type: Optional[str] = None, file_size: Optional[int] = None, thumbnail: Optional[PhotoSize] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__( file_id=file_id, file_unique_id=file_unique_id, file_size=file_size, thumbnail=thumbnail, api_kwargs=api_kwargs, ) with self._unfrozen(): # Optional self.mime_type: Optional[str] = mime_type self.file_name: Optional[str] = file_name python-telegram-bot-21.1.1/telegram/_files/file.py000066400000000000000000000344641460724040100220400ustar00rootroot00000000000000#!/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 File.""" import shutil import urllib.parse as urllib_parse from base64 import b64decode from pathlib import Path from typing import TYPE_CHECKING, BinaryIO, Optional from telegram._passport.credentials import decrypt from telegram._telegramobject import TelegramObject from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.files import is_local_file from telegram._utils.types import FilePathInput, JSONDict, ODVInput if TYPE_CHECKING: from telegram import FileCredentials class File(TelegramObject): """ This object represents a file ready to be downloaded. The file can be e.g. downloaded with :attr:`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 :meth:`telegram.Bot.get_file`. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. .. versionchanged:: 20.0 ``download`` was split into :meth:`download_to_drive` and :meth:`download_to_memory`. Note: * Maximum file size to download is :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_DOWNLOAD`. * If you obtain an instance of this class from :attr:`telegram.PassportFile.get_file`, then it will automatically be decrypted as it downloads when you call e.g. :meth:`download_to_drive`. Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`, optional): File size in bytes, if known. file_path (:obj:`str`, optional): File path. Use e.g. :meth:`download_to_drive` to get the file. Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`): Optional. File size in bytes, if known. file_path (:obj:`str`): Optional. File path. Use e.g. :meth:`download_to_drive` to get the file. """ __slots__ = ( "_credentials", "file_id", "file_path", "file_size", "file_unique_id", ) def __init__( self, file_id: str, file_unique_id: str, file_size: Optional[int] = None, file_path: Optional[str] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.file_id: str = str(file_id) self.file_unique_id: str = str(file_unique_id) # Optionals self.file_size: Optional[int] = file_size self.file_path: Optional[str] = file_path self._credentials: Optional[FileCredentials] = None self._id_attrs = (self.file_unique_id,) self._freeze() def _get_encoded_url(self) -> str: """Convert any UTF-8 char in :obj:`File.file_path` into a url encoded ASCII string.""" sres = urllib_parse.urlsplit(str(self.file_path)) return urllib_parse.urlunsplit( urllib_parse.SplitResult( sres.scheme, sres.netloc, urllib_parse.quote(sres.path), sres.query, sres.fragment ) ) def _prepare_decrypt(self, buf: bytes) -> bytes: return decrypt(b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf) async def download_to_drive( self, custom_path: Optional[FilePathInput] = 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, ) -> Path: """ Download this file. By default, the file is saved in the current working directory with :attr:`file_path` as file name. If the file has no filename, the file ID will be used as filename. If :paramref:`custom_path` is supplied as a :obj:`str` or :obj:`pathlib.Path`, it will be saved to that path. Note: If :paramref:`custom_path` isn't provided and :attr:`file_path` is the path of a local file (which is the case when a Bot API Server is running in local mode), this method will just return the path. The only exception to this are encrypted files (e.g. a passport file). For these, a file with the prefix `decrypted_` will be created in the same directory as the original file in order to decrypt the file without changing the existing one in-place. .. seealso:: :wiki:`Working with Files and Media ` .. versionchanged:: 20.0 * :paramref:`custom_path` parameter now also accepts :class:`pathlib.Path` as argument. * Returns :class:`pathlib.Path` object in cases where previously a :obj:`str` was returned. * This method was previously called ``download``. It was split into :meth:`download_to_drive` and :meth:`download_to_memory`. Args: custom_path (:class:`pathlib.Path` | :obj:`str` , optional): The path where the file will be saved to. If not specified, will be saved in the current working directory with :attr:`file_path` as file name or the :attr:`file_id` if :attr:`file_path` is not set. Keyword Args: read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.write_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.connect_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. Returns: :class:`pathlib.Path`: Returns the Path object the file was downloaded to. """ local_file = is_local_file(self.file_path) url = None if local_file else self._get_encoded_url() # if _credentials exists we want to decrypt the file if local_file and self._credentials: file_to_decrypt = Path(self.file_path) buf = self._prepare_decrypt(file_to_decrypt.read_bytes()) if custom_path is not None: path = Path(custom_path) else: path = Path(str(file_to_decrypt.parent) + "/decrypted_" + file_to_decrypt.name) path.write_bytes(buf) return path if custom_path is not None and local_file: shutil.copyfile(self.file_path, str(custom_path)) return Path(custom_path) if custom_path: filename = Path(custom_path) elif local_file: return Path(self.file_path) elif self.file_path: filename = Path(Path(self.file_path).name) else: filename = Path.cwd() / self.file_id buf = await self.get_bot().request.retrieve( url, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, ) if self._credentials: buf = self._prepare_decrypt(buf) filename.write_bytes(buf) return filename async def download_to_memory( self, out: BinaryIO, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, ) -> None: """ Download this file into memory. :paramref:`out` needs to be supplied with a :obj:`io.BufferedIOBase`, the file contents will be saved to that object using the :obj:`out.write` method. .. seealso:: :wiki:`Working with Files and Media ` Hint: If you want to immediately read the data from ``out`` after calling this method, you should call ``out.seek(0)`` first. See also :meth:`io.IOBase.seek`. .. versionadded:: 20.0 Args: out (:obj:`io.BufferedIOBase`): A file-like object. Must be opened for writing in binary mode. Keyword Args: read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.write_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.connect_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. """ local_file = is_local_file(self.file_path) url = None if local_file else self._get_encoded_url() path = Path(self.file_path) if local_file else None if local_file: buf = path.read_bytes() else: buf = await self.get_bot().request.retrieve( url, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, ) if self._credentials: buf = self._prepare_decrypt(buf) out.write(buf) async def download_as_bytearray( self, buf: Optional[bytearray] = 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, ) -> bytearray: """Download this file and return it as a bytearray. Args: buf (:obj:`bytearray`, optional): Extend the given bytearray with the downloaded data. Keyword Args: read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. versionadded:: 20.0 write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.write_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. versionadded:: 20.0 connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.connect_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. versionadded:: 20.0 pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. versionadded:: 20.0 Returns: :obj:`bytearray`: The same object as :paramref:`buf` if it was specified. Otherwise a newly allocated :obj:`bytearray`. """ if buf is None: buf = bytearray() if is_local_file(self.file_path): bytes_data = Path(self.file_path).read_bytes() else: bytes_data = await self.get_bot().request.retrieve( self._get_encoded_url(), read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, ) if self._credentials: buf.extend(self._prepare_decrypt(bytes_data)) else: buf.extend(bytes_data) return buf def set_credentials(self, credentials: "FileCredentials") -> None: """Sets the passport credentials for the file. Args: credentials (:class:`telegram.FileCredentials`): The credentials. """ self._credentials = credentials python-telegram-bot-21.1.1/telegram/_files/inputfile.py000066400000000000000000000101371460724040100231070ustar00rootroot00000000000000#!/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 InputFile.""" import mimetypes from typing import IO, Optional, Union from uuid import uuid4 from telegram._utils.files import load_file from telegram._utils.types import FieldTuple _DEFAULT_MIME_TYPE = "application/octet-stream" class InputFile: """This object represents a Telegram InputFile. .. versionchanged:: 20.0 * The former attribute ``attach`` was renamed to :attr:`attach_name`. * Method ``is_image`` was removed. If you pass :obj:`bytes` to :paramref:`obj` and would like to have the mime type automatically guessed, please pass :paramref:`filename` in addition. Args: obj (:term:`file object` | :obj:`bytes` | :obj:`str`): An open file descriptor or the files content as bytes or string. Note: If :paramref:`obj` is a string, it will be encoded as bytes via :external:obj:`obj.encode('utf-8') `. .. versionchanged:: 20.0 Accept string input. filename (:obj:`str`, optional): Filename for this InputFile. attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in the request to Telegram should point to the multipart data via an ``attach://`` URI. Defaults to `False`. Attributes: input_file_content (:obj:`bytes`): The binary content of the file to send. attach_name (:obj:`str`): Optional. If present, the parameter this file belongs to in the request to Telegram should point to the multipart data via a an URI of the form ``attach://`` URI. filename (:obj:`str`): Filename for the file to be sent. mimetype (:obj:`str`): The mimetype inferred from the file to be sent. """ __slots__ = ("attach_name", "filename", "input_file_content", "mimetype") def __init__( self, obj: Union[IO[bytes], bytes, str], filename: Optional[str] = None, attach: bool = False, ): if isinstance(obj, bytes): self.input_file_content: bytes = obj elif isinstance(obj, str): self.input_file_content = obj.encode("utf-8") else: reported_filename, self.input_file_content = load_file(obj) filename = filename or reported_filename self.attach_name: Optional[str] = "attached" + uuid4().hex if attach else None if filename: self.mimetype: str = ( mimetypes.guess_type(filename, strict=False)[0] or _DEFAULT_MIME_TYPE ) else: self.mimetype = _DEFAULT_MIME_TYPE self.filename: str = filename or self.mimetype.replace("/", ".") @property def field_tuple(self) -> FieldTuple: """Field tuple representing the contents of the file for upload to the Telegram servers. Returns: Tuple[:obj:`str`, :obj:`bytes`, :obj:`str`]: """ return self.filename, self.input_file_content, self.mimetype @property def attach_uri(self) -> Optional[str]: """URI to insert into the JSON data for uploading the file. Returns :obj:`None`, if :attr:`attach_name` is :obj:`None`. """ return f"attach://{self.attach_name}" if self.attach_name else None python-telegram-bot-21.1.1/telegram/_files/inputmedia.py000066400000000000000000000643601460724040100232560ustar00rootroot00000000000000#!/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 InputMedia Objects.""" from typing import Optional, Sequence, Tuple, Union from telegram import constants from telegram._files.animation import Animation from telegram._files.audio import Audio from telegram._files.document import Document from telegram._files.inputfile import InputFile from telegram._files.photosize import PhotoSize from telegram._files.video import Video from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.files import parse_file_input from telegram._utils.types import FileInput, JSONDict, ODVInput from telegram.constants import InputMediaType MediaType = Union[Animation, Audio, Document, PhotoSize, Video] class InputMedia(TelegramObject): """ Base class for Telegram InputMedia Objects. .. versionchanged:: 20.0 Added arguments and attributes :attr:`type`, :attr:`media`, :attr:`caption`, :attr:`caption_entities`, :paramref:`parse_mode`. .. seealso:: :wiki:`Working with Files and Media ` Args: media_type (:obj:`str`): Type of media that the instance represents. media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Animation` | :class:`telegram.Audio` | \ :class:`telegram.Document` | :class:`telegram.PhotoSize` | \ :class:`telegram.Video`): File to send. |fileinputnopath| Lastly you can pass an existing telegram media object of the corresponding type to send. caption (:obj:`str`, optional): Caption of the media to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| .. versionchanged:: 20.0 |sequenceclassargs| parse_mode (:obj:`str`, optional): |parse_mode| Attributes: type (:obj:`str`): Type of the input media. media (:obj:`str` | :class:`telegram.InputFile`): Media to send. caption (:obj:`str`): Optional. Caption of the media to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. |parse_mode| caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr| .. versionchanged:: 20.0 * |tupleclassattrs| * |alwaystuple| """ __slots__ = ("caption", "caption_entities", "media", "parse_mode", "type") def __init__( self, media_type: str, media: Union[str, InputFile, MediaType], caption: Optional[str] = None, caption_entities: Optional[Sequence[MessageEntity]] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.type: str = enum.get_member(constants.InputMediaType, media_type, media_type) self.media: Union[str, InputFile, Animation, Audio, Document, PhotoSize, Video] = media self.caption: Optional[str] = caption self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) self.parse_mode: ODVInput[str] = parse_mode self._freeze() @staticmethod def _parse_thumbnail_input(thumbnail: Optional[FileInput]) -> Optional[Union[str, InputFile]]: # We use local_mode=True because we don't have access to the actual setting and want # things to work in local mode. return ( parse_file_input(thumbnail, attach=True, local_mode=True) if thumbnail is not None else thumbnail ) class InputMediaAnimation(InputMedia): """Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. Note: When using a :class:`telegram.Animation` for the :attr:`media` attribute, it will take the width, height and duration from that video, unless otherwise specified with the optional arguments. .. seealso:: :wiki:`Working with Files and Media ` .. versionchanged:: 20.5 |removed_thumb_note| Args: media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Animation`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.Animation` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. 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 caption (:obj:`str`, optional): Caption of the animation 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| width (:obj:`int`, optional): Animation width. height (:obj:`int`, optional): Animation height. duration (:obj:`int`, optional): Animation duration in seconds. 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): |thumbdocstringnopath| .. versionadded:: 20.2 Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.ANIMATION`. media (:obj:`str` | :class:`telegram.InputFile`): Animation to send. caption (:obj:`str`): Optional. Caption of the animation to be sent, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities parsing. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr| .. versionchanged:: 20.0 * |tupleclassattrs| * |alwaystuple| width (:obj:`int`): Optional. Animation width. height (:obj:`int`): Optional. Animation height. duration (:obj:`int`): Optional. Animation duration in seconds. has_spoiler (:obj:`bool`): Optional. :obj:`True`, if the animation is covered with a spoiler animation. .. versionadded:: 20.0 thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase| .. versionadded:: 20.2 """ __slots__ = ("duration", "has_spoiler", "height", "thumbnail", "width") def __init__( self, media: Union[FileInput, Animation], caption: Optional[str] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, width: Optional[int] = None, height: Optional[int] = None, duration: Optional[int] = None, caption_entities: Optional[Sequence[MessageEntity]] = None, filename: Optional[str] = None, has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, *, api_kwargs: Optional[JSONDict] = None, ): if isinstance(media, Animation): width = media.width if width is None else width height = media.height if height is None else height duration = media.duration if duration is None else duration media = media.file_id else: # We use local_mode=True because we don't have access to the actual setting and want # things to work in local mode. media = parse_file_input(media, filename=filename, attach=True, local_mode=True) super().__init__( InputMediaType.ANIMATION, media, caption, caption_entities, parse_mode, api_kwargs=api_kwargs, ) with self._unfrozen(): self.thumbnail: Optional[Union[str, InputFile]] = self._parse_thumbnail_input( thumbnail ) self.width: Optional[int] = width self.height: Optional[int] = height self.duration: Optional[int] = duration self.has_spoiler: Optional[bool] = has_spoiler class InputMediaPhoto(InputMedia): """Represents a photo to be sent. .. seealso:: :wiki:`Working with Files and Media ` Args: media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.PhotoSize`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.PhotoSize` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. 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 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| has_spoiler (:obj:`bool`, optional): Pass :obj:`True`, if the photo needs to be covered with a spoiler animation. .. versionadded:: 20.0 Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.PHOTO`. media (:obj:`str` | :class:`telegram.InputFile`): Photo to send. 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| has_spoiler (:obj:`bool`): Optional. :obj:`True`, if the photo is covered with a spoiler animation. .. versionadded:: 20.0 """ __slots__ = ("has_spoiler",) def __init__( self, media: Union[FileInput, PhotoSize], caption: Optional[str] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Optional[Sequence[MessageEntity]] = None, filename: Optional[str] = None, has_spoiler: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): # We use local_mode=True because we don't have access to the actual setting and want # things to work in local mode. media = parse_file_input(media, PhotoSize, filename=filename, attach=True, local_mode=True) super().__init__( InputMediaType.PHOTO, media, caption, caption_entities, parse_mode, api_kwargs=api_kwargs, ) with self._unfrozen(): self.has_spoiler: Optional[bool] = has_spoiler class InputMediaVideo(InputMedia): """Represents a video to be sent. .. seealso:: :wiki:`Working with Files and Media ` Note: * When using a :class:`telegram.Video` for the :attr:`media` attribute, it will take the width, height and duration from that video, unless otherwise specified with the optional arguments. * :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. .. versionchanged:: 20.5 |removed_thumb_note| Args: media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Video`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.Video` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. 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 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| width (:obj:`int`, optional): Video width. height (:obj:`int`, optional): Video height. duration (:obj:`int`, optional): Video duration in seconds. supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is suitable for streaming. 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): |thumbdocstringnopath| .. versionadded:: 20.2 Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.VIDEO`. media (:obj:`str` | :class:`telegram.InputFile`): Video file to send. 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| width (:obj:`int`): Optional. Video width. height (:obj:`int`): Optional. Video height. duration (:obj:`int`): Optional. Video duration in seconds. supports_streaming (:obj:`bool`): Optional. :obj:`True`, if the uploaded video is suitable for streaming. has_spoiler (:obj:`bool`): Optional. :obj:`True`, if the video is covered with a spoiler animation. .. versionadded:: 20.0 thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase| .. versionadded:: 20.2 """ __slots__ = ( "duration", "has_spoiler", "height", "supports_streaming", "thumbnail", "width", ) def __init__( self, media: Union[FileInput, Video], caption: Optional[str] = None, width: Optional[int] = None, height: Optional[int] = None, duration: Optional[int] = None, supports_streaming: Optional[bool] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Optional[Sequence[MessageEntity]] = None, filename: Optional[str] = None, has_spoiler: Optional[bool] = None, thumbnail: Optional[FileInput] = None, *, api_kwargs: Optional[JSONDict] = None, ): if isinstance(media, Video): width = width if width is not None else media.width height = height if height is not None else media.height duration = duration if duration is not None else media.duration media = media.file_id else: # We use local_mode=True because we don't have access to the actual setting and want # things to work in local mode. media = parse_file_input(media, filename=filename, attach=True, local_mode=True) super().__init__( InputMediaType.VIDEO, media, caption, caption_entities, parse_mode, api_kwargs=api_kwargs, ) with self._unfrozen(): self.width: Optional[int] = width self.height: Optional[int] = height self.duration: Optional[int] = duration self.thumbnail: Optional[Union[str, InputFile]] = self._parse_thumbnail_input( thumbnail ) self.supports_streaming: Optional[bool] = supports_streaming self.has_spoiler: Optional[bool] = has_spoiler class InputMediaAudio(InputMedia): """Represents an audio file to be treated as music to be sent. .. seealso:: :wiki:`Working with Files and Media ` Note: When using a :class:`telegram.Audio` for the :attr:`media` attribute, it will take the duration, performer and title from that video, unless otherwise specified with the optional arguments. .. versionchanged:: 20.5 |removed_thumb_note| Args: media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Audio`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.Audio` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. 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 caption (:obj:`str`, optional): Caption of the audio 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| duration (:obj:`int`, optional): Duration of the audio in seconds as defined by sender. performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio tags. title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags. thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ optional): |thumbdocstringnopath| .. versionadded:: 20.2 Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.AUDIO`. media (:obj:`str` | :class:`telegram.InputFile`): Audio file to send. caption (:obj:`str`): Optional. Caption of the audio 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| duration (:obj:`int`): Optional. Duration of the audio in seconds. performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio tags. title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags. thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase| .. versionadded:: 20.2 """ __slots__ = ("duration", "performer", "thumbnail", "title") def __init__( self, media: Union[FileInput, Audio], caption: Optional[str] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, duration: Optional[int] = None, performer: Optional[str] = None, title: Optional[str] = None, caption_entities: Optional[Sequence[MessageEntity]] = None, filename: Optional[str] = None, thumbnail: Optional[FileInput] = None, *, api_kwargs: Optional[JSONDict] = None, ): if isinstance(media, Audio): duration = media.duration if duration is None else duration performer = media.performer if performer is None else performer title = media.title if title is None else title media = media.file_id else: # We use local_mode=True because we don't have access to the actual setting and want # things to work in local mode. media = parse_file_input(media, filename=filename, attach=True, local_mode=True) super().__init__( InputMediaType.AUDIO, media, caption, caption_entities, parse_mode, api_kwargs=api_kwargs, ) with self._unfrozen(): self.thumbnail: Optional[Union[str, InputFile]] = self._parse_thumbnail_input( thumbnail ) self.duration: Optional[int] = duration self.title: Optional[str] = title self.performer: Optional[str] = performer class InputMediaDocument(InputMedia): """Represents a general file to be sent. .. seealso:: :wiki:`Working with Files and Media ` .. versionchanged:: 20.5 |removed_thumb_note| Args: media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Document`): File to send. |fileinputnopath| Lastly you can pass an existing :class:`telegram.Document` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. 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. .. versionadded:: 13.1 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| disable_content_type_detection (:obj:`bool`, optional): Disables automatic server-side content type detection for files uploaded using multipart/form-data. Always :obj:`True`, if the document is sent as part of an album. thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ optional): |thumbdocstringnopath| .. versionadded:: 20.2 Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.DOCUMENT`. media (:obj:`str` | :class:`telegram.InputFile`): File to send. 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| disable_content_type_detection (:obj:`bool`): Optional. Disables automatic server-side content type detection for files uploaded using multipart/form-data. Always :obj:`True`, if the document is sent as part of an album. thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase| .. versionadded:: 20.2 """ __slots__ = ("disable_content_type_detection", "thumbnail") def __init__( self, media: Union[FileInput, Document], caption: Optional[str] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_content_type_detection: Optional[bool] = None, caption_entities: Optional[Sequence[MessageEntity]] = None, filename: Optional[str] = None, thumbnail: Optional[FileInput] = None, *, api_kwargs: Optional[JSONDict] = None, ): # We use local_mode=True because we don't have access to the actual setting and want # things to work in local mode. media = parse_file_input(media, Document, filename=filename, attach=True, local_mode=True) super().__init__( InputMediaType.DOCUMENT, media, caption, caption_entities, parse_mode, api_kwargs=api_kwargs, ) with self._unfrozen(): self.thumbnail: Optional[Union[str, InputFile]] = self._parse_thumbnail_input( thumbnail ) self.disable_content_type_detection: Optional[bool] = disable_content_type_detection python-telegram-bot-21.1.1/telegram/_files/inputsticker.py000066400000000000000000000127651460724040100236450ustar00rootroot00000000000000#!/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 InputSticker.""" from typing import TYPE_CHECKING, Optional, Sequence, Tuple, Union from telegram._files.sticker import MaskPosition from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.files import parse_file_input from telegram._utils.types import FileInput, JSONDict if TYPE_CHECKING: from telegram._files.inputfile import InputFile class InputSticker(TelegramObject): """ This object describes a sticker to be added to a sticker set. .. versionadded:: 20.2 .. versionchanged:: 21.1 As of Bot API 7.2, the new argument :paramref:`format` is a required argument, and thus the order of the arguments has changed. Args: sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): The added sticker. |uploadinputnopath| Animated and video stickers can't be uploaded via HTTP URL. emoji_list (Sequence[:obj:`str`]): Sequence of :tg-const:`telegram.constants.StickerLimit.MIN_STICKER_EMOJI` - :tg-const:`telegram.constants.StickerLimit.MAX_STICKER_EMOJI` emoji associated with the sticker. mask_position (:obj:`telegram.MaskPosition`, optional): Position where the mask should be placed on faces. For ":tg-const:`telegram.constants.StickerType.MASK`" stickers only. keywords (Sequence[:obj:`str`], optional): Sequence of 0-:tg-const:`telegram.constants.StickerLimit.MAX_SEARCH_KEYWORDS` search keywords for the sticker with the total length of up to :tg-const:`telegram.constants.StickerLimit.MAX_KEYWORD_LENGTH` characters. For ":tg-const:`telegram.constants.StickerType.REGULAR`" and ":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only. 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 Attributes: sticker (:obj:`str` | :class:`telegram.InputFile`): The added sticker. emoji_list (Tuple[:obj:`str`]): Tuple of :tg-const:`telegram.constants.StickerLimit.MIN_STICKER_EMOJI` - :tg-const:`telegram.constants.StickerLimit.MAX_STICKER_EMOJI` emoji associated with the sticker. mask_position (:obj:`telegram.MaskPosition`): Optional. Position where the mask should be placed on faces. For ":tg-const:`telegram.constants.StickerType.MASK`" stickers only. keywords (Tuple[:obj:`str`]): Optional. Tuple of 0-:tg-const:`telegram.constants.StickerLimit.MAX_SEARCH_KEYWORDS` search keywords for the sticker with the total length of up to :tg-const:`telegram.constants.StickerLimit.MAX_KEYWORD_LENGTH` characters. For ":tg-const:`telegram.constants.StickerType.REGULAR`" and ":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only. ":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only. 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 """ __slots__ = ("emoji_list", "format", "keywords", "mask_position", "sticker") def __init__( self, sticker: FileInput, emoji_list: Sequence[str], format: str, # pylint: disable=redefined-builtin mask_position: Optional[MaskPosition] = None, keywords: Optional[Sequence[str]] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # We use local_mode=True because we don't have access to the actual setting and want # things to work in local mode. self.sticker: Union[str, InputFile] = parse_file_input( sticker, local_mode=True, attach=True, ) self.emoji_list: Tuple[str, ...] = parse_sequence_arg(emoji_list) self.format: str = format self.mask_position: Optional[MaskPosition] = mask_position self.keywords: Tuple[str, ...] = parse_sequence_arg(keywords) self._freeze() python-telegram-bot-21.1.1/telegram/_files/location.py000066400000000000000000000112321460724040100227150ustar00rootroot00000000000000#!/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 Location.""" from typing import Final, Optional from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict class Location(TelegramObject): """This object represents a point on the map. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`longitude` and :attr:`latitude` are equal. Args: longitude (:obj:`float`): Longitude as defined by sender. latitude (:obj:`float`): Latitude as defined by sender. horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location, measured in meters; 0-:tg-const:`telegram.Location.HORIZONTAL_ACCURACY`. live_period (:obj:`int`, optional): Time relative to the message sending date, during which the location can be updated, in seconds. For active live locations only. heading (:obj:`int`, optional): The direction in which user is moving, in degrees; :tg-const:`telegram.Location.MIN_HEADING`-:tg-const:`telegram.Location.MAX_HEADING`. For active live locations only. proximity_alert_radius (:obj:`int`, optional): Maximum distance for proximity alerts about approaching another chat member, in meters. For sent live locations only. Attributes: longitude (:obj:`float`): Longitude as defined by sender. latitude (:obj:`float`): Latitude as defined by sender. horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location, measured in meters; 0-:tg-const:`telegram.Location.HORIZONTAL_ACCURACY`. live_period (:obj:`int`): Optional. Time relative to the message sending date, during which the location can be updated, in seconds. For active live locations only. heading (:obj:`int`): Optional. The direction in which user is moving, in degrees; :tg-const:`telegram.Location.MIN_HEADING`-:tg-const:`telegram.Location.MAX_HEADING`. For active live locations only. proximity_alert_radius (:obj:`int`): Optional. Maximum distance for proximity alerts about approaching another chat member, in meters. For sent live locations only. """ __slots__ = ( "heading", "horizontal_accuracy", "latitude", "live_period", "longitude", "proximity_alert_radius", ) def __init__( self, longitude: float, latitude: float, horizontal_accuracy: Optional[float] = None, live_period: Optional[int] = None, heading: Optional[int] = None, proximity_alert_radius: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.longitude: float = longitude self.latitude: float = latitude # Optionals self.horizontal_accuracy: Optional[float] = horizontal_accuracy self.live_period: Optional[int] = live_period 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.longitude, self.latitude) self._freeze() 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 """ python-telegram-bot-21.1.1/telegram/_files/photosize.py000066400000000000000000000054121460724040100231340ustar00rootroot00000000000000#!/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 PhotoSize.""" from typing import Optional from telegram._files._basemedium import _BaseMedium from telegram._utils.types import JSONDict class PhotoSize(_BaseMedium): """This object represents one size of a photo or a file/sticker thumbnail. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. width (:obj:`int`): Photo width. height (:obj:`int`): Photo height. file_size (:obj:`int`, optional): File size in bytes. Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. width (:obj:`int`): Photo width. height (:obj:`int`): Photo height. file_size (:obj:`int`): Optional. File size in bytes. """ __slots__ = ("height", "width") def __init__( self, file_id: str, file_unique_id: str, width: int, height: int, file_size: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__( file_id=file_id, file_unique_id=file_unique_id, file_size=file_size, api_kwargs=api_kwargs, ) with self._unfrozen(): # Required self.width: int = width self.height: int = height python-telegram-bot-21.1.1/telegram/_files/sticker.py000066400000000000000000000420431460724040100225550ustar00rootroot00000000000000#!/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 stickers.""" from typing import TYPE_CHECKING, Final, Optional, Sequence, Tuple from telegram import constants from telegram._files._basethumbedmedium import _BaseThumbedMedium from telegram._files.file import File from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict from telegram._utils.warnings import warn from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import Bot class Sticker(_BaseThumbedMedium): """This object represents a sticker. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. Note: As of v13.11 :paramref:`is_video` is a required 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:: 20.5 |removed_thumb_note| Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. width (:obj:`int`): Sticker width. height (:obj:`int`): Sticker height. is_animated (:obj:`bool`): :obj:`True`, if the sticker is animated. is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker. .. versionadded:: 13.11 type (:obj:`str`): Type of the sticker. Currently one of :attr:`REGULAR`, :attr:`MASK`, :attr:`CUSTOM_EMOJI`. The type of the sticker is independent from its format, which is determined by the fields :attr:`is_animated` and :attr:`is_video`. .. versionadded:: 20.0 emoji (:obj:`str`, optional): Emoji associated with the sticker set_name (:obj:`str`, optional): Name of the sticker set to which the sticker belongs. mask_position (:class:`telegram.MaskPosition`, optional): For mask stickers, the position where the mask should be placed. file_size (:obj:`int`, optional): File size in bytes. premium_animation (:class:`telegram.File`, optional): For premium regular stickers, premium animation for the sticker. .. versionadded:: 20.0 custom_emoji_id (:obj:`str`, optional): For custom emoji stickers, unique identifier of the custom emoji. .. versionadded:: 20.0 thumbnail (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the ``.WEBP`` or ``.JPG`` format. .. versionadded:: 20.2 needs_repainting (:obj:`bool`, optional): :obj:`True`, if the sticker must be repainted to a text color in messages, the color of the Telegram Premium badge in emoji status, white color on chat photos, or another appropriate color in other places. .. versionadded:: 20.2 Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. width (:obj:`int`): Sticker width. height (:obj:`int`): Sticker height. is_animated (:obj:`bool`): :obj:`True`, if the sticker is animated. is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker. .. versionadded:: 13.11 type (:obj:`str`): Type of the sticker. Currently one of :attr:`REGULAR`, :attr:`MASK`, :attr:`CUSTOM_EMOJI`. The type of the sticker is independent from its format, which is determined by the fields :attr:`is_animated` and :attr:`is_video`. .. versionadded:: 20.0 emoji (:obj:`str`): Optional. Emoji associated with the sticker. set_name (:obj:`str`): Optional. Name of the sticker set to which the sticker belongs. mask_position (:class:`telegram.MaskPosition`): Optional. For mask stickers, the position where the mask should be placed. file_size (:obj:`int`): Optional. File size in bytes. premium_animation (:class:`telegram.File`): Optional. For premium regular stickers, premium animation for the sticker. .. versionadded:: 20.0 custom_emoji_id (:obj:`str`): Optional. For custom emoji stickers, unique identifier of the custom emoji. .. versionadded:: 20.0 thumbnail (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the ``.WEBP`` or ``.JPG`` format. .. versionadded:: 20.2 needs_repainting (:obj:`bool`): Optional. :obj:`True`, if the sticker must be repainted to a text color in messages, the color of the Telegram Premium badge in emoji status, white color on chat photos, or another appropriate color in other places. .. versionadded:: 20.2 """ __slots__ = ( "custom_emoji_id", "emoji", "height", "is_animated", "is_video", "mask_position", "needs_repainting", "premium_animation", "set_name", "type", "width", ) def __init__( self, file_id: str, file_unique_id: str, width: int, height: int, is_animated: bool, is_video: bool, type: str, # pylint: disable=redefined-builtin emoji: Optional[str] = None, file_size: Optional[int] = None, set_name: Optional[str] = None, mask_position: Optional["MaskPosition"] = None, premium_animation: Optional["File"] = None, custom_emoji_id: Optional[str] = None, thumbnail: Optional[PhotoSize] = None, needs_repainting: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__( file_id=file_id, file_unique_id=file_unique_id, file_size=file_size, thumbnail=thumbnail, api_kwargs=api_kwargs, ) with self._unfrozen(): # Required self.width: int = width self.height: int = height self.is_animated: bool = is_animated self.is_video: bool = is_video self.type: str = enum.get_member(constants.StickerType, type, type) # Optional self.emoji: Optional[str] = emoji self.set_name: Optional[str] = set_name self.mask_position: Optional[MaskPosition] = mask_position self.premium_animation: Optional[File] = premium_animation self.custom_emoji_id: Optional[str] = custom_emoji_id self.needs_repainting: Optional[bool] = needs_repainting REGULAR: Final[str] = constants.StickerType.REGULAR """:const:`telegram.constants.StickerType.REGULAR`""" MASK: Final[str] = constants.StickerType.MASK """:const:`telegram.constants.StickerType.MASK`""" CUSTOM_EMOJI: Final[str] = constants.StickerType.CUSTOM_EMOJI """:const:`telegram.constants.StickerType.CUSTOM_EMOJI`""" @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Sticker"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data["thumbnail"] = PhotoSize.de_json(data.get("thumbnail"), bot) data["mask_position"] = MaskPosition.de_json(data.get("mask_position"), bot) data["premium_animation"] = File.de_json(data.get("premium_animation"), bot) 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("thumb") is not None: api_kwargs["thumb"] = data.pop("thumb") return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) class StickerSet(TelegramObject): """This object represents a sticker set. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`name` is equal. Note: As of v13.11 :paramref:`is_video` is a required 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:: 20.0 The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` instead. .. versionchanged:: 21.1 The parameters ``is_video`` and ``is_animated`` are deprecated and now made optional. Thus, the order of the arguments had to be changed. .. versionchanged:: 20.5 |removed_thumb_note| Args: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers. .. deprecated:: 21.1 Bot API 7.2 deprecated this field. This parameter will be removed in a future version of the library. is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. .. versionadded:: 13.11 .. deprecated:: 21.1 Bot API 7.2 deprecated this field. This parameter will be removed in a future version of the library. stickers (Sequence[:class:`telegram.Sticker`]): List of all set stickers. .. versionchanged:: 20.0 |sequenceclassargs| sticker_type (:obj:`str`): Type of stickers in the set, currently one of :attr:`telegram.Sticker.REGULAR`, :attr:`telegram.Sticker.MASK`, :attr:`telegram.Sticker.CUSTOM_EMOJI`. .. versionadded:: 20.0 thumbnail (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the ``.WEBP``, ``.TGS``, or ``.WEBM`` format. .. versionadded:: 20.2 Attributes: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers. .. deprecated:: 21.1 Bot API 7.2 deprecated this field. This parameter will be removed in a future version of the library. is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. .. versionadded:: 13.11 .. deprecated:: 21.1 Bot API 7.2 deprecated this field. This parameter will be removed in a future version of the library. stickers (Tuple[:class:`telegram.Sticker`]): List of all set stickers. .. versionchanged:: 20.0 |tupleclassattrs| sticker_type (:obj:`str`): Type of stickers in the set, currently one of :attr:`telegram.Sticker.REGULAR`, :attr:`telegram.Sticker.MASK`, :attr:`telegram.Sticker.CUSTOM_EMOJI`. .. versionadded:: 20.0 thumbnail (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the ``.WEBP``, ``.TGS``, or ``.WEBM`` format. .. versionadded:: 20.2 """ __slots__ = ( "is_animated", "is_video", "name", "sticker_type", "stickers", "thumbnail", "title", ) def __init__( self, name: str, title: str, stickers: Sequence[Sticker], sticker_type: str, is_animated: Optional[bool] = None, is_video: Optional[bool] = None, thumbnail: Optional[PhotoSize] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.name: str = name self.title: str = title self.stickers: Tuple[Sticker, ...] = parse_sequence_arg(stickers) self.sticker_type: str = sticker_type # Optional self.thumbnail: Optional[PhotoSize] = thumbnail if is_animated is not None or is_video is not None: warn( "The parameters `is_animated` and `is_video` are deprecated and will be removed " "in a future version.", PTBDeprecationWarning, stacklevel=2, ) self.is_animated: Optional[bool] = is_animated self.is_video: Optional[bool] = is_video self._id_attrs = (self.name,) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["StickerSet"]: """See :meth:`telegram.TelegramObject.de_json`.""" if not data: return None data["thumbnail"] = PhotoSize.de_json(data.get("thumbnail"), bot) data["stickers"] = Sticker.de_list(data.get("stickers"), bot) api_kwargs = {} # These are deprecated fields that TG still returns for backwards compatibility # Let's filter them out to speed up the de-json process for deprecated_field in ("contains_masks", "thumb"): if deprecated_field in data: api_kwargs[deprecated_field] = data.pop(deprecated_field) return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) class MaskPosition(TelegramObject): """This object describes the position on faces where a mask should be placed by default. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`point`, :attr:`x_shift`, :attr:`y_shift` and, :attr:`scale` are equal. Args: point (:obj:`str`): The part of the face relative to which the mask should be placed. One of :attr:`FOREHEAD`, :attr:`EYES`, :attr:`MOUTH`, or :attr:`CHIN`. x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face size, from left to right. For example, choosing ``-1.0`` will place mask just to the left of the default mask position. y_shift (:obj:`float`): Shift by Y-axis measured in heights of the mask scaled to the face size, from top to bottom. For example, ``1.0`` will place the mask just below the default mask position. scale (:obj:`float`): Mask scaling coefficient. For example, ``2.0`` means double size. Attributes: point (:obj:`str`): The part of the face relative to which the mask should be placed. One of :attr:`FOREHEAD`, :attr:`EYES`, :attr:`MOUTH`, or :attr:`CHIN`. x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face size, from left to right. For example, choosing ``-1.0`` will place mask just to the left of the default mask position. y_shift (:obj:`float`): Shift by Y-axis measured in heights of the mask scaled to the face size, from top to bottom. For example, ``1.0`` will place the mask just below the default mask position. scale (:obj:`float`): Mask scaling coefficient. For example, ``2.0`` means double size. """ __slots__ = ("point", "scale", "x_shift", "y_shift") FOREHEAD: Final[str] = constants.MaskPosition.FOREHEAD """:const:`telegram.constants.MaskPosition.FOREHEAD`""" EYES: Final[str] = constants.MaskPosition.EYES """:const:`telegram.constants.MaskPosition.EYES`""" MOUTH: Final[str] = constants.MaskPosition.MOUTH """:const:`telegram.constants.MaskPosition.MOUTH`""" CHIN: Final[str] = constants.MaskPosition.CHIN """:const:`telegram.constants.MaskPosition.CHIN`""" def __init__( self, point: str, x_shift: float, y_shift: float, scale: float, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.point: str = point self.x_shift: float = x_shift self.y_shift: float = y_shift self.scale: float = scale self._id_attrs = (self.point, self.x_shift, self.y_shift, self.scale) self._freeze() python-telegram-bot-21.1.1/telegram/_files/venue.py000066400000000000000000000105621460724040100222340ustar00rootroot00000000000000#!/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 Venue.""" from typing import TYPE_CHECKING, Optional 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 Venue(TelegramObject): """This object represents a venue. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`location` and :attr:`title` are equal. Note: Foursquare details and Google Place details are mutually exclusive. However, this behaviour is undocumented and might be changed by Telegram. Args: location (:class:`telegram.Location`): Venue location. title (:obj:`str`): Name of the venue. address (:obj:`str`): Address of the venue. foursquare_id (:obj:`str`, optional): Foursquare identifier of the venue. foursquare_type (:obj:`str`, optional): Foursquare type of the venue. (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: location (:class:`telegram.Location`): Venue location. title (:obj:`str`): Name of the venue. address (:obj:`str`): Address of the venue. foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue. foursquare_type (:obj:`str`): Optional. Foursquare type of the venue. (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", "location", "title", ) def __init__( self, location: Location, 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) # Required self.location: Location = location 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.location, self.title) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Venue"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data["location"] = Location.de_json(data.get("location"), bot) return super().de_json(data=data, bot=bot) python-telegram-bot-21.1.1/telegram/_files/video.py000066400000000000000000000100221460724040100222070ustar00rootroot00000000000000#!/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 Video.""" from typing import Optional from telegram._files._basethumbedmedium import _BaseThumbedMedium from telegram._files.photosize import PhotoSize from telegram._utils.types import JSONDict class Video(_BaseThumbedMedium): """This object represents a video file. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. .. versionchanged:: 20.5 |removed_thumb_note| Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. width (:obj:`int`): Video width as defined by sender. height (:obj:`int`): Video height as defined by sender. duration (:obj:`int`): Duration of the video in seconds as defined by sender. file_name (:obj:`str`, optional): Original filename as defined by sender. mime_type (:obj:`str`, optional): MIME type of a file as defined by sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail. .. versionadded:: 20.2 Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. width (:obj:`int`): Video width as defined by sender. height (:obj:`int`): Video height as defined by sender. duration (:obj:`int`): Duration of the video in seconds as defined by sender. file_name (:obj:`str`): Optional. Original filename as defined by sender. mime_type (:obj:`str`): Optional. MIME type of a file as defined by sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail. .. versionadded:: 20.2 """ __slots__ = ("duration", "file_name", "height", "mime_type", "width") def __init__( self, file_id: str, file_unique_id: str, width: int, height: int, duration: int, mime_type: Optional[str] = None, file_size: Optional[int] = None, file_name: Optional[str] = None, thumbnail: Optional[PhotoSize] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__( file_id=file_id, file_unique_id=file_unique_id, file_size=file_size, thumbnail=thumbnail, api_kwargs=api_kwargs, ) with self._unfrozen(): # Required self.width: int = width self.height: int = height self.duration: int = duration # Optional self.mime_type: Optional[str] = mime_type self.file_name: Optional[str] = file_name python-telegram-bot-21.1.1/telegram/_files/videonote.py000066400000000000000000000066741460724040100231170ustar00rootroot00000000000000#!/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 VideoNote.""" from typing import Optional from telegram._files._basethumbedmedium import _BaseThumbedMedium from telegram._files.photosize import PhotoSize from telegram._utils.types import JSONDict class VideoNote(_BaseThumbedMedium): """This object represents a video message (available in Telegram apps as of v.4.0). Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. .. versionchanged:: 20.5 |removed_thumb_note| Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. length (:obj:`int`): Video width and height (diameter of the video message) as defined by sender. duration (:obj:`int`): Duration of the video in seconds as defined by sender. file_size (:obj:`int`, optional): File size in bytes. thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail. .. versionadded:: 20.2 Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. length (:obj:`int`): Video width and height (diameter of the video message) as defined by sender. duration (:obj:`int`): Duration of the video in seconds as defined by sender. file_size (:obj:`int`): Optional. File size in bytes. thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail. .. versionadded:: 20.2 """ __slots__ = ("duration", "length") def __init__( self, file_id: str, file_unique_id: str, length: int, duration: int, file_size: Optional[int] = None, thumbnail: Optional[PhotoSize] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__( file_id=file_id, file_unique_id=file_unique_id, file_size=file_size, thumbnail=thumbnail, api_kwargs=api_kwargs, ) with self._unfrozen(): # Required self.length: int = length self.duration: int = duration python-telegram-bot-21.1.1/telegram/_files/voice.py000066400000000000000000000057271460724040100222260ustar00rootroot00000000000000#!/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 Voice.""" from typing import Optional from telegram._files._basemedium import _BaseMedium from telegram._utils.types import JSONDict class Voice(_BaseMedium): """This object represents a voice note. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. duration (:obj:`int`): Duration of the audio in seconds as defined by sender. mime_type (:obj:`str`, optional): MIME type of the file as defined by sender. file_size (:obj:`int`, optional): File size in bytes. Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. duration (:obj:`int`): Duration of the audio in seconds as defined by sender. mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender. file_size (:obj:`int`): Optional. File size in bytes. """ __slots__ = ("duration", "mime_type") def __init__( self, file_id: str, file_unique_id: str, duration: int, mime_type: Optional[str] = None, file_size: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__( file_id=file_id, file_unique_id=file_unique_id, file_size=file_size, api_kwargs=api_kwargs, ) with self._unfrozen(): # Required self.duration: int = duration # Optional self.mime_type: Optional[str] = mime_type python-telegram-bot-21.1.1/telegram/_forcereply.py000066400000000000000000000104371460724040100221630ustar00rootroot00000000000000#!/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. 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 """ python-telegram-bot-21.1.1/telegram/_forumtopic.py000066400000000000000000000154651460724040100222060ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_games/000077500000000000000000000000001460724040100205265ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_games/__init__.py000066400000000000000000000000001460724040100226250ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_games/callbackgame.py000066400000000000000000000024351460724040100234720ustar00rootroot00000000000000#!/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 CallbackGame.""" from typing import Optional from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict class CallbackGame(TelegramObject): """A placeholder, currently holds no information. Use BotFather to set up your game.""" __slots__ = () def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: super().__init__(api_kwargs=api_kwargs) self._freeze() python-telegram-bot-21.1.1/telegram/_games/game.py000066400000000000000000000176621460724040100220250ustar00rootroot00000000000000#!/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 Game.""" from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple from telegram._files.animation import Animation from telegram._files.photosize import PhotoSize from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot class Game(TelegramObject): """ This object represents a game. Use `BotFather `_ to create and edit games, their short names will act as unique identifiers. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`title`, :attr:`description` and :attr:`photo` are equal. Args: title (:obj:`str`): Title of the game. description (:obj:`str`): Description of the game. photo (Sequence[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game message in chats. .. versionchanged:: 20.0 |sequenceclassargs| text (:obj:`str`, optional): Brief description of the game or high scores included in the game message. Can be automatically edited to include current high scores for the game when the bot calls :meth:`telegram.Bot.set_game_score`, or manually edited using :meth:`telegram.Bot.edit_message_text`. 0-:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters. text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities that appear in text, such as usernames, URLs, bot commands, etc. .. versionchanged:: 20.0 |sequenceclassargs| animation (:class:`telegram.Animation`, optional): Animation that will be displayed in the game message in chats. Upload via `BotFather `_. Attributes: title (:obj:`str`): Title of the game. description (:obj:`str`): Description of the game. photo (Tuple[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game message in chats. .. versionchanged:: 20.0 |tupleclassattrs| text (:obj:`str`): Optional. Brief description of the game or high scores included in the game message. Can be automatically edited to include current high scores for the game when the bot calls :meth:`telegram.Bot.set_game_score`, or manually edited using :meth:`telegram.Bot.edit_message_text`. 0-:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters. text_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. Special entities that appear in text, such as usernames, URLs, bot commands, etc. This tuple is empty if the message does not contain text entities. .. versionchanged:: 20.0 |tupleclassattrs| animation (:class:`telegram.Animation`): Optional. Animation that will be displayed in the game message in chats. Upload via `BotFather `_. """ __slots__ = ( "animation", "description", "photo", "text", "text_entities", "title", ) def __init__( self, title: str, description: str, photo: Sequence[PhotoSize], text: Optional[str] = None, text_entities: Optional[Sequence[MessageEntity]] = None, animation: Optional[Animation] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.title: str = title self.description: str = description self.photo: Tuple[PhotoSize, ...] = parse_sequence_arg(photo) # Optionals self.text: Optional[str] = text self.text_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) self.animation: Optional[Animation] = animation self._id_attrs = (self.title, self.description, self.photo) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Game"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data["photo"] = PhotoSize.de_list(data.get("photo"), bot) data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot) data["animation"] = Animation.de_json(data.get("animation"), bot) return super().de_json(data=data, bot=bot) def parse_text_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 this game has no text. """ if not self.text: raise RuntimeError("This Game has no 'text'.") entity_text = self.text.encode("utf-16-le") entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2] return entity_text.decode("utf-16-le") def parse_text_entities(self, types: Optional[List[str]] = None) -> Dict[MessageEntity, str]: """ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. It contains entities from this message filtered by their :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:`text_entities` attribute, since it calculates the correct substring from the message text based on UTF-16 codepoints. See :attr:`parse_text_entity` for more info. Args: types (List[:obj:`str`], optional): List of :class:`telegram.MessageEntity` types as strings. If the :attr:`~telegram.MessageEntity.type` attribute of an entity is contained in this list, it will be returned. Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`. Returns: Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to the text that belongs to them, calculated based on UTF-16 codepoints. """ if types is None: types = MessageEntity.ALL_TYPES return { entity: self.parse_text_entity(entity) for entity in self.text_entities if entity.type in types } python-telegram-bot-21.1.1/telegram/_games/gamehighscore.py000066400000000000000000000047261460724040100237160ustar00rootroot00000000000000#!/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 GameHighScore.""" 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 GameHighScore(TelegramObject): """This object represents one row of the high scores table for a game. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`position`, :attr:`user` and :attr:`score` are equal. Args: position (:obj:`int`): Position in high score table for the game. user (:class:`telegram.User`): User. score (:obj:`int`): Score. Attributes: position (:obj:`int`): Position in high score table for the game. user (:class:`telegram.User`): User. score (:obj:`int`): Score. """ __slots__ = ("position", "score", "user") def __init__( self, position: int, user: User, score: int, *, api_kwargs: Optional[JSONDict] = None ): super().__init__(api_kwargs=api_kwargs) self.position: int = position self.user: User = user self.score: int = score self._id_attrs = (self.position, self.user, self.score) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["GameHighScore"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data["user"] = User.de_json(data.get("user"), bot) return super().de_json(data=data, bot=bot) python-telegram-bot-21.1.1/telegram/_giveaway.py000066400000000000000000000350061460724040100216240ustar00rootroot00000000000000#!/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: "Bot") -> Optional["Giveaway"]: """See :obj:`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: "Bot") -> Optional["GiveawayWinners"]: """See :obj:`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: "Bot") -> Optional["GiveawayCompleted"]: """See :obj:`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) python-telegram-bot-21.1.1/telegram/_inline/000077500000000000000000000000001460724040100207105ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_inline/__init__.py000066400000000000000000000000001460724040100230070ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_inline/inlinekeyboardbutton.py000066400000000000000000000355061460724040100255260ustar00rootroot00000000000000#!/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: * You must use exactly one of the optional fields. 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` maybe 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 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 (:obj:`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. .. 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. Can be empty, in which case just the bot's username will be inserted. This offers an easy way for users to start using your bot in inline mode when they are currently in a private chat with it. Especially useful when combined with ``switch_pm*`` actions - in this case the user will be automatically returned to the chat they switched from, skipping the chat selection screen. Tip: This is similar to the new 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. Can 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. callback_game (:class:`telegram.CallbackGame`, optional): Description of the game that will be launched when the user presses the button. 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. This type of button **must** always be the **first** button in the first row and can only be used in invoice messages. switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`, optional): 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. .. 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 button is pressed, UTF-8 :tg-const:`telegram.InlineKeyboardButton.MIN_CALLBACK_DATA`- :tg-const:`telegram.InlineKeyboardButton.MAX_CALLBACK_DATA` bytes. web_app (:obj:`telegram.WebAppInfo`): Optional. Description of the `Web App `_ 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. .. 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. Can be empty, in which case just the bot's username will be inserted. This offers an easy way for users to start using your bot in inline mode when they are currently in a private chat with it. Especially useful when combined with ``switch_pm*`` actions - in this case the user will be automatically returned to the chat they switched from, skipping the chat selection screen. Tip: This is similar to the new 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. Can 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. callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will be launched when the user presses the button. 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. This type of button **must** always be the **first** button in the first row and can only be used in invoice messages. switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`): Optional. 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. .. 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: "Bot") -> 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 """ python-telegram-bot-21.1.1/telegram/_inline/inlinekeyboardmarkup.py000066400000000000000000000127411460724040100255060ustar00rootroot00000000000000#!/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:: An 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: "Bot") -> 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] python-telegram-bot-21.1.1/telegram/_inline/inlinequery.py000066400000000000000000000217731460724040100236400ustar00rootroot00000000000000#!/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: "Bot") -> 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 """ python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresult.py000066400000000000000000000054261460724040100250740ustar00rootroot00000000000000#!/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 """ python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultarticle.py000066400000000000000000000122321460724040100264310ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultaudio.py000066400000000000000000000130771460724040100261170ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultcachedaudio.py000066400000000000000000000120221460724040100272340ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultcacheddocument.py000066400000000000000000000130421460724040100277540ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultcachedgif.py000066400000000000000000000124571460724040100267140ustar00rootroot00000000000000#!/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. 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. """ __slots__ = ( "caption", "caption_entities", "gif_file_id", "input_message_content", "parse_mode", "reply_markup", "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, *, 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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultcachedmpeg4gif.py000066400000000000000000000126121460724040100276420ustar00rootroot00000000000000#!/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. 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. """ __slots__ = ( "caption", "caption_entities", "input_message_content", "mpeg4_file_id", "parse_mode", "reply_markup", "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, *, 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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultcachedphoto.py000066400000000000000000000130571460724040100272750ustar00rootroot00000000000000#!/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. 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. """ __slots__ = ( "caption", "caption_entities", "description", "input_message_content", "parse_mode", "photo_file_id", "reply_markup", "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, *, 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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultcachedsticker.py000066400000000000000000000071131460724040100276040ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultcachedvideo.py000066400000000000000000000127221460724040100272500ustar00rootroot00000000000000#!/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. 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. """ __slots__ = ( "caption", "caption_entities", "description", "input_message_content", "parse_mode", "reply_markup", "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, *, 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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultcachedvoice.py000066400000000000000000000123471460724040100272520ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultcontact.py000066400000000000000000000126321460724040100264450ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultdocument.py000066400000000000000000000157471460724040100266420ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultgame.py000066400000000000000000000052571460724040100257300ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultgif.py000066400000000000000000000172061460724040100255610ustar00rootroot00000000000000#!/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`, optional): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. Warning: The Bot API does **not** define this as an optional argument. It is formally optional for backwards compatibility with the deprecated :paramref:`thumb_url`. If you pass neither :paramref:`thumbnail_url` nor :paramref:`thumb_url`, :class:`ValueError` will be raised. .. versionadded:: 20.2 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. Raises: :class:`ValueError`: If neither :paramref:`thumbnail_url` nor :paramref:`thumb_url` is supplied or if both are supplied and are not equal. Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.GIF`. id (:obj:`str`): Unique identifier for this result, :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. gif_url (:obj:`str`): A valid URL for the GIF file. File size must not exceed 1MB. gif_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. """ __slots__ = ( "caption", "caption_entities", "gif_duration", "gif_height", "gif_url", "gif_width", "input_message_content", "parse_mode", "reply_markup", "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, *, 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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultlocation.py000066400000000000000000000221601460724040100266170ustar00rootroot00000000000000#!/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`. 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 """ python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultmpeg4gif.py000066400000000000000000000173571460724040100265250ustar00rootroot00000000000000#!/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`, optional): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. Warning: The Bot API does **not** define this as an optional argument. It is formally optional for backwards compatibility with the deprecated :paramref:`thumb_url`. If you pass neither :paramref:`thumbnail_url` nor :paramref:`thumb_url`, :class:`ValueError` will be raised. .. versionadded:: 20.2 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. Raises: :class:`ValueError`: If neither :paramref:`thumbnail_url` nor :paramref:`thumb_url` is supplied or if both are supplied and are not equal. Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.MPEG4GIF`. id (:obj:`str`): Unique identifier for this result, :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB. mpeg4_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. """ __slots__ = ( "caption", "caption_entities", "input_message_content", "mpeg4_duration", "mpeg4_height", "mpeg4_url", "mpeg4_width", "parse_mode", "reply_markup", "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, *, 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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultphoto.py000066400000000000000000000157301460724040100261450ustar00rootroot00000000000000#!/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`, optional): URL of the thumbnail for the photo. Warning: The Bot API does **not** define this as an optional argument. It is formally optional for backwards compatibility with the deprecated :paramref:`thumb_url`. If you pass neither :paramref:`thumbnail_url` nor :paramref:`thumb_url`, :class:`ValueError` will be raised. .. versionadded:: 20.2 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. Raises: :class:`ValueError`: If neither :paramref:`thumbnail_url` nor :paramref:`thumb_url` is supplied or if both are supplied and are not equal. Attributes: type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.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. """ __slots__ = ( "caption", "caption_entities", "description", "input_message_content", "parse_mode", "photo_height", "photo_url", "photo_width", "reply_markup", "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, *, 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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultsbutton.py000066400000000000000000000123511460724040100265060ustar00rootroot00000000000000#!/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: "Bot") -> 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`""" python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultvenue.py000066400000000000000000000160041460724040100261310ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultvideo.py000066400000000000000000000206421460724040100261200ustar00rootroot00000000000000#!/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. Warning: The Bot API does **not** define this as an optional argument. It is formally optional for backwards compatibility with the deprecated :paramref:`thumb_url`. If you pass neither :paramref:`thumbnail_url` nor :paramref:`thumb_url`, :class:`ValueError` will be raised. .. versionadded:: 20.2 title (:obj:`str`, optional): Title for the result. Warning: The Bot API does **not** define this as an optional argument. It is formally optional to ensure backwards compatibility of :paramref:`thumbnail_url` with the deprecated :paramref:`thumb_url`, which required that :paramref:`thumbnail_url` become optional. :class:`TypeError` will be raised if no ``title`` is passed. 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). Raises: :class:`ValueError`: If neither :paramref:`thumbnail_url` nor :paramref:`thumb_url` is supplied or if both are supplied and are not equal. :class:`TypeError`: If no :paramref:`title` is passed. 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). """ __slots__ = ( "caption", "caption_entities", "description", "input_message_content", "mime_type", "parse_mode", "reply_markup", "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, *, 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 python-telegram-bot-21.1.1/telegram/_inline/inlinequeryresultvoice.py000066400000000000000000000127231460724040100261200ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/telegram/_inline/inputcontactmessagecontent.py000066400000000000000000000052721460724040100267430ustar00rootroot00000000000000#!/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,) python-telegram-bot-21.1.1/telegram/_inline/inputinvoicemessagecontent.py000066400000000000000000000310361460724040100267410ustar00rootroot00000000000000#!/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 for your internal processes. provider_token (:obj:`str`): Payment provider token, obtained via `@Botfather `_. currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on `currencies `_ prices (Sequence[:class:`telegram.LabeledPrice`]): Price breakdown, a list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) .. 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``. 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. need_phone_number (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's phone number to complete the order need_email (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's email address to complete the order. need_shipping_address (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's shipping address to complete the order send_phone_number_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's phone number should be sent to provider. send_email_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's email address should be sent to provider. is_flexible (:obj:`bool`, optional): Pass :obj:`True`, if the final price depends on the shipping method. 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 for your internal processes. provider_token (:obj:`str`): Payment provider token, obtained via `@Botfather `_. currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on `currencies `_ prices (Tuple[:class:`telegram.LabeledPrice`]): Price breakdown, a list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) .. 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``. 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. need_phone_number (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's phone number to complete the order need_email (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's email address to complete the order. need_shipping_address (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's shipping address to complete the order send_phone_number_to_provider (:obj:`bool`): Optional. Pass :obj:`True`, if user's phone number should be sent to provider. send_email_to_provider (:obj:`bool`): Optional. Pass :obj:`True`, if user's email address should be sent to provider. is_flexible (:obj:`bool`): Optional. Pass :obj:`True`, if the final price depends on the shipping method. """ __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: str, 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: 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: "Bot" ) -> 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) python-telegram-bot-21.1.1/telegram/_inline/inputlocationmessagecontent.py000066400000000000000000000146721460724040100271240ustar00rootroot00000000000000#!/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`. 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 """ python-telegram-bot-21.1.1/telegram/_inline/inputmessagecontent.py000066400000000000000000000030371460724040100253640ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_inline/inputtextmessagecontent.py000066400000000000000000000113041460724040100262650ustar00rootroot00000000000000#!/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,) python-telegram-bot-21.1.1/telegram/_inline/inputvenuemessagecontent.py000066400000000000000000000107211460724040100264250ustar00rootroot00000000000000#!/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, ) python-telegram-bot-21.1.1/telegram/_keyboardbutton.py000066400000000000000000000217201460724040100230420ustar00rootroot00000000000000#!/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. 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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_keyboardbuttonpolltype.py000066400000000000000000000047631460724040100246430ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_keyboardbuttonrequest.py000066400000000000000000000310631460724040100244540ustar00rootroot00000000000000#!/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. 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. .. seealso:: `Telegram Docs on requesting users \ `_ .. 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. 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. .. seealso:: `Telegram Docs on requesting chats \ `_ .. 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: "Bot" ) -> 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) python-telegram-bot-21.1.1/telegram/_linkpreviewoptions.py000066400000000000000000000106221460724040100237600ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_loginurl.py000066400000000000000000000120161460724040100216370ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_menubutton.py000066400000000000000000000153101460724040100222040ustar00rootroot00000000000000#!/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: "Bot") -> 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`): 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 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`. 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`. """ __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: "Bot") -> 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() python-telegram-bot-21.1.1/telegram/_message.py000066400000000000000000006142331460724040100214410ustar00rootroot00000000000000#!/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._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._passport.passportdata import PassportData from telegram._payment.invoice import Invoice 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.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, 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: "Bot", 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) 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; empty for messages sent to channels. For backward compatibility, this will contain a fake sender user in non-channel chats, if the message was sent on behalf of a chat. sender_chat (:class:`telegram.Chat`, optional): Sender of the message, sent on behalf of a chat. For example, the channel itself for channel posts, the supergroup itself for messages from anonymous group administrators, the linked channel for messages automatically forwarded to the discussion group. For backward compatibility, :attr:`from_user` contains a fake sender user in non-channel chats, if the message was sent on behalf of a chat. 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 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| 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. 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, 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. successful_payment (:class:`telegram.SuccessfulPayment`, optional): Message is a service message about a successful payment, information about the payment. connected_website (:obj:`str`, optional): The domain name of the website on which the user has logged in. 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 (:obj:`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 Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. from_user (:class:`telegram.User`): Optional. Sender of the message; empty for messages sent to channels. For backward compatibility, this will contain a fake sender user in non-channel chats, if the message was sent on behalf of a chat. sender_chat (:class:`telegram.Chat`): Optional. Sender of the message, sent on behalf of a chat. For example, the channel itself for channel posts, the supergroup itself for messages from anonymous group administrators, the linked channel for messages automatically forwarded to the discussion group. For backward compatibility, :attr:`from_user` contains a fake sender user in non-channel chats, if the message was sent on behalf of a chat. 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 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| 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. 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, 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. successful_payment (:class:`telegram.SuccessfulPayment`): Optional. Message is a service message about a successful payment, information about the payment. connected_website (:obj:`str`): Optional. The domain name of the website on which the user has logged in. 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 (:obj:`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 .. |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_shared", "connected_website", "contact", "delete_chat_photo", "dice", "document", "edit_date", "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", "passport_data", "photo", "pinned_message", "poll", "proximity_alert_triggered", "quote", "reply_markup", "reply_to_message", "reply_to_story", "sender_boost_count", "sender_business_bot", "sender_chat", "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, *, 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._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: "Bot") -> 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) # 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], Poll, Sticker, Story, SuccessfulPayment, Venue, Video, VideoNote, Voice, None, ]: """If this message is neither a plain text message nor a status update, this gives the attachment that this message was sent with. This 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.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. """ 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] 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("utf-16-le") utf16_quote = quote.encode("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( "The `quote` parameter is deprecated in favor of the `do_quote` parameter. Please " "update your code to use `do_quote` instead.", PTBDeprecationWarning, 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) async def reply_poll( self, question: str, options: Sequence[str], 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) async def reply_invoice( self, title: str, description: str, payload: str, provider_token: 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, *, 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, ) 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.Chat.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, *, 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, ) 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, *, 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, ) 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, *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. 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, ) 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, *, read_timeout: ODVInput[float] = DEFAULT_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, *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. 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, ) 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, *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. 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, ) 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, *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. 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, ) 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, *, 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, *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. 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, inline_message_id=None, ) 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, *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. 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, ) 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, *args, **kwargs ) For the documentation of the arguments, please see :meth:`telegram.Bot.stop_poll`. 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, ) 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, *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.chat_id, message_id=self.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, ) 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, *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.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 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'.") entity_text = self.text.encode("utf-16-le") entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2] return entity_text.decode("utf-16-le") 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'.") entity_text = self.caption.encode("utf-16-le") entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2] return entity_text.decode("utf-16-le") 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. """ if types is None: types = MessageEntity.ALL_TYPES return { entity: self.parse_entity(entity) for entity in self.entities if entity.type in 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. """ if types is None: types = MessageEntity.ALL_TYPES return { entity: self.parse_caption_entity(entity) for entity in self.caption_entities if entity.type in 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("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.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("utf-16-le") ) + insert ) last_offset = entity.offset - offset + entity.length # see comment above html_text += escape(utf_16_text[last_offset * 2 :].decode("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.UNDERLINE, MessageEntity.STRIKETHROUGH, MessageEntity.SPOILER, MessageEntity.BLOCKQUOTE, MessageEntity.CUSTOM_EMOJI, ): 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("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 == MessageEntity.BLOCKQUOTE: insert = ">" + "\n>".join(escaped_text.splitlines()) 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( "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("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 ) python-telegram-bot-21.1.1/telegram/_messageautodeletetimerchanged.py000066400000000000000000000037371460724040100260710ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_messageentity.py000066400000000000000000000213461460724040100226730ustar00rootroot00000000000000#!/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.""" from typing import TYPE_CHECKING, Final, List, Optional from telegram import constants from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum from telegram._utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot 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: "Bot") -> 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) MENTION: Final[str] = constants.MessageEntityType.MENTION """:const:`telegram.constants.MessageEntityType.MENTION`""" HASHTAG: Final[str] = constants.MessageEntityType.HASHTAG """:const:`telegram.constants.MessageEntityType.HASHTAG`""" CASHTAG: Final[str] = constants.MessageEntityType.CASHTAG """:const:`telegram.constants.MessageEntityType.CASHTAG`""" PHONE_NUMBER: Final[str] = constants.MessageEntityType.PHONE_NUMBER """:const:`telegram.constants.MessageEntityType.PHONE_NUMBER`""" BOT_COMMAND: Final[str] = constants.MessageEntityType.BOT_COMMAND """:const:`telegram.constants.MessageEntityType.BOT_COMMAND`""" URL: Final[str] = constants.MessageEntityType.URL """:const:`telegram.constants.MessageEntityType.URL`""" EMAIL: Final[str] = constants.MessageEntityType.EMAIL """:const:`telegram.constants.MessageEntityType.EMAIL`""" BOLD: Final[str] = constants.MessageEntityType.BOLD """:const:`telegram.constants.MessageEntityType.BOLD`""" ITALIC: Final[str] = constants.MessageEntityType.ITALIC """:const:`telegram.constants.MessageEntityType.ITALIC`""" CODE: Final[str] = constants.MessageEntityType.CODE """:const:`telegram.constants.MessageEntityType.CODE`""" PRE: Final[str] = constants.MessageEntityType.PRE """:const:`telegram.constants.MessageEntityType.PRE`""" 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`""" STRIKETHROUGH: Final[str] = constants.MessageEntityType.STRIKETHROUGH """:const:`telegram.constants.MessageEntityType.STRIKETHROUGH`""" SPOILER: Final[str] = constants.MessageEntityType.SPOILER """:const:`telegram.constants.MessageEntityType.SPOILER` .. versionadded:: 13.10 """ CUSTOM_EMOJI: Final[str] = constants.MessageEntityType.CUSTOM_EMOJI """:const:`telegram.constants.MessageEntityType.CUSTOM_EMOJI` .. versionadded:: 20.0 """ BLOCKQUOTE: Final[str] = constants.MessageEntityType.BLOCKQUOTE """:const:`telegram.constants.MessageEntityType.BLOCKQUOTE` .. versionadded:: 20.8 """ ALL_TYPES: Final[List[str]] = list(constants.MessageEntityType) """List[:obj:`str`]: A list of all available message entity types.""" python-telegram-bot-21.1.1/telegram/_messageid.py000066400000000000000000000032501460724040100217450ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_messageorigin.py000066400000000000000000000235611460724040100226470ustar00rootroot00000000000000#!/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: "Bot") -> 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 python-telegram-bot-21.1.1/telegram/_messagereactionupdated.py000066400000000000000000000174261460724040100245360ustar00rootroot00000000000000#!/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: "Bot" ) -> 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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_passport/000077500000000000000000000000001460724040100213055ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_passport/__init__.py000066400000000000000000000000001460724040100234040ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_passport/credentials.py000066400000000000000000000543421460724040100241640ustar00rootroot00000000000000#!/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, redefined-builtin import json from base64 import b64decode from typing import TYPE_CHECKING, Optional, Sequence, Tuple, no_type_check try: from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers.algorithms import AES from cryptography.hazmat.primitives.ciphers.modes import CBC from cryptography.hazmat.primitives.hashes import SHA1, SHA256, SHA512, Hash CRYPTO_INSTALLED = True except ImportError: default_backend = None # type: ignore[assignment] MGF1, OAEP, Cipher, AES, CBC = (None, None, None, None, None) # type: ignore[misc,assignment] SHA1, SHA256, SHA512, Hash = (None, None, None, None) # type: ignore[misc,assignment] CRYPTO_INSTALLED = False from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict from telegram.error import PassportDecryptionError if TYPE_CHECKING: from telegram import Bot @no_type_check def decrypt(secret, hash, data): """ Decrypt per telegram docs at https://core.telegram.org/passport. Args: secret (:obj:`str` or :obj:`bytes`): The encryption secret, either as bytes or as a base64 encoded string. hash (:obj:`str` or :obj:`bytes`): The hash, either as bytes or as a base64 encoded string. data (:obj:`str` or :obj:`bytes`): The data to decrypt, either as bytes or as a base64 encoded string. file (:obj:`bool`): Force data to be treated as raw data, instead of trying to b64decode it. Raises: :class:`PassportDecryptionError`: Given hash does not match hash of decrypted data. Returns: :obj:`bytes`: The decrypted data as bytes. """ if not CRYPTO_INSTALLED: raise RuntimeError( "To use Telegram Passports, PTB must be installed via `pip install " '"python-telegram-bot[passport]"`.' ) # Make a SHA512 hash of secret + update digest = Hash(SHA512(), backend=default_backend()) digest.update(secret + hash) secret_hash_hash = digest.finalize() # First 32 chars is our key, next 16 is the initialisation vector key, init_vector = secret_hash_hash[:32], secret_hash_hash[32 : 32 + 16] # Init a AES-CBC cipher and decrypt the data cipher = Cipher(AES(key), CBC(init_vector), backend=default_backend()) decryptor = cipher.decryptor() data = decryptor.update(data) + decryptor.finalize() # Calculate SHA256 hash of the decrypted data digest = Hash(SHA256(), backend=default_backend()) digest.update(data) data_hash = digest.finalize() # If the newly calculated hash did not match the one telegram gave us if data_hash != hash: # Raise a error that is caught inside telegram.PassportData and transformed into a warning raise PassportDecryptionError(f"Hashes are not equal! {data_hash} != {hash}") # Return data without padding return data[data[0] :] @no_type_check def decrypt_json(secret, hash, data): """Decrypts data using secret and hash and then decodes utf-8 string and loads json""" return json.loads(decrypt(secret, hash, data).decode("utf-8")) class EncryptedCredentials(TelegramObject): """Contains data required for decrypting and authenticating EncryptedPassportElement. See the Telegram Passport Documentation for a complete description of the data decryption and authentication processes. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`data`, :attr:`hash` and :attr:`secret` are equal. Note: This object is decrypted only when originating from :obj:`telegram.PassportData.decrypted_credentials`. Args: data (:class:`telegram.Credentials` | :obj:`str`): Decrypted data with unique user's nonce, data hashes and secrets used for EncryptedPassportElement decryption and authentication or base64 encrypted data. hash (:obj:`str`): Base64-encoded data hash for data authentication. secret (:obj:`str`): Decrypted or encrypted secret used for decryption. Attributes: data (:class:`telegram.Credentials` | :obj:`str`): Decrypted data with unique user's nonce, data hashes and secrets used for EncryptedPassportElement decryption and authentication or base64 encrypted data. hash (:obj:`str`): Base64-encoded data hash for data authentication. secret (:obj:`str`): Decrypted or encrypted secret used for decryption. """ __slots__ = ( "_decrypted_data", "_decrypted_secret", "data", "hash", "secret", ) def __init__( self, data: str, hash: str, secret: str, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.data: str = data self.hash: str = hash self.secret: str = secret self._id_attrs = (self.data, self.hash, self.secret) self._decrypted_secret: Optional[bytes] = None self._decrypted_data: Optional[Credentials] = None self._freeze() @property def decrypted_secret(self) -> bytes: """ :obj:`bytes`: Lazily decrypt and return secret. Raises: telegram.error.PassportDecryptionError: Decryption failed. Usually due to bad private/public key but can also suggest malformed/tampered data. """ if self._decrypted_secret is None: if not CRYPTO_INSTALLED: raise RuntimeError( "To use Telegram Passports, PTB must be installed via `pip install " '"python-telegram-bot[passport]"`.' ) # Try decrypting according to step 1 at # https://core.telegram.org/passport#decrypting-data # We make sure to base64 decode the secret first. # Telegram says to use OAEP padding so we do that. The Mask Generation Function # is the default for OAEP, the algorithm is the default for PHP which is what # Telegram's backend servers run. try: self._decrypted_secret = self.get_bot().private_key.decrypt( # type: ignore b64decode(self.secret), OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None), # skipcq ) except ValueError as exception: # If decryption fails raise exception raise PassportDecryptionError(exception) from exception return self._decrypted_secret @property def decrypted_data(self) -> "Credentials": """ :class:`telegram.Credentials`: Lazily decrypt and return credentials data. This object also contains the user specified nonce as `decrypted_data.nonce`. Raises: telegram.error.PassportDecryptionError: Decryption failed. Usually due to bad private/public key but can also suggest malformed/tampered data. """ if self._decrypted_data is None: self._decrypted_data = Credentials.de_json( decrypt_json(self.decrypted_secret, b64decode(self.hash), b64decode(self.data)), self.get_bot(), ) return self._decrypted_data # type: ignore[return-value] class Credentials(TelegramObject): """ Attributes: secure_data (:class:`telegram.SecureData`): Credentials for encrypted data nonce (:obj:`str`): Bot-specified nonce """ __slots__ = ("nonce", "secure_data") def __init__( self, secure_data: "SecureData", nonce: str, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.secure_data: SecureData = secure_data self.nonce: str = nonce self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Credentials"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data["secure_data"] = SecureData.de_json(data.get("secure_data"), bot=bot) return super().de_json(data=data, bot=bot) class SecureData(TelegramObject): """ This object represents the credentials that were used to decrypt the encrypted data. All fields are optional and depend on fields that were requested. Args: personal_details (:class:`telegram.SecureValue`, optional): Credentials for encrypted personal details. passport (:class:`telegram.SecureValue`, optional): Credentials for encrypted passport. internal_passport (:class:`telegram.SecureValue`, optional): Credentials for encrypted internal passport. driver_license (:class:`telegram.SecureValue`, optional): Credentials for encrypted driver license. identity_card (:class:`telegram.SecureValue`, optional): Credentials for encrypted ID card address (:class:`telegram.SecureValue`, optional): Credentials for encrypted residential address. utility_bill (:class:`telegram.SecureValue`, optional): Credentials for encrypted utility bill. bank_statement (:class:`telegram.SecureValue`, optional): Credentials for encrypted bank statement. rental_agreement (:class:`telegram.SecureValue`, optional): Credentials for encrypted rental agreement. passport_registration (:class:`telegram.SecureValue`, optional): Credentials for encrypted registration from internal passport. temporary_registration (:class:`telegram.SecureValue`, optional): Credentials for encrypted temporary registration. Attributes: personal_details (:class:`telegram.SecureValue`): Optional. Credentials for encrypted personal details. passport (:class:`telegram.SecureValue`): Optional. Credentials for encrypted passport. internal_passport (:class:`telegram.SecureValue`): Optional. Credentials for encrypted internal passport. driver_license (:class:`telegram.SecureValue`): Optional. Credentials for encrypted driver license. identity_card (:class:`telegram.SecureValue`): Optional. Credentials for encrypted ID card address (:class:`telegram.SecureValue`): Optional. Credentials for encrypted residential address. utility_bill (:class:`telegram.SecureValue`): Optional. Credentials for encrypted utility bill. bank_statement (:class:`telegram.SecureValue`): Optional. Credentials for encrypted bank statement. rental_agreement (:class:`telegram.SecureValue`): Optional. Credentials for encrypted rental agreement. passport_registration (:class:`telegram.SecureValue`): Optional. Credentials for encrypted registration from internal passport. temporary_registration (:class:`telegram.SecureValue`): Optional. Credentials for encrypted temporary registration. """ __slots__ = ( "address", "bank_statement", "driver_license", "identity_card", "internal_passport", "passport", "passport_registration", "personal_details", "rental_agreement", "temporary_registration", "utility_bill", ) def __init__( self, personal_details: Optional["SecureValue"] = None, passport: Optional["SecureValue"] = None, internal_passport: Optional["SecureValue"] = None, driver_license: Optional["SecureValue"] = None, identity_card: Optional["SecureValue"] = None, address: Optional["SecureValue"] = None, utility_bill: Optional["SecureValue"] = None, bank_statement: Optional["SecureValue"] = None, rental_agreement: Optional["SecureValue"] = None, passport_registration: Optional["SecureValue"] = None, temporary_registration: Optional["SecureValue"] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Optionals self.temporary_registration: Optional[SecureValue] = temporary_registration self.passport_registration: Optional[SecureValue] = passport_registration self.rental_agreement: Optional[SecureValue] = rental_agreement self.bank_statement: Optional[SecureValue] = bank_statement self.utility_bill: Optional[SecureValue] = utility_bill self.address: Optional[SecureValue] = address self.identity_card: Optional[SecureValue] = identity_card self.driver_license: Optional[SecureValue] = driver_license self.internal_passport: Optional[SecureValue] = internal_passport self.passport: Optional[SecureValue] = passport self.personal_details: Optional[SecureValue] = personal_details self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SecureData"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data["temporary_registration"] = SecureValue.de_json( data.get("temporary_registration"), bot=bot ) data["passport_registration"] = SecureValue.de_json( data.get("passport_registration"), bot=bot ) data["rental_agreement"] = SecureValue.de_json(data.get("rental_agreement"), bot=bot) data["bank_statement"] = SecureValue.de_json(data.get("bank_statement"), bot=bot) data["utility_bill"] = SecureValue.de_json(data.get("utility_bill"), bot=bot) data["address"] = SecureValue.de_json(data.get("address"), bot=bot) data["identity_card"] = SecureValue.de_json(data.get("identity_card"), bot=bot) data["driver_license"] = SecureValue.de_json(data.get("driver_license"), bot=bot) data["internal_passport"] = SecureValue.de_json(data.get("internal_passport"), bot=bot) data["passport"] = SecureValue.de_json(data.get("passport"), bot=bot) data["personal_details"] = SecureValue.de_json(data.get("personal_details"), bot=bot) return super().de_json(data=data, bot=bot) class SecureValue(TelegramObject): """ This object represents the credentials that were used to decrypt the encrypted value. All fields are optional and depend on the type of field. Args: data (:class:`telegram.DataCredentials`, optional): Credentials for encrypted Telegram Passport data. Available for "personal_details", "passport", "driver_license", "identity_card", "identity_passport" and "address" types. front_side (:class:`telegram.FileCredentials`, optional): Credentials for encrypted document's front side. Available for "passport", "driver_license", "identity_card" and "internal_passport". reverse_side (:class:`telegram.FileCredentials`, optional): Credentials for encrypted document's reverse side. Available for "driver_license" and "identity_card". selfie (:class:`telegram.FileCredentials`, optional): Credentials for encrypted selfie of the user with a document. Can be available for "passport", "driver_license", "identity_card" and "internal_passport". translation (List[:class:`telegram.FileCredentials`], optional): Credentials for an encrypted translation of the document. Available for "passport", "driver_license", "identity_card", "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration". files (List[:class:`telegram.FileCredentials`], optional): Credentials for encrypted files. Available for "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. Attributes: data (:class:`telegram.DataCredentials`): Optional. Credentials for encrypted Telegram Passport data. Available for "personal_details", "passport", "driver_license", "identity_card", "identity_passport" and "address" types. front_side (:class:`telegram.FileCredentials`): Optional. Credentials for encrypted document's front side. Available for "passport", "driver_license", "identity_card" and "internal_passport". reverse_side (:class:`telegram.FileCredentials`): Optional. Credentials for encrypted document's reverse side. Available for "driver_license" and "identity_card". selfie (:class:`telegram.FileCredentials`): Optional. Credentials for encrypted selfie of the user with a document. Can be available for "passport", "driver_license", "identity_card" and "internal_passport". translation (Tuple[:class:`telegram.FileCredentials`]): Optional. Credentials for an encrypted translation of the document. Available for "passport", "driver_license", "identity_card", "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration". .. versionchanged:: 20.0 |tupleclassattrs| files (Tuple[:class:`telegram.FileCredentials`]): Optional. Credentials for encrypted files. Available for "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. .. versionchanged:: 20.0 * |tupleclassattrs| * |alwaystuple| """ __slots__ = ("data", "files", "front_side", "reverse_side", "selfie", "translation") def __init__( self, data: Optional["DataCredentials"] = None, front_side: Optional["FileCredentials"] = None, reverse_side: Optional["FileCredentials"] = None, selfie: Optional["FileCredentials"] = None, files: Optional[Sequence["FileCredentials"]] = None, translation: Optional[Sequence["FileCredentials"]] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.data: Optional[DataCredentials] = data self.front_side: Optional[FileCredentials] = front_side self.reverse_side: Optional[FileCredentials] = reverse_side self.selfie: Optional[FileCredentials] = selfie self.files: Tuple[FileCredentials, ...] = parse_sequence_arg(files) self.translation: Tuple[FileCredentials, ...] = parse_sequence_arg(translation) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SecureValue"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data["data"] = DataCredentials.de_json(data.get("data"), bot=bot) data["front_side"] = FileCredentials.de_json(data.get("front_side"), bot=bot) data["reverse_side"] = FileCredentials.de_json(data.get("reverse_side"), bot=bot) data["selfie"] = FileCredentials.de_json(data.get("selfie"), bot=bot) data["files"] = FileCredentials.de_list(data.get("files"), bot=bot) data["translation"] = FileCredentials.de_list(data.get("translation"), bot=bot) return super().de_json(data=data, bot=bot) class _CredentialsBase(TelegramObject): """Base class for DataCredentials and FileCredentials.""" __slots__ = ("data_hash", "file_hash", "hash", "secret") def __init__( self, hash: str, secret: str, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) with self._unfrozen(): self.hash: str = hash self.secret: str = secret # Aliases just to be sure self.file_hash: str = self.hash self.data_hash: str = self.hash class DataCredentials(_CredentialsBase): """ These credentials can be used to decrypt encrypted data from the data field in EncryptedPassportData. Args: data_hash (:obj:`str`): Checksum of encrypted data secret (:obj:`str`): Secret of encrypted data Attributes: hash (:obj:`str`): Checksum of encrypted data secret (:obj:`str`): Secret of encrypted data """ __slots__ = () def __init__(self, data_hash: str, secret: str, *, api_kwargs: Optional[JSONDict] = None): super().__init__(hash=data_hash, secret=secret, api_kwargs=api_kwargs) self._freeze() class FileCredentials(_CredentialsBase): """ These credentials can be used to decrypt encrypted files from the front_side, reverse_side, selfie and files fields in EncryptedPassportData. Args: file_hash (:obj:`str`): Checksum of encrypted file secret (:obj:`str`): Secret of encrypted file Attributes: hash (:obj:`str`): Checksum of encrypted file secret (:obj:`str`): Secret of encrypted file """ __slots__ = () def __init__(self, file_hash: str, secret: str, *, api_kwargs: Optional[JSONDict] = None): super().__init__(hash=file_hash, secret=secret, api_kwargs=api_kwargs) self._freeze() python-telegram-bot-21.1.1/telegram/_passport/data.py000066400000000000000000000143451460724040100225770ustar00rootroot00000000000000#!/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 Optional from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict class PersonalDetails(TelegramObject): """ This object represents personal details. Args: first_name (:obj:`str`): First Name. middle_name (:obj:`str`): Optional. First Name. last_name (:obj:`str`): Last Name. birth_date (:obj:`str`): Date of birth in DD.MM.YYYY format. gender (:obj:`str`): Gender, male or female. country_code (:obj:`str`): Citizenship (ISO 3166-1 alpha-2 country code). residence_country_code (:obj:`str`): Country of residence (ISO 3166-1 alpha-2 country code). first_name_native (:obj:`str`): First Name in the language of the user's country of residence. middle_name_native (:obj:`str`): Optional. Middle Name in the language of the user's country of residence. last_name_native (:obj:`str`): Last Name in the language of the user's country of residence. Attributes: first_name (:obj:`str`): First Name. middle_name (:obj:`str`): Optional. First Name. last_name (:obj:`str`): Last Name. birth_date (:obj:`str`): Date of birth in DD.MM.YYYY format. gender (:obj:`str`): Gender, male or female. country_code (:obj:`str`): Citizenship (ISO 3166-1 alpha-2 country code). residence_country_code (:obj:`str`): Country of residence (ISO 3166-1 alpha-2 country code). first_name_native (:obj:`str`): First Name in the language of the user's country of residence. middle_name_native (:obj:`str`): Optional. Middle Name in the language of the user's country of residence. last_name_native (:obj:`str`): Last Name in the language of the user's country of residence. """ __slots__ = ( "birth_date", "country_code", "first_name", "first_name_native", "gender", "last_name", "last_name_native", "middle_name", "middle_name_native", "residence_country_code", ) def __init__( self, first_name: str, last_name: str, birth_date: str, gender: str, country_code: str, residence_country_code: str, first_name_native: Optional[str] = None, last_name_native: Optional[str] = None, middle_name: Optional[str] = None, middle_name_native: Optional[str] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.first_name: str = first_name self.last_name: str = last_name self.middle_name: Optional[str] = middle_name self.birth_date: str = birth_date self.gender: str = gender self.country_code: str = country_code self.residence_country_code: str = residence_country_code self.first_name_native: Optional[str] = first_name_native self.last_name_native: Optional[str] = last_name_native self.middle_name_native: Optional[str] = middle_name_native self._freeze() class ResidentialAddress(TelegramObject): """ This object represents a residential address. Args: street_line1 (:obj:`str`): First line for the address. street_line2 (:obj:`str`): Optional. Second line for the address. city (:obj:`str`): City. state (:obj:`str`): Optional. State. country_code (:obj:`str`): ISO 3166-1 alpha-2 country code. post_code (:obj:`str`): Address post code. Attributes: street_line1 (:obj:`str`): First line for the address. street_line2 (:obj:`str`): Optional. Second line for the address. city (:obj:`str`): City. state (:obj:`str`): Optional. State. country_code (:obj:`str`): ISO 3166-1 alpha-2 country code. post_code (:obj:`str`): Address post code. """ __slots__ = ( "city", "country_code", "post_code", "state", "street_line1", "street_line2", ) def __init__( self, street_line1: str, street_line2: str, city: str, state: str, country_code: str, post_code: str, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.street_line1: str = street_line1 self.street_line2: str = street_line2 self.city: str = city self.state: str = state self.country_code: str = country_code self.post_code: str = post_code self._freeze() class IdDocumentData(TelegramObject): """ This object represents the data of an identity document. Args: document_no (:obj:`str`): Document number. expiry_date (:obj:`str`): Optional. Date of expiry, in DD.MM.YYYY format. Attributes: document_no (:obj:`str`): Document number. expiry_date (:obj:`str`): Optional. Date of expiry, in DD.MM.YYYY format. """ __slots__ = ("document_no", "expiry_date") def __init__( self, document_no: str, expiry_date: str, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.document_no: str = document_no self.expiry_date: str = expiry_date self._freeze() python-telegram-bot-21.1.1/telegram/_passport/encryptedpassportelement.py000066400000000000000000000306661460724040100270350ustar00rootroot00000000000000#!/usr/bin/env python # flake8: noqa: E501 # 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 EncryptedPassportElement.""" from base64 import b64decode from typing import TYPE_CHECKING, Optional, Sequence, Tuple, Union from telegram._passport.credentials import decrypt_json from telegram._passport.data import IdDocumentData, PersonalDetails, ResidentialAddress from telegram._passport.passportfile import PassportFile from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot, Credentials class EncryptedPassportElement(TelegramObject): """ Contains information about documents or other Telegram Passport elements shared with the bot by the user. The data has been automatically decrypted by python-telegram-bot. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`type`, :attr:`data`, :attr:`phone_number`, :attr:`email`, :attr:`files`, :attr:`front_side`, :attr:`reverse_side` and :attr:`selfie` are equal. Note: This object is decrypted only when originating from :obj:`telegram.PassportData.decrypted_data`. Args: type (:obj:`str`): Element type. One of "personal_details", "passport", "driver_license", "identity_card", "internal_passport", "address", "utility_bill", "bank_statement", "rental_agreement", "passport_registration", "temporary_registration", "phone_number", "email". hash (:obj:`str`): Base64-encoded element hash for using in :class:`telegram.PassportElementErrorUnspecified`. data (:class:`telegram.PersonalDetails` | :class:`telegram.IdDocumentData` | \ :class:`telegram.ResidentialAddress` | :obj:`str`, optional): Decrypted or encrypted data; available only for "personal_details", "passport", "driver_license", "identity_card", "internal_passport" and "address" types. phone_number (:obj:`str`, optional): User's verified phone number; available only for "phone_number" type. email (:obj:`str`, optional): User's verified email address; available only for "email" type. files (Sequence[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted files with documents provided by the user; available only for "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. .. versionchanged:: 20.0 |sequenceclassargs| front_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the front side of the document, provided by the user; Available only for "passport", "driver_license", "identity_card" and "internal_passport". reverse_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the reverse side of the document, provided by the user; Available only for "driver_license" and "identity_card". selfie (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the selfie of the user holding a document, provided by the user; available if requested for "passport", "driver_license", "identity_card" and "internal_passport". translation (Sequence[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted files with translated versions of documents provided by the user; available if requested requested for "passport", "driver_license", "identity_card", "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. .. versionchanged:: 20.0 |sequenceclassargs| Attributes: type (:obj:`str`): Element type. One of "personal_details", "passport", "driver_license", "identity_card", "internal_passport", "address", "utility_bill", "bank_statement", "rental_agreement", "passport_registration", "temporary_registration", "phone_number", "email". hash (:obj:`str`): Base64-encoded element hash for using in :class:`telegram.PassportElementErrorUnspecified`. data (:class:`telegram.PersonalDetails` | :class:`telegram.IdDocumentData` | \ :class:`telegram.ResidentialAddress` | :obj:`str`): Optional. Decrypted or encrypted data; available only for "personal_details", "passport", "driver_license", "identity_card", "internal_passport" and "address" types. phone_number (:obj:`str`): Optional. User's verified phone number; available only for "phone_number" type. email (:obj:`str`): Optional. User's verified email address; available only for "email" type. files (Tuple[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted files with documents provided by the user; available only for "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. .. versionchanged:: 20.0 * |tupleclassattrs| * |alwaystuple| front_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the front side of the document, provided by the user; available only for "passport", "driver_license", "identity_card" and "internal_passport". reverse_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the reverse side of the document, provided by the user; available only for "driver_license" and "identity_card". selfie (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the selfie of the user holding a document, provided by the user; available if requested for "passport", "driver_license", "identity_card" and "internal_passport". translation (Tuple[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted files with translated versions of documents provided by the user; available if requested for "passport", "driver_license", "identity_card", "internal_passport", "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. .. versionchanged:: 20.0 * |tupleclassattrs| * |alwaystuple| """ __slots__ = ( "data", "email", "files", "front_side", "hash", "phone_number", "reverse_side", "selfie", "translation", "type", ) def __init__( self, type: str, # pylint: disable=redefined-builtin hash: str, # pylint: disable=redefined-builtin data: Optional[Union[PersonalDetails, IdDocumentData, ResidentialAddress]] = None, phone_number: Optional[str] = None, email: Optional[str] = None, files: Optional[Sequence[PassportFile]] = None, front_side: Optional[PassportFile] = None, reverse_side: Optional[PassportFile] = None, selfie: Optional[PassportFile] = None, translation: Optional[Sequence[PassportFile]] = None, credentials: Optional["Credentials"] = None, # pylint: disable=unused-argument *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.type: str = type # Optionals self.data: Optional[Union[PersonalDetails, IdDocumentData, ResidentialAddress]] = data self.phone_number: Optional[str] = phone_number self.email: Optional[str] = email self.files: Tuple[PassportFile, ...] = parse_sequence_arg(files) self.front_side: Optional[PassportFile] = front_side self.reverse_side: Optional[PassportFile] = reverse_side self.selfie: Optional[PassportFile] = selfie self.translation: Tuple[PassportFile, ...] = parse_sequence_arg(translation) self.hash: str = hash self._id_attrs = ( self.type, self.data, self.phone_number, self.email, self.files, self.front_side, self.reverse_side, self.selfie, ) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["EncryptedPassportElement"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data["files"] = PassportFile.de_list(data.get("files"), bot) or None data["front_side"] = PassportFile.de_json(data.get("front_side"), bot) data["reverse_side"] = PassportFile.de_json(data.get("reverse_side"), bot) data["selfie"] = PassportFile.de_json(data.get("selfie"), bot) data["translation"] = PassportFile.de_list(data.get("translation"), bot) or None return super().de_json(data=data, bot=bot) @classmethod def de_json_decrypted( cls, data: Optional[JSONDict], bot: "Bot", credentials: "Credentials" ) -> Optional["EncryptedPassportElement"]: """Variant of :meth:`telegram.TelegramObject.de_json` that also takes into account passport credentials. Args: data (Dict[:obj:`str`, ...]): The JSON data. bot (:class:`telegram.Bot`): The bot associated with this object. credentials (:class:`telegram.FileCredentials`): The credentials Returns: :class:`telegram.EncryptedPassportElement`: """ if not data: return None if data["type"] not in ("phone_number", "email"): secure_data = getattr(credentials.secure_data, data["type"]) if secure_data.data is not None: # If not already decrypted if not isinstance(data["data"], dict): data["data"] = decrypt_json( b64decode(secure_data.data.secret), b64decode(secure_data.data.hash), b64decode(data["data"]), ) if data["type"] == "personal_details": data["data"] = PersonalDetails.de_json(data["data"], bot=bot) elif data["type"] in ( "passport", "internal_passport", "driver_license", "identity_card", ): data["data"] = IdDocumentData.de_json(data["data"], bot=bot) elif data["type"] == "address": data["data"] = ResidentialAddress.de_json(data["data"], bot=bot) data["files"] = ( PassportFile.de_list_decrypted(data.get("files"), bot, secure_data.files) or None ) data["front_side"] = PassportFile.de_json_decrypted( data.get("front_side"), bot, secure_data.front_side ) data["reverse_side"] = PassportFile.de_json_decrypted( data.get("reverse_side"), bot, secure_data.reverse_side ) data["selfie"] = PassportFile.de_json_decrypted( data.get("selfie"), bot, secure_data.selfie ) data["translation"] = ( PassportFile.de_list_decrypted( data.get("translation"), bot, secure_data.translation ) or None ) return super().de_json(data=data, bot=bot) python-telegram-bot-21.1.1/telegram/_passport/passportdata.py000066400000000000000000000120631460724040100243660ustar00rootroot00000000000000#!/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/]. """Contains information about Telegram Passport data shared with the bot by the user.""" from typing import TYPE_CHECKING, Optional, Sequence, Tuple from telegram._passport.credentials import EncryptedCredentials from telegram._passport.encryptedpassportelement import EncryptedPassportElement from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot, Credentials class PassportData(TelegramObject): """Contains information about Telegram Passport data shared with the bot by the user. Note: To be able to decrypt this object, you must pass your ``private_key`` to either :class:`telegram.ext.Updater` or :class:`telegram.Bot`. Decrypted data is then found in :attr:`decrypted_data` and the payload can be found in :attr:`decrypted_credentials`'s attribute :attr:`telegram.Credentials.nonce`. Args: data (Sequence[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information about documents and other Telegram Passport elements that was shared with the bot. .. versionchanged:: 20.0 |sequenceclassargs| credentials (:class:`telegram.EncryptedCredentials`)): Encrypted credentials. Attributes: data (Tuple[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information about documents and other Telegram Passport elements that was shared with the bot. .. versionchanged:: 20.0 |tupleclassattrs| credentials (:class:`telegram.EncryptedCredentials`): Encrypted credentials. """ __slots__ = ("_decrypted_data", "credentials", "data") def __init__( self, data: Sequence[EncryptedPassportElement], credentials: EncryptedCredentials, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.data: Tuple[EncryptedPassportElement, ...] = parse_sequence_arg(data) self.credentials: EncryptedCredentials = credentials self._decrypted_data: Optional[Tuple[EncryptedPassportElement]] = None self._id_attrs = tuple([x.type for x in data] + [credentials.hash]) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PassportData"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data["data"] = EncryptedPassportElement.de_list(data.get("data"), bot) data["credentials"] = EncryptedCredentials.de_json(data.get("credentials"), bot) return super().de_json(data=data, bot=bot) @property def decrypted_data(self) -> Tuple[EncryptedPassportElement, ...]: """ Tuple[:class:`telegram.EncryptedPassportElement`]: Lazily decrypt and return information about documents and other Telegram Passport elements which were shared with the bot. .. versionchanged:: 20.0 Returns a tuple instead of a list. Raises: telegram.error.PassportDecryptionError: Decryption failed. Usually due to bad private/public key but can also suggest malformed/tampered data. """ if self._decrypted_data is None: self._decrypted_data = tuple( # type: ignore[assignment] EncryptedPassportElement.de_json_decrypted( element.to_dict(), self.get_bot(), self.decrypted_credentials ) for element in self.data ) return self._decrypted_data # type: ignore[return-value] @property def decrypted_credentials(self) -> "Credentials": """ :class:`telegram.Credentials`: Lazily decrypt and return credentials that were used to decrypt the data. This object also contains the user specified payload as `decrypted_data.payload`. Raises: telegram.error.PassportDecryptionError: Decryption failed. Usually due to bad private/public key but can also suggest malformed/tampered data. """ return self.credentials.decrypted_data python-telegram-bot-21.1.1/telegram/_passport/passportelementerrors.py000066400000000000000000000445571460724040100263600ustar00rootroot00000000000000#!/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 PassportElementError.""" from typing import List, Optional from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict from telegram._utils.warnings import warn from telegram.warnings import PTBDeprecationWarning class PassportElementError(TelegramObject): """Baseclass for the PassportElementError* classes. This object represents an error in the Telegram Passport element which was submitted that should be resolved by the user. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`source` and :attr:`type` are equal. Args: source (:obj:`str`): Error source. type (:obj:`str`): The section of the user's Telegram Passport which has the error. message (:obj:`str`): Error message. Attributes: source (:obj:`str`): Error source. type (:obj:`str`): The section of the user's Telegram Passport which has the error. message (:obj:`str`): Error message. """ __slots__ = ("message", "source", "type") def __init__( self, source: str, type: str, message: str, *, api_kwargs: Optional[JSONDict] = None ): super().__init__(api_kwargs=api_kwargs) # Required self.source: str = str(source) self.type: str = str(type) self.message: str = str(message) self._id_attrs = (self.source, self.type) self._freeze() class PassportElementErrorDataField(PassportElementError): """ Represents an issue in one of the data fields that was provided by the user. The error is considered resolved when the field's value changes. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`field_name`, :attr:`data_hash` and :attr:`message` are equal. Args: type (:obj:`str`): The section of the user's Telegram Passport which has the error, one of ``"personal_details"``, ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``, ``"address"``. field_name (:obj:`str`): Name of the data field which has the error. data_hash (:obj:`str`): Base64-encoded data hash. message (:obj:`str`): Error message. Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the error, one of ``"personal_details"``, ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``, ``"address"``. field_name (:obj:`str`): Name of the data field which has the error. data_hash (:obj:`str`): Base64-encoded data hash. message (:obj:`str`): Error message. """ __slots__ = ("data_hash", "field_name") def __init__( self, type: str, field_name: str, data_hash: str, message: str, *, api_kwargs: Optional[JSONDict] = None, ): # Required super().__init__("data", type, message, api_kwargs=api_kwargs) with self._unfrozen(): self.field_name: str = field_name self.data_hash: str = data_hash self._id_attrs = ( self.source, self.type, self.field_name, self.data_hash, self.message, ) class PassportElementErrorFile(PassportElementError): """ Represents an issue with a document scan. The error is considered resolved when the file with the document scan changes. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hash`, and :attr:`message` are equal. Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hash (:obj:`str`): Base64-encoded file hash. message (:obj:`str`): Error message. Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hash (:obj:`str`): Base64-encoded file hash. message (:obj:`str`): Error message. """ __slots__ = ("file_hash",) def __init__( self, type: str, file_hash: str, message: str, *, api_kwargs: Optional[JSONDict] = None ): # Required super().__init__("file", type, message, api_kwargs=api_kwargs) with self._unfrozen(): self.file_hash: str = file_hash self._id_attrs = (self.source, self.type, self.file_hash, self.message) class PassportElementErrorFiles(PassportElementError): """ Represents an issue with a list of scans. The error is considered resolved when the list of files with the document scans changes. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hashes`, and :attr:`message` are equal. Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hashes (List[:obj:`str`]): List of base64-encoded file hashes. message (:obj:`str`): Error message. Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. message (:obj:`str`): Error message. """ __slots__ = ("_file_hashes",) def __init__( self, type: str, file_hashes: List[str], message: str, *, api_kwargs: Optional[JSONDict] = None, ): # Required super().__init__("files", type, message, api_kwargs=api_kwargs) with self._unfrozen(): self._file_hashes: List[str] = file_hashes self._id_attrs = (self.source, self.type, self.message, *tuple(file_hashes)) def to_dict(self, recursive: bool = True) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict` for details.""" data = super().to_dict(recursive) data["file_hashes"] = self._file_hashes return data @property def file_hashes(self) -> List[str]: """List of base64-encoded file hashes. .. deprecated:: 20.6 This attribute will return a tuple instead of a list in future major versions. """ warn( "The attribute `file_hashes` will return a tuple instead of a list in future major" " versions.", PTBDeprecationWarning, stacklevel=2, ) return self._file_hashes class PassportElementErrorFrontSide(PassportElementError): """ Represents an issue with the front side of a document. The error is considered resolved when the file with the front side of the document changes. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hash`, and :attr:`message` are equal. Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the front side of the document. message (:obj:`str`): Error message. Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the front side of the document. message (:obj:`str`): Error message. """ __slots__ = ("file_hash",) def __init__( self, type: str, file_hash: str, message: str, *, api_kwargs: Optional[JSONDict] = None ): # Required super().__init__("front_side", type, message, api_kwargs=api_kwargs) with self._unfrozen(): self.file_hash: str = file_hash self._id_attrs = (self.source, self.type, self.file_hash, self.message) class PassportElementErrorReverseSide(PassportElementError): """ Represents an issue with the reverse side of a document. The error is considered resolved when the file with the reverse side of the document changes. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hash`, and :attr:`message` are equal. Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"driver_license"``, ``"identity_card"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the reverse side of the document. message (:obj:`str`): Error message. Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"driver_license"``, ``"identity_card"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the reverse side of the document. message (:obj:`str`): Error message. """ __slots__ = ("file_hash",) def __init__( self, type: str, file_hash: str, message: str, *, api_kwargs: Optional[JSONDict] = None ): # Required super().__init__("reverse_side", type, message, api_kwargs=api_kwargs) with self._unfrozen(): self.file_hash: str = file_hash self._id_attrs = (self.source, self.type, self.file_hash, self.message) class PassportElementErrorSelfie(PassportElementError): """ Represents an issue with the selfie with a document. The error is considered resolved when the file with the selfie changes. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hash`, and :attr:`message` are equal. Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the selfie. message (:obj:`str`): Error message. Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``. file_hash (:obj:`str`): Base64-encoded hash of the file with the selfie. message (:obj:`str`): Error message. """ __slots__ = ("file_hash",) def __init__( self, type: str, file_hash: str, message: str, *, api_kwargs: Optional[JSONDict] = None ): # Required super().__init__("selfie", type, message, api_kwargs=api_kwargs) with self._unfrozen(): self.file_hash: str = file_hash self._id_attrs = (self.source, self.type, self.file_hash, self.message) class PassportElementErrorTranslationFile(PassportElementError): """ Represents an issue with one of the files that constitute the translation of a document. The error is considered resolved when the file changes. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hash`, and :attr:`message` are equal. Args: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hash (:obj:`str`): Base64-encoded hash of the file. message (:obj:`str`): Error message. Attributes: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hash (:obj:`str`): Base64-encoded hash of the file. message (:obj:`str`): Error message. """ __slots__ = ("file_hash",) def __init__( self, type: str, file_hash: str, message: str, *, api_kwargs: Optional[JSONDict] = None ): # Required super().__init__("translation_file", type, message, api_kwargs=api_kwargs) with self._unfrozen(): self.file_hash: str = file_hash self._id_attrs = (self.source, self.type, self.file_hash, self.message) class PassportElementErrorTranslationFiles(PassportElementError): """ Represents an issue with the translated version of a document. The error is considered resolved when a file with the document translation changes. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hashes`, and :attr:`message` are equal. Args: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hashes (List[:obj:`str`]): List of base64-encoded file hashes. message (:obj:`str`): Error message. Attributes: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. message (:obj:`str`): Error message. """ __slots__ = ("_file_hashes",) def __init__( self, type: str, file_hashes: List[str], message: str, *, api_kwargs: Optional[JSONDict] = None, ): # Required super().__init__("translation_files", type, message, api_kwargs=api_kwargs) with self._unfrozen(): self._file_hashes: List[str] = file_hashes self._id_attrs = (self.source, self.type, self.message, *tuple(file_hashes)) def to_dict(self, recursive: bool = True) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict` for details.""" data = super().to_dict(recursive) data["file_hashes"] = self._file_hashes return data @property def file_hashes(self) -> List[str]: """List of base64-encoded file hashes. .. deprecated:: 20.6 This attribute will return a tuple instead of a list in future major versions. """ warn( "The attribute `file_hashes` will return a tuple instead of a list in future major" " versions. See the stability policy:" " https://docs.python-telegram-bot.org/en/stable/stability_policy.html", PTBDeprecationWarning, stacklevel=2, ) return self._file_hashes class PassportElementErrorUnspecified(PassportElementError): """ Represents an issue in an unspecified place. The error is considered resolved when new data is added. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`element_hash`, and :attr:`message` are equal. Args: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue. element_hash (:obj:`str`): Base64-encoded element hash. message (:obj:`str`): Error message. Attributes: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue. element_hash (:obj:`str`): Base64-encoded element hash. message (:obj:`str`): Error message. """ __slots__ = ("element_hash",) def __init__( self, type: str, element_hash: str, message: str, *, api_kwargs: Optional[JSONDict] = None ): # Required super().__init__("unspecified", type, message, api_kwargs=api_kwargs) with self._unfrozen(): self.element_hash: str = element_hash self._id_attrs = (self.source, self.type, self.element_hash, self.message) python-telegram-bot-21.1.1/telegram/_passport/passportfile.py000066400000000000000000000157651460724040100244100ustar00rootroot00000000000000#!/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 Encrypted PassportFile.""" from typing import TYPE_CHECKING, List, Optional, Tuple from telegram._telegramobject import TelegramObject from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput from telegram._utils.warnings import warn from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import Bot, File, FileCredentials class PassportFile(TelegramObject): """ This object represents a file uploaded to Telegram Passport. Currently all Telegram Passport files are in JPEG format when decrypted and don't exceed 10MB. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`file_unique_id` is equal. Args: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`): File size in bytes. file_date (:obj:`int`): Unix time when the file was uploaded. .. deprecated:: 20.6 This argument will only accept a datetime instead of an integer in future major versions. Attributes: file_id (:obj:`str`): Identifier for this file, which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. file_size (:obj:`int`): File size in bytes. """ __slots__ = ( "_credentials", "_file_date", "file_id", "file_size", "file_unique_id", ) def __init__( self, file_id: str, file_unique_id: str, file_date: int, file_size: int, credentials: Optional["FileCredentials"] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) # Required self.file_id: str = file_id self.file_unique_id: str = file_unique_id self.file_size: int = file_size self._file_date: int = file_date # Optionals self._credentials: Optional[FileCredentials] = credentials self._id_attrs = (self.file_unique_id,) self._freeze() def to_dict(self, recursive: bool = True) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict` for details.""" data = super().to_dict(recursive) data["file_date"] = self._file_date return data @property def file_date(self) -> int: """:obj:`int`: Unix time when the file was uploaded. .. deprecated:: 20.6 This attribute will return a datetime instead of a integer in future major versions. """ warn( "The attribute `file_date` will return a datetime instead of an integer in future" " major versions.", PTBDeprecationWarning, stacklevel=2, ) return self._file_date @classmethod def de_json_decrypted( cls, data: Optional[JSONDict], bot: "Bot", credentials: "FileCredentials" ) -> Optional["PassportFile"]: """Variant of :meth:`telegram.TelegramObject.de_json` that also takes into account passport credentials. Args: data (Dict[:obj:`str`, ...]): The JSON data. bot (:class:`telegram.Bot`): The bot associated with this object. credentials (:class:`telegram.FileCredentials`): The credentials Returns: :class:`telegram.PassportFile`: """ data = cls._parse_data(data) if not data: return None data["credentials"] = credentials return super().de_json(data=data, bot=bot) @classmethod def de_list_decrypted( cls, data: Optional[List[JSONDict]], bot: "Bot", credentials: List["FileCredentials"] ) -> Tuple[Optional["PassportFile"], ...]: """Variant of :meth:`telegram.TelegramObject.de_list` that also takes into account passport credentials. .. 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`): The bot associated with these objects. credentials (:class:`telegram.FileCredentials`): The credentials Returns: Tuple[:class:`telegram.PassportFile`]: """ if not data: return () return tuple( obj for obj in ( cls.de_json_decrypted(passport_file, bot, credentials[i]) for i, passport_file in enumerate(data) ) if obj is not None ) async def get_file( 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, ) -> "File": """ Wrapper over :meth:`telegram.Bot.get_file`. Will automatically assign the correct credentials to the returned :class:`telegram.File` if originating from :obj:`telegram.PassportData.decrypted_data`. For the documentation of the arguments, please see :meth:`telegram.Bot.get_file`. Returns: :class:`telegram.File` Raises: :class:`telegram.error.TelegramError` """ file = await self.get_bot().get_file( file_id=self.file_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) if self._credentials: file.set_credentials(self._credentials) return file python-telegram-bot-21.1.1/telegram/_payment/000077500000000000000000000000001460724040100211075ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_payment/__init__.py000066400000000000000000000000001460724040100232060ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_payment/invoice.py000066400000000000000000000116241460724040100231210ustar00rootroot00000000000000#!/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 Invoice.""" from typing import Final, Optional from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict class Invoice(TelegramObject): """This object contains basic information about an invoice. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`title`, :attr:`description`, :paramref:`start_parameter`, :attr:`currency` and :attr:`total_amount` are equal. Args: title (:obj:`str`): Product name. description (:obj:`str`): Product description. start_parameter (:obj:`str`): Unique bot deep-linking parameter that can be used to generate this invoice. currency (:obj:`str`): Three-letter ISO 4217 currency code. total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass ``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). Attributes: title (:obj:`str`): Product name. description (:obj:`str`): Product description. start_parameter (:obj:`str`): Unique bot deep-linking parameter that can be used to generate this invoice. currency (:obj:`str`): Three-letter ISO 4217 currency code. total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 ``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). """ __slots__ = ( "currency", "description", "start_parameter", "title", "total_amount", ) def __init__( self, title: str, description: str, start_parameter: str, currency: str, total_amount: int, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.title: str = title self.description: str = description self.start_parameter: str = start_parameter self.currency: str = currency self.total_amount: int = total_amount self._id_attrs = ( self.title, self.description, self.start_parameter, self.currency, self.total_amount, ) self._freeze() MIN_TITLE_LENGTH: Final[int] = constants.InvoiceLimit.MIN_TITLE_LENGTH """:const:`telegram.constants.InvoiceLimit.MIN_TITLE_LENGTH` .. versionadded:: 20.0 """ MAX_TITLE_LENGTH: Final[int] = constants.InvoiceLimit.MAX_TITLE_LENGTH """:const:`telegram.constants.InvoiceLimit.MAX_TITLE_LENGTH` .. versionadded:: 20.0 """ MIN_DESCRIPTION_LENGTH: Final[int] = constants.InvoiceLimit.MIN_DESCRIPTION_LENGTH """:const:`telegram.constants.InvoiceLimit.MIN_DESCRIPTION_LENGTH` .. versionadded:: 20.0 """ MAX_DESCRIPTION_LENGTH: Final[int] = constants.InvoiceLimit.MAX_DESCRIPTION_LENGTH """:const:`telegram.constants.InvoiceLimit.MAX_DESCRIPTION_LENGTH` .. versionadded:: 20.0 """ MIN_PAYLOAD_LENGTH: Final[int] = constants.InvoiceLimit.MIN_PAYLOAD_LENGTH """:const:`telegram.constants.InvoiceLimit.MIN_PAYLOAD_LENGTH` .. versionadded:: 20.0 """ MAX_PAYLOAD_LENGTH: Final[int] = constants.InvoiceLimit.MAX_PAYLOAD_LENGTH """:const:`telegram.constants.InvoiceLimit.MAX_PAYLOAD_LENGTH` .. versionadded:: 20.0 """ MAX_TIP_AMOUNTS: Final[int] = constants.InvoiceLimit.MAX_TIP_AMOUNTS """:const:`telegram.constants.InvoiceLimit.MAX_TIP_AMOUNTS` .. versionadded:: 20.0 """ python-telegram-bot-21.1.1/telegram/_payment/labeledprice.py000066400000000000000000000052171460724040100241010ustar00rootroot00000000000000#!/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 LabeledPrice.""" from typing import Optional from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict class LabeledPrice(TelegramObject): """This object represents a portion of the price for goods or services. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`label` and :attr:`amount` are equal. Examples: :any:`Payment Bot ` Args: label (:obj:`str`): Portion label. amount (:obj:`int`): Price of the product in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass ``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). Attributes: label (:obj:`str`): Portion label. amount (:obj:`int`): Price of the product in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 ``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). """ __slots__ = ("amount", "label") def __init__(self, label: str, amount: int, *, api_kwargs: Optional[JSONDict] = None): super().__init__(api_kwargs=api_kwargs) self.label: str = label self.amount: int = amount self._id_attrs = (self.label, self.amount) self._freeze() python-telegram-bot-21.1.1/telegram/_payment/orderinfo.py000066400000000000000000000060561460724040100234570ustar00rootroot00000000000000#!/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 OrderInfo.""" from typing import TYPE_CHECKING, Optional from telegram._payment.shippingaddress import ShippingAddress from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot class OrderInfo(TelegramObject): """This object represents information about an order. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`name`, :attr:`phone_number`, :attr:`email` and :attr:`shipping_address` are equal. Args: name (:obj:`str`, optional): User name. phone_number (:obj:`str`, optional): User's phone number. email (:obj:`str`, optional): User email. shipping_address (:class:`telegram.ShippingAddress`, optional): User shipping address. Attributes: name (:obj:`str`): Optional. User name. phone_number (:obj:`str`): Optional. User's phone number. email (:obj:`str`): Optional. User email. shipping_address (:class:`telegram.ShippingAddress`): Optional. User shipping address. """ __slots__ = ("email", "name", "phone_number", "shipping_address") def __init__( self, name: Optional[str] = None, phone_number: Optional[str] = None, email: Optional[str] = None, shipping_address: Optional[ShippingAddress] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.name: Optional[str] = name self.phone_number: Optional[str] = phone_number self.email: Optional[str] = email self.shipping_address: Optional[ShippingAddress] = shipping_address self._id_attrs = (self.name, self.phone_number, self.email, self.shipping_address) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["OrderInfo"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return cls() data["shipping_address"] = ShippingAddress.de_json(data.get("shipping_address"), bot) return super().de_json(data=data, bot=bot) python-telegram-bot-21.1.1/telegram/_payment/precheckoutquery.py000066400000000000000000000134201460724040100250630ustar00rootroot00000000000000#!/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 PreCheckoutQuery.""" from typing import TYPE_CHECKING, Optional from telegram._payment.orderinfo import OrderInfo 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 class PreCheckoutQuery(TelegramObject): """This object contains information about an incoming pre-checkout query. 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. Args: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. currency (:obj:`str`): Three-letter ISO 4217 currency code. total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass ``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). invoice_payload (:obj:`str`): Bot specified invoice payload. shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user. Attributes: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. currency (:obj:`str`): Three-letter ISO 4217 currency code. total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 ``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). invoice_payload (:obj:`str`): Bot specified invoice payload. shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user. """ __slots__ = ( "currency", "from_user", "id", "invoice_payload", "order_info", "shipping_option_id", "total_amount", ) def __init__( self, id: str, # pylint: disable=redefined-builtin from_user: User, currency: str, total_amount: int, invoice_payload: str, shipping_option_id: Optional[str] = None, order_info: Optional[OrderInfo] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.id: str = id self.from_user: User = from_user self.currency: str = currency self.total_amount: int = total_amount self.invoice_payload: str = invoice_payload self.shipping_option_id: Optional[str] = shipping_option_id self.order_info: Optional[OrderInfo] = order_info self._id_attrs = (self.id,) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PreCheckoutQuery"]: """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["order_info"] = OrderInfo.de_json(data.get("order_info"), bot) return super().de_json(data=data, bot=bot) async def answer( self, 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: """Shortcut for:: await bot.answer_pre_checkout_query(update.pre_checkout_query.id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.answer_pre_checkout_query`. """ return await self.get_bot().answer_pre_checkout_query( pre_checkout_query_id=self.id, ok=ok, error_message=error_message, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) python-telegram-bot-21.1.1/telegram/_payment/shippingaddress.py000066400000000000000000000056411460724040100246560ustar00rootroot00000000000000#!/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 ShippingAddress.""" from typing import Optional from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict class ShippingAddress(TelegramObject): """This object represents a Telegram ShippingAddress. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`country_code`, :attr:`state`, :attr:`city`, :attr:`street_line1`, :attr:`street_line2` and :attr:`post_code` are equal. Args: country_code (:obj:`str`): ISO 3166-1 alpha-2 country code. state (:obj:`str`): State, if applicable. city (:obj:`str`): City. street_line1 (:obj:`str`): First line for the address. street_line2 (:obj:`str`): Second line for the address. post_code (:obj:`str`): Address post code. Attributes: country_code (:obj:`str`): ISO 3166-1 alpha-2 country code. state (:obj:`str`): State, if applicable. city (:obj:`str`): City. street_line1 (:obj:`str`): First line for the address. street_line2 (:obj:`str`): Second line for the address. post_code (:obj:`str`): Address post code. """ __slots__ = ( "city", "country_code", "post_code", "state", "street_line1", "street_line2", ) def __init__( self, country_code: str, state: str, city: str, street_line1: str, street_line2: str, post_code: str, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.country_code: str = country_code self.state: str = state self.city: str = city self.street_line1: str = street_line1 self.street_line2: str = street_line2 self.post_code: str = post_code self._id_attrs = ( self.country_code, self.state, self.city, self.street_line1, self.street_line2, self.post_code, ) self._freeze() python-telegram-bot-21.1.1/telegram/_payment/shippingoption.py000066400000000000000000000047061460724040100245420ustar00rootroot00000000000000#!/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 ShippingOption.""" from typing import TYPE_CHECKING, Optional, Sequence, Tuple from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.types import JSONDict if TYPE_CHECKING: from telegram import LabeledPrice class ShippingOption(TelegramObject): """This object represents one shipping option. 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:`Payment Bot ` Args: id (:obj:`str`): Shipping option identifier. title (:obj:`str`): Option title. prices (Sequence[:class:`telegram.LabeledPrice`]): List of price portions. .. versionchanged:: 20.0 |sequenceclassargs| Attributes: id (:obj:`str`): Shipping option identifier. title (:obj:`str`): Option title. prices (Tuple[:class:`telegram.LabeledPrice`]): List of price portions. .. versionchanged:: 20.0 |tupleclassattrs| """ __slots__ = ("id", "prices", "title") def __init__( self, id: str, # pylint: disable=redefined-builtin title: str, prices: Sequence["LabeledPrice"], *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.id: str = id self.title: str = title self.prices: Tuple[LabeledPrice, ...] = parse_sequence_arg(prices) self._id_attrs = (self.id,) self._freeze() python-telegram-bot-21.1.1/telegram/_payment/shippingquery.py000066400000000000000000000105731460724040100243760ustar00rootroot00000000000000#!/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 ShippingQuery.""" from typing import TYPE_CHECKING, Optional, Sequence from telegram._payment.shippingaddress import ShippingAddress 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 from telegram._payment.shippingoption import ShippingOption class ShippingQuery(TelegramObject): """This object contains information about an incoming shipping query. 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. Args: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. invoice_payload (:obj:`str`): Bot specified invoice payload. shipping_address (:class:`telegram.ShippingAddress`): User specified shipping address. Attributes: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. invoice_payload (:obj:`str`): Bot specified invoice payload. shipping_address (:class:`telegram.ShippingAddress`): User specified shipping address. """ __slots__ = ("from_user", "id", "invoice_payload", "shipping_address") def __init__( self, id: str, # pylint: disable=redefined-builtin from_user: User, invoice_payload: str, shipping_address: ShippingAddress, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.id: str = id self.from_user: User = from_user self.invoice_payload: str = invoice_payload self.shipping_address: ShippingAddress = shipping_address self._id_attrs = (self.id,) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ShippingQuery"]: """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["shipping_address"] = ShippingAddress.de_json(data.get("shipping_address"), bot) return super().de_json(data=data, bot=bot) async def answer( self, 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: """Shortcut for:: await bot.answer_shipping_query(update.shipping_query.id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.answer_shipping_query`. """ return await self.get_bot().answer_shipping_query( shipping_query_id=self.id, ok=ok, shipping_options=shipping_options, error_message=error_message, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) python-telegram-bot-21.1.1/telegram/_payment/successfulpayment.py000066400000000000000000000115131460724040100252370ustar00rootroot00000000000000#!/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 SuccessfulPayment.""" from typing import TYPE_CHECKING, Optional from telegram._payment.orderinfo import OrderInfo from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot class SuccessfulPayment(TelegramObject): """This object contains basic information about a successful payment. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`telegram_payment_charge_id` and :attr:`provider_payment_charge_id` are equal. Args: currency (:obj:`str`): Three-letter ISO 4217 currency code. total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass ``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). invoice_payload (:obj:`str`): Bot specified invoice payload. shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user. telegram_payment_charge_id (:obj:`str`): Telegram payment identifier. provider_payment_charge_id (:obj:`str`): Provider payment identifier. Attributes: currency (:obj:`str`): Three-letter ISO 4217 currency code. total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 ``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). invoice_payload (:obj:`str`): Bot specified invoice payload. shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user. telegram_payment_charge_id (:obj:`str`): Telegram payment identifier. provider_payment_charge_id (:obj:`str`): Provider payment identifier. """ __slots__ = ( "currency", "invoice_payload", "order_info", "provider_payment_charge_id", "shipping_option_id", "telegram_payment_charge_id", "total_amount", ) def __init__( self, currency: str, total_amount: int, invoice_payload: str, telegram_payment_charge_id: str, provider_payment_charge_id: str, shipping_option_id: Optional[str] = None, order_info: Optional[OrderInfo] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.currency: str = currency self.total_amount: int = total_amount self.invoice_payload: str = invoice_payload self.shipping_option_id: Optional[str] = shipping_option_id self.order_info: Optional[OrderInfo] = order_info self.telegram_payment_charge_id: str = telegram_payment_charge_id self.provider_payment_charge_id: str = provider_payment_charge_id self._id_attrs = (self.telegram_payment_charge_id, self.provider_payment_charge_id) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SuccessfulPayment"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data["order_info"] = OrderInfo.de_json(data.get("order_info"), bot) return super().de_json(data=data, bot=bot) python-telegram-bot-21.1.1/telegram/_poll.py000066400000000000000000000427021460724040100207570ustar00rootroot00000000000000#!/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.types import JSONDict if TYPE_CHECKING: from telegram import 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. 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. """ __slots__ = ("text", "voter_count") def __init__(self, text: str, voter_count: int, *, api_kwargs: Optional[JSONDict] = None): super().__init__(api_kwargs=api_kwargs) self.text: str = text self.voter_count: int = voter_count self._id_attrs = (self.text, self.voter_count) self._freeze() 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: "Bot") -> 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| 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| """ __slots__ = ( "allows_multiple_answers", "close_date", "correct_option_id", "explanation", "explanation_entities", "id", "is_anonymous", "is_closed", "open_period", "options", "question", "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, *, 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._id_attrs = (self.id,) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> 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) return super().de_json(data=data, bot=bot) def parse_explanation_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 poll has no explanation. """ if not self.explanation: raise RuntimeError("This Poll has no 'explanation'.") entity_text = self.explanation.encode("utf-16-le") entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2] return entity_text.decode("utf-16-le") 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. """ if types is None: types = MessageEntity.ALL_TYPES return { entity: self.parse_explanation_entity(entity) for entity in self.explanation_entities if entity.type in 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 """ python-telegram-bot-21.1.1/telegram/_proximityalerttriggered.py000066400000000000000000000054601460724040100250020ustar00rootroot00000000000000#!/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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_reaction.py000066400000000000000000000146171460724040100216210ustar00rootroot00000000000000#!/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, Final, Literal, Optional, 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` and :class:`telegram.ReactionTypeCustomEmoji`. .. versionadded:: 20.8 Args: type (:obj:`str`): Type of the reaction. Can be :attr:`~telegram.ReactionType.EMOJI` or :attr:`~telegram.ReactionType.CUSTOM_EMOJI`. Attributes: type (:obj:`str`): Type of the reaction. Can be :attr:`~telegram.ReactionType.EMOJI` or :attr:`~telegram.ReactionType.CUSTOM_EMOJI`. """ __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`""" def __init__( self, type: Union[ # pylint: disable=redefined-builtin Literal["emoji", "custom_emoji"], 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: "Bot") -> Optional["ReactionType"]: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None if cls is ReactionType and data.get("type") in [cls.EMOJI, cls.CUSTOM_EMOJI]: reaction_type = data.pop("type") if reaction_type == cls.EMOJI: return ReactionTypeEmoji.de_json(data=data, bot=bot) return ReactionTypeCustomEmoji.de_json(data=data, bot=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 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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_reply.py000066400000000000000000000503621460724040100211450ustar00rootroot00000000000000#!/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._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. 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. 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. 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. 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. 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. """ __slots__ = ( "animation", "audio", "chat", "contact", "dice", "document", "game", "giveaway", "giveaway_winners", "has_media_spoiler", "invoice", "link_preview_options", "location", "message_id", "origin", "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, *, 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._id_attrs = (self.origin,) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ExternalReplyInfo"]: """See :obj:`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) 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[:obj:`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[:obj:`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: "Bot") -> Optional["TextQuote"]: """See :obj:`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| 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[:obj:`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| 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[:obj:`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: "Bot") -> Optional["ReplyParameters"]: """See :obj:`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) python-telegram-bot-21.1.1/telegram/_replykeyboardmarkup.py000066400000000000000000000401201460724040100240750ustar00rootroot00000000000000#!/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. 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:: An 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 """ python-telegram-bot-21.1.1/telegram/_replykeyboardremove.py000066400000000000000000000062631460724040100241050ustar00rootroot00000000000000#!/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`). 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() python-telegram-bot-21.1.1/telegram/_sentwebappmessage.py000066400000000000000000000042271460724040100235260ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_shared.py000066400000000000000000000315001460724040100212510ustar00rootroot00000000000000#!/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 from telegram._utils.warnings import warn from telegram._utils.warnings_transition import ( build_deprecation_warning_message, warn_about_deprecated_attr_in_property, ) from telegram.warnings import PTBDeprecationWarning 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 :attr:`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 :attr:`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 .. deprecated:: 21.1 In future versions, this argument will become keyword only. user_ids (Sequence[:obj:`int`], optional): Identifiers of the shared users. These numbers may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting them. But they have at most 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 users and could be unable to use these identifiers, unless the users are already known to the bot by some other means. .. deprecated:: 21.1 Bot API 7.2 introduced by :paramref:`users`, replacing this argument. Hence, this argument is now optional and will be removed in future versions. 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, user_ids: Optional[Sequence[int]] = None, users: Optional[Sequence["SharedUser"]] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(api_kwargs=api_kwargs) self.request_id: int = request_id if users is None: raise TypeError("`users` is a required argument since Bot API 7.2") self.users: Tuple[SharedUser, ...] = parse_sequence_arg(users) if user_ids is not None: warn( build_deprecation_warning_message( deprecated_name="user_ids", new_name="users", object_type="parameter", bot_api_version="7.2", ), PTBDeprecationWarning, stacklevel=2, ) self._id_attrs = (self.request_id, self.users) self._freeze() @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> 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) @property def user_ids(self) -> Tuple[int, ...]: """ Tuple[:obj:`int`]: Identifiers of the shared users. These numbers may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting them. But they have at most 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 users and could be unable to use these identifiers, unless the users are already known to the bot by some other means. .. deprecated:: 21.1 As Bot API 7.2 replaces this attribute with :attr:`users`, this attribute will be removed in future versions. """ warn_about_deprecated_attr_in_property( deprecated_attr_name="user_ids", new_attr_name="users", bot_api_version="7.2", stacklevel=2, ) return tuple(user.user_id for user in self.users) 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: "Bot") -> 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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_story.py000066400000000000000000000047671460724040100212020ustar00rootroot00000000000000#!/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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_switchinlinequerychosenchat.py000066400000000000000000000100501460724040100256260ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_telegramobject.py000066400000000000000000000662061460724040100230050ustar00rootroot00000000000000#!/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: :obj:`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: "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 exc 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: "Bot") -> Optional[Tele_co]: """Converts JSON data to a Telegram object. Args: data (Dict[:obj:`str`, ...]): The JSON data. bot (:class:`telegram.Bot`): The bot associated with this object. Returns: The Telegram object. """ return cls._de_json(data=data, bot=bot) @classmethod def de_list( cls: Type[Tele_co], data: Optional[List[JSONDict]], bot: "Bot" ) -> 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`): The bot associated with these objects. 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 python-telegram-bot-21.1.1/telegram/_update.py000066400000000000000000001032661460724040100212760ustar00rootroot00000000000000#!/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 non-service 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 non-service 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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_user.py000066400000000000000000002346721460724040100210000ustar00rootroot00000000000000#!/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, 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 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 .. |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", "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, *, 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._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, *, read_timeout: ODVInput[float] = DEFAULT_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, api_kwargs=api_kwargs, ) async def unpin_message( self, 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, ) -> 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) async def send_invoice( self, title: str, description: str, payload: str, provider_token: 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) async def send_poll( self, question: str, options: Sequence[str], 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, *, 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, ) 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, *, 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, ) 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, *, 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, ) 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, ) python-telegram-bot-21.1.1/telegram/_userprofilephotos.py000066400000000000000000000055331460724040100236060ustar00rootroot00000000000000#!/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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_utils/000077500000000000000000000000001460724040100205725ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_utils/__init__.py000066400000000000000000000000001460724040100226710ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/_utils/argumentparsing.py000066400000000000000000000044061460724040100243560ustar00rootroot00000000000000#!/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 helper functions related to parsing arguments for classes and methods. Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ from typing import Optional, Sequence, Tuple, TypeVar from telegram._linkpreviewoptions import LinkPreviewOptions from telegram._utils.types import ODVInput T = TypeVar("T") def parse_sequence_arg(arg: Optional[Sequence[T]]) -> Tuple[T, ...]: """Parses an optional sequence into a tuple Args: arg (:obj:`Sequence`): The sequence to parse. Returns: :obj:`Tuple`: The sequence converted to a tuple or an empty tuple. """ return tuple(arg) if arg else () def parse_lpo_and_dwpp( disable_web_page_preview: Optional[bool], link_preview_options: ODVInput[LinkPreviewOptions] ) -> ODVInput[LinkPreviewOptions]: """Wrapper around warn_about_deprecated_arg_return_new_arg. Takes care of converting disable_web_page_preview to LinkPreviewOptions. """ if disable_web_page_preview and link_preview_options: raise ValueError( "Parameters `disable_web_page_preview` and `link_preview_options` are mutually " "exclusive." ) if disable_web_page_preview is not None: link_preview_options = LinkPreviewOptions(is_disabled=disable_web_page_preview) return link_preview_options python-telegram-bot-21.1.1/telegram/_utils/datetime.py000066400000000000000000000213341460724040100227430ustar00rootroot00000000000000#!/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 helper functions related to datetime and timestamp conversations. .. versionchanged:: 20.0 Previously, the contents of this module were available through the (no longer existing) module ``telegram._utils.helpers``. Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ import datetime as dtm import time from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: from telegram import Bot # pytz is only available if it was installed as dependency of APScheduler, so we make a little # workaround here DTM_UTC = dtm.timezone.utc try: import pytz UTC = pytz.utc except ImportError: UTC = DTM_UTC # type: ignore[assignment] def _localize(datetime: dtm.datetime, tzinfo: dtm.tzinfo) -> dtm.datetime: """Localize the datetime, where UTC is handled depending on whether pytz is available or not""" if tzinfo is DTM_UTC: return datetime.replace(tzinfo=DTM_UTC) return tzinfo.localize(datetime) # type: ignore[attr-defined] def to_float_timestamp( time_object: Union[float, dtm.timedelta, dtm.datetime, dtm.time], reference_timestamp: Optional[float] = None, tzinfo: Optional[dtm.tzinfo] = None, ) -> float: """ Converts a given time object to a float POSIX timestamp. Used to convert different time specifications to a common format. The time object can be relative (i.e. indicate a time increment, or a time of day) or absolute. Objects from the :class:`datetime` module that are timezone-naive will be assumed to be in UTC, if ``bot`` is not passed or ``bot.defaults`` is :obj:`None`. Args: time_object (:obj:`float` | :obj:`datetime.timedelta` | \ :obj:`datetime.datetime` | :obj:`datetime.time`): Time value to convert. The semantics of this parameter will depend on its type: * :obj:`float` will be interpreted as "seconds from :paramref:`reference_t`" * :obj:`datetime.timedelta` will be interpreted as "time increment from :paramref:`reference_timestamp`" * :obj:`datetime.datetime` will be interpreted as an absolute date/time value * :obj:`datetime.time` will be interpreted as a specific time of day reference_timestamp (:obj:`float`, optional): POSIX timestamp that indicates the absolute time from which relative calculations are to be performed (e.g. when :paramref:`time_object` is given as an :obj:`int`, indicating "seconds from :paramref:`reference_time`"). Defaults to now (the time at which this function is called). If :paramref:`time_object` is given as an absolute representation of date & time (i.e. a :obj:`datetime.datetime` object), :paramref:`reference_timestamp` is not relevant and so its value should be :obj:`None`. If this is not the case, a :exc:`ValueError` will be raised. tzinfo (:class:`datetime.tzinfo`, optional): If :paramref:`time_object` is a naive object from the :mod:`datetime` module, it will be interpreted as this timezone. Defaults to ``pytz.utc``, if available, and :attr:`datetime.timezone.utc` otherwise. Note: Only to be used by ``telegram.ext``. Returns: :obj:`float` | :obj:`None`: The return value depends on the type of argument :paramref:`time_object`. If :paramref:`time_object` is given as a time increment (i.e. as a :obj:`int`, :obj:`float` or :obj:`datetime.timedelta`), then the return value will be :paramref:`reference_timestamp` + :paramref:`time_object`. Else if it is given as an absolute date/time value (i.e. a :obj:`datetime.datetime` object), the equivalent value as a POSIX timestamp will be returned. Finally, if it is a time of the day without date (i.e. a :obj:`datetime.time` object), the return value is the nearest future occurrence of that time of day. Raises: TypeError: If :paramref:`time_object` s type is not one of those described above. ValueError: If :paramref:`time_object` is a :obj:`datetime.datetime` and :paramref:`reference_timestamp` is not :obj:`None`. """ if reference_timestamp is None: reference_timestamp = time.time() elif isinstance(time_object, dtm.datetime): raise ValueError("t is an (absolute) datetime while reference_timestamp is not None") if isinstance(time_object, dtm.timedelta): return reference_timestamp + time_object.total_seconds() if isinstance(time_object, (int, float)): return reference_timestamp + time_object if tzinfo is None: tzinfo = UTC if isinstance(time_object, dtm.time): reference_dt = dtm.datetime.fromtimestamp( reference_timestamp, tz=time_object.tzinfo or tzinfo ) reference_date = reference_dt.date() reference_time = reference_dt.timetz() aware_datetime = dtm.datetime.combine(reference_date, time_object) if aware_datetime.tzinfo is None: aware_datetime = _localize(aware_datetime, tzinfo) # if the time of day has passed today, use tomorrow if reference_time > aware_datetime.timetz(): aware_datetime += dtm.timedelta(days=1) return _datetime_to_float_timestamp(aware_datetime) if isinstance(time_object, dtm.datetime): if time_object.tzinfo is None: time_object = _localize(time_object, tzinfo) return _datetime_to_float_timestamp(time_object) raise TypeError(f"Unable to convert {type(time_object).__name__} object to timestamp") def to_timestamp( dt_obj: Union[float, dtm.timedelta, dtm.datetime, dtm.time, None], reference_timestamp: Optional[float] = None, tzinfo: Optional[dtm.tzinfo] = None, ) -> Optional[int]: """ Wrapper over :func:`to_float_timestamp` which returns an integer (the float value truncated down to the nearest integer). See the documentation for :func:`to_float_timestamp` for more details. """ return ( int(to_float_timestamp(dt_obj, reference_timestamp, tzinfo)) if dt_obj is not None else None ) def from_timestamp( unixtime: Optional[int], tzinfo: Optional[dtm.tzinfo] = None, ) -> Optional[dtm.datetime]: """ Converts an (integer) unix timestamp to a timezone aware datetime object. :obj:`None` s are left alone (i.e. ``from_timestamp(None)`` is :obj:`None`). Args: unixtime (:obj:`int`): Integer POSIX timestamp. tzinfo (:obj:`datetime.tzinfo`, optional): The timezone to which the timestamp is to be converted to. Defaults to :obj:`None`, in which case the returned datetime object will be timezone aware and in UTC. Returns: Timezone aware equivalent :obj:`datetime.datetime` value if :paramref:`unixtime` is not :obj:`None`; else :obj:`None`. """ if unixtime is None: return None return dtm.datetime.fromtimestamp(unixtime, tz=UTC if tzinfo is None else tzinfo) def extract_tzinfo_from_defaults(bot: "Bot") -> Union[dtm.tzinfo, None]: """ Extracts the timezone info from the default values of the bot. If the bot has no default values, :obj:`None` is returned. """ # We don't use `ininstance(bot, ExtBot)` here so that this works # in `python-telegram-bot-raw` as well if hasattr(bot, "defaults") and bot.defaults: return bot.defaults.tzinfo return None def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float: """ Converts a datetime object to a float timestamp (with sub-second precision). If the datetime object is timezone-naive, it is assumed to be in UTC. """ if dt_obj.tzinfo is None: dt_obj = dt_obj.replace(tzinfo=dtm.timezone.utc) return dt_obj.timestamp() python-telegram-bot-21.1.1/telegram/_utils/defaultvalue.py000066400000000000000000000103211460724040100236220ustar00rootroot00000000000000#!/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 DefaultValue class. .. versionchanged:: 20.0 Previously, the contents of this module were available through the (no longer existing) module ``telegram._utils.helpers``. Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ from typing import Generic, TypeVar, Union, overload DVType = TypeVar("DVType", bound=object) # pylint: disable=invalid-name OT = TypeVar("OT", bound=object) class DefaultValue(Generic[DVType]): """Wrapper for immutable default arguments that allows to check, if the default value was set explicitly. Usage:: default_one = DefaultValue(1) def f(arg=default_one): if arg is default_one: print('`arg` is the default') arg = arg.value else: print('`arg` was set explicitly') print(f'`arg` = {str(arg)}') This yields:: >>> f() `arg` is the default `arg` = 1 >>> f(1) `arg` was set explicitly `arg` = 1 >>> f(2) `arg` was set explicitly `arg` = 2 Also allows to evaluate truthiness:: default = DefaultValue(value) if default: ... is equivalent to:: default = DefaultValue(value) if value: ... ``repr(DefaultValue(value))`` returns ``repr(value)`` and ``str(DefaultValue(value))`` returns ``f'DefaultValue({value})'``. Args: value (:class:`object`): The value of the default argument Attributes: value (:class:`object`): The value of the default argument """ __slots__ = ("value",) def __init__(self, value: DVType): self.value: DVType = value def __bool__(self) -> bool: return bool(self.value) # This is mostly here for readability during debugging def __str__(self) -> str: return f"DefaultValue({self.value})" # This is here to have the default instances nicely rendered in the docs def __repr__(self) -> str: return repr(self.value) @overload @staticmethod def get_value(obj: "DefaultValue[OT]") -> OT: ... @overload @staticmethod def get_value(obj: OT) -> OT: ... @staticmethod def get_value(obj: Union[OT, "DefaultValue[OT]"]) -> OT: """Shortcut for:: return obj.value if isinstance(obj, DefaultValue) else obj Args: obj (:obj:`object`): The object to process Returns: Same type as input, or the value of the input: The value """ return obj.value if isinstance(obj, DefaultValue) else obj DEFAULT_NONE: DefaultValue[None] = DefaultValue(None) """:class:`DefaultValue`: Default :obj:`None`""" DEFAULT_FALSE: DefaultValue[bool] = DefaultValue(False) """:class:`DefaultValue`: Default :obj:`False`""" DEFAULT_TRUE: DefaultValue[bool] = DefaultValue(True) """:class:`DefaultValue`: Default :obj:`True` .. versionadded:: 20.0 """ DEFAULT_20: DefaultValue[int] = DefaultValue(20) """:class:`DefaultValue`: Default :obj:`20`""" DEFAULT_IP: DefaultValue[str] = DefaultValue("127.0.0.1") """:class:`DefaultValue`: Default :obj:`127.0.0.1` .. versionadded:: 20.8 """ DEFAULT_80: DefaultValue[int] = DefaultValue(80) """:class:`DefaultValue`: Default :obj:`80` .. versionadded:: 20.8 """ python-telegram-bot-21.1.1/telegram/_utils/enum.py000066400000000000000000000051211460724040100221070ustar00rootroot00000000000000#!/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 helper functions related to enums. Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ import enum as _enum import sys from typing import Type, TypeVar, Union _A = TypeVar("_A") _B = TypeVar("_B") _Enum = TypeVar("_Enum", bound=_enum.Enum) def get_member(enum_cls: Type[_Enum], value: _A, default: _B) -> Union[_Enum, _A, _B]: """Tries to call ``enum_cls(value)`` to convert the value into an enumeration member. If that fails, the ``default`` is returned. """ try: return enum_cls(value) except ValueError: return default # Python 3.11 and above has a different output for mixin classes for IntEnum, StrEnum and IntFlag # see https://docs.python.org/3.11/library/enum.html#notes. We want e.g. str(StrEnumTest.FOO) to # return "foo" instead of "StrEnumTest.FOO", which is not the case < py3.11 class StringEnum(str, _enum.Enum): """Helper class for string enums where ``str(member)`` prints the value, but ``repr(member)`` gives ``EnumName.MEMBER_NAME``. """ __slots__ = () def __repr__(self) -> str: return f"<{self.__class__.__name__}.{self.name}>" def __str__(self) -> str: return str.__str__(self) # Apply the __repr__ modification and __str__ fix to IntEnum class IntEnum(_enum.IntEnum): """Helper class for int enums where ``str(member)`` prints the value, but ``repr(member)`` gives ``EnumName.MEMBER_NAME``. """ __slots__ = () def __repr__(self) -> str: return f"<{self.__class__.__name__}.{self.name}>" if sys.version_info < (3, 11): def __str__(self) -> str: return str(self.value) python-telegram-bot-21.1.1/telegram/_utils/files.py000066400000000000000000000131631460724040100222520ustar00rootroot00000000000000#!/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 helper functions related to handling of files. .. versionchanged:: 20.0 Previously, the contents of this module were available through the (no longer existing) module ``telegram._utils.helpers``. Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ from pathlib import Path from typing import IO, TYPE_CHECKING, Any, Optional, Tuple, Type, TypeVar, Union, cast, overload from telegram._utils.types import FileInput, FilePathInput if TYPE_CHECKING: from telegram import InputFile, TelegramObject _T = TypeVar("_T", bound=Union[bytes, "InputFile", str, Path, None]) @overload def load_file(obj: IO[bytes]) -> Tuple[Optional[str], bytes]: ... @overload def load_file(obj: _T) -> Tuple[None, _T]: ... def load_file( obj: Optional[FileInput], ) -> Tuple[Optional[str], Union[bytes, "InputFile", str, Path, None]]: """If the input is a file handle, read the data and name and return it. Otherwise, return the input unchanged. """ if obj is None: return None, None try: contents = obj.read() # type: ignore[union-attr] except AttributeError: return None, cast(Union[bytes, "InputFile", str, Path], obj) if hasattr(obj, "name") and not isinstance(obj.name, int): filename = Path(obj.name).name else: filename = None return filename, contents def is_local_file(obj: Optional[FilePathInput]) -> bool: """ Checks if a given string is a file on local system. Args: obj (:obj:`str`): The string to check. """ if obj is None: return False path = Path(obj) try: return path.is_file() except Exception: return False def parse_file_input( # pylint: disable=too-many-return-statements file_input: Union[FileInput, "TelegramObject"], tg_type: Optional[Type["TelegramObject"]] = None, filename: Optional[str] = None, attach: bool = False, local_mode: bool = False, ) -> Union[str, "InputFile", Any]: """ Parses input for sending files: * For string input, if the input is an absolute path of a local file: * if ``local_mode`` is ``True``, adds the ``file://`` prefix. If the input is a relative path of a local file, computes the absolute path and adds the ``file://`` prefix. * if ``local_mode`` is ``False``, loads the file as binary data and builds an :class:`InputFile` from that Returns the input unchanged, otherwise. * :class:`pathlib.Path` objects are treated the same way as strings. * For IO and bytes input, returns an :class:`telegram.InputFile`. * If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id`` attribute. Args: file_input (:obj:`str` | :obj:`bytes` | :term:`file object` | Telegram media object): The input to parse. tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g. :class:`telegram.Animation`. filename (:obj:`str`, optional): The filename. Only relevant in case an :class:`telegram.InputFile` is returned. attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in the request to Telegram should point to the multipart data via an ``attach://`` URI. Defaults to `False`. Only relevant if an :class:`telegram.InputFile` is returned. local_mode (:obj:`bool`, optional): Pass :obj:`True` if the bot is running an api server in ``--local`` mode. Returns: :obj:`str` | :class:`telegram.InputFile` | :obj:`object`: The parsed input or the untouched :attr:`file_input`, in case it's no valid file input. """ # Importing on file-level yields cyclic Import Errors from telegram import InputFile # pylint: disable=import-outside-toplevel if isinstance(file_input, str) and file_input.startswith("file://"): if not local_mode: raise ValueError("Specified file input is a file URI, but local mode is not enabled.") return file_input if isinstance(file_input, (str, Path)): if is_local_file(file_input): path = Path(file_input) if local_mode: return path.absolute().as_uri() return InputFile(path.open(mode="rb"), filename=filename, attach=attach) return file_input if isinstance(file_input, bytes): return InputFile(file_input, filename=filename, attach=attach) if hasattr(file_input, "read"): return InputFile(cast(IO, file_input), filename=filename, attach=attach) if tg_type and isinstance(file_input, tg_type): return file_input.file_id # type: ignore[attr-defined] return file_input python-telegram-bot-21.1.1/telegram/_utils/logging.py000066400000000000000000000036621460724040100226010ustar00rootroot00000000000000#!/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 helper functions related to logging. Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ import logging from typing import Optional def get_logger(file_name: str, class_name: Optional[str] = None) -> logging.Logger: """Returns a logger with an appropriate name. Use as follows:: logger = get_logger(__name__) If for example `__name__` is `telegram.ext._updater`, the logger will be named `telegram.ext.Updater`. If `class_name` is passed, this will result in `telegram.ext.`. Useful e.g. for CamelCase class names. If the file name points to a utils module, the logger name will simply be `telegram(.ext)`. Returns: :class:`logging.Logger`: The logger. """ parts = file_name.split("_") if parts[1].startswith("utils") and class_name is None: name = parts[0].rstrip(".") else: name = f"{parts[0]}{class_name or parts[1].capitalize()}" return logging.getLogger(name) python-telegram-bot-21.1.1/telegram/_utils/markup.py000066400000000000000000000040641460724040100224470ustar00rootroot00000000000000#!/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 helper function for Telegram's ReplyMarkups .. versionchanged:: 20.0 Previously, the contents of this module were available through the (no longer existing) class ``telegram.ReplyMarkup``. Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ from collections.abc import Sequence def check_keyboard_type(keyboard: object) -> bool: """Checks if the keyboard provided is of the correct type - A sequence of sequences. Implicitly tested in the init-tests of `{Inline, Reply}KeyboardMarkup` """ # string and bytes may actually be used for ReplyKeyboardMarkup in which case each button # would contain a single character. But that use case should be discouraged and we don't # allow it here. if not isinstance(keyboard, Sequence) or isinstance(keyboard, (str, bytes)): return False for row in keyboard: if not isinstance(row, Sequence) or isinstance(row, (str, bytes)): return False for inner in row: if isinstance(inner, Sequence) and not isinstance(inner, str): return False return True python-telegram-bot-21.1.1/telegram/_utils/repr.py000066400000000000000000000034511460724040100221170ustar00rootroot00000000000000#!/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 auxiliary functionality for building strings for __repr__ method. Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ from typing import Any def build_repr_with_selected_attrs(obj: object, **kwargs: Any) -> str: """Create ``__repr__`` string in the style ``Classname[arg1=1, arg2=2]``. The square brackets emphasize the fact that an object cannot be instantiated from this string. Attributes that are to be used in the representation, are passed as kwargs. """ return ( f"{obj.__class__.__name__}" # square brackets emphasize that an object cannot be instantiated with these params f"[{', '.join(_stringify(name, value) for name, value in kwargs.items())}]" ) def _stringify(key: str, val: Any) -> str: return f"{key}={val.__qualname__ if callable(val) else val}" python-telegram-bot-21.1.1/telegram/_utils/strings.py000066400000000000000000000026411460724040100226400ustar00rootroot00000000000000#!/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 helper functions related to string manipulation. Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ def to_camel_case(snake_str: str) -> str: """Converts a snake_case string to camelCase. Args: snake_str (:obj:`str`): The string to convert. Returns: :obj:`str`: The converted string. """ components = snake_str.split("_") return components[0] + "".join(x.title() for x in components[1:]) python-telegram-bot-21.1.1/telegram/_utils/types.py000066400000000000000000000070711460724040100223150ustar00rootroot00000000000000#!/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 custom typing aliases for internal use within the library. Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ from pathlib import Path from typing import ( IO, TYPE_CHECKING, Any, Collection, Dict, Literal, Optional, Tuple, TypeVar, Union, ) if TYPE_CHECKING: from telegram import ( ForceReply, InlineKeyboardMarkup, InputFile, ReplyKeyboardMarkup, ReplyKeyboardRemove, ) from telegram._utils.defaultvalue import DefaultValue FileLike = Union[IO[bytes], "InputFile"] """Either a bytes-stream (e.g. open file handler) or a :class:`telegram.InputFile`.""" FilePathInput = Union[str, Path] """A filepath either as string or as :obj:`pathlib.Path` object.""" FileInput = Union[FilePathInput, FileLike, bytes, str] """Valid input for passing files to Telegram. Either a file id as string, a file like object, a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes`.""" JSONDict = Dict[str, Any] """Dictionary containing response from Telegram or data to send to the API.""" DVValueType = TypeVar("DVValueType") # pylint: disable=invalid-name DVType = Union[DVValueType, "DefaultValue[DVValueType]"] """Generic type for a variable which can be either `type` or `DefaultVaule[type]`.""" ODVInput = Optional[Union["DefaultValue[DVValueType]", DVValueType, "DefaultValue[None]"]] """Generic type for bot method parameters which can have defaults. ``ODVInput[type]`` is the same as ``Optional[Union[DefaultValue[type], type, DefaultValue[None]]``.""" DVInput = Union["DefaultValue[DVValueType]", DVValueType, "DefaultValue[None]"] """Generic type for bot method parameters which can have defaults. ``DVInput[type]`` is the same as ``Union[DefaultValue[type], type, DefaultValue[None]]``.""" RT = TypeVar("RT") SCT = Union[RT, Collection[RT]] # pylint: disable=invalid-name """Single instance or collection of instances.""" ReplyMarkup = Union[ "InlineKeyboardMarkup", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", "ForceReply" ] """Type alias for reply markup objects. .. versionadded:: 20.0 """ FieldTuple = Tuple[str, bytes, str] """Alias for return type of `InputFile.field_tuple`.""" UploadFileDict = Dict[str, FieldTuple] """Dictionary containing file data to be uploaded to the API.""" HTTPVersion = Literal["1.1", "2.0", "2"] """Allowed HTTP versions. .. versionadded:: 20.4""" CorrectOptionID = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] MarkdownVersion = Literal[1, 2] SocketOpt = Union[ Tuple[int, int, int], Tuple[int, int, Union[bytes, bytearray]], Tuple[int, int, None, int], ] python-telegram-bot-21.1.1/telegram/_utils/warnings.py000066400000000000000000000037051460724040100230010ustar00rootroot00000000000000#!/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 helper functions related to warnings issued by the library. .. versionadded:: 20.0 Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ import warnings from typing import Type from telegram.warnings import PTBUserWarning def warn(message: str, category: Type[Warning] = PTBUserWarning, stacklevel: int = 0) -> None: """ Helper function used as a shortcut for warning with default values. .. versionadded:: 20.0 Args: message (:obj:`str`): Specify the warnings message to pass to ``warnings.warn()``. category (:obj:`Type[Warning]`, optional): Specify the Warning class to pass to ``warnings.warn()``. Defaults to :class:`telegram.warnings.PTBUserWarning`. stacklevel (:obj:`int`, optional): Specify the stacklevel to pass to ``warnings.warn()``. Pass the same value as you'd pass directly to ``warnings.warn()``. Defaults to ``0``. """ warnings.warn(message, category=category, stacklevel=stacklevel + 1) python-telegram-bot-21.1.1/telegram/_utils/warnings_transition.py000066400000000000000000000074271460724040100252600ustar00rootroot00000000000000#!/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 functionality used for transition warnings issued by this library. It was created to prevent circular imports that would be caused by creating the warnings inside warnings.py. .. versionadded:: 20.2 """ from typing import Any, Callable, Type from telegram._utils.warnings import warn from telegram.warnings import PTBDeprecationWarning def build_deprecation_warning_message( deprecated_name: str, new_name: str, object_type: str, bot_api_version: str, ) -> str: """Builds a warning message for the transition in API when an object is renamed. Returns a warning message that can be used in `warn` function. """ return ( f"The {object_type} '{deprecated_name}' was renamed to '{new_name}' in Bot API " f"{bot_api_version}. We recommend using '{new_name}' instead of " f"'{deprecated_name}'." ) # Narrower type hints will cause linting errors and/or circular imports. # We'll use `Any` here and put type hints in the calling code. def warn_about_deprecated_arg_return_new_arg( deprecated_arg: Any, new_arg: Any, deprecated_arg_name: str, new_arg_name: str, bot_api_version: str, stacklevel: int = 2, warn_callback: Callable[[str, Type[Warning], int], None] = warn, ) -> Any: """A helper function for the transition in API when argument is renamed. Checks the `deprecated_arg` and `new_arg` objects; warns if non-None `deprecated_arg` object was passed. Returns `new_arg` object (either the one originally passed by the user or the one that user passed as `deprecated_arg`). Raises `ValueError` if both `deprecated_arg` and `new_arg` objects were passed, and they are different. """ if deprecated_arg and new_arg and deprecated_arg != new_arg: base_message = build_deprecation_warning_message( deprecated_name=deprecated_arg_name, new_name=new_arg_name, object_type="parameter", bot_api_version=bot_api_version, ) raise ValueError( f"You passed different entities as '{deprecated_arg_name}' and '{new_arg_name}'. " f"{base_message}" ) if deprecated_arg: warn_callback( f"Bot API {bot_api_version} renamed the argument '{deprecated_arg_name}' to " f"'{new_arg_name}'.", PTBDeprecationWarning, stacklevel + 1, ) return deprecated_arg return new_arg def warn_about_deprecated_attr_in_property( deprecated_attr_name: str, new_attr_name: str, bot_api_version: str, stacklevel: int = 2, ) -> None: """A helper function for the transition in API when attribute is renamed. Call from properties. The properties replace deprecated attributes in classes and issue these deprecation warnings. """ warn( f"Bot API {bot_api_version} renamed the attribute '{deprecated_attr_name}' to " f"'{new_attr_name}'.", PTBDeprecationWarning, stacklevel=stacklevel + 1, ) python-telegram-bot-21.1.1/telegram/_version.py000066400000000000000000000042721460724040100214760ustar00rootroot00000000000000#!/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__ = ("__bot_api_version__", "__bot_api_version_info__", "__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=1, micro=1, releaselevel="final", serial=0 ) __version__: Final[str] = str(__version_info__) # # SETUP.PY MARKER # Lines above this line will be `exec`-cuted in setup.py. Make sure that this only contains # std-lib imports! from telegram import constants # noqa: E402 # pylint: disable=wrong-import-position __bot_api_version__: Final[str] = constants.BOT_API_VERSION __bot_api_version_info__: Final[constants._BotAPIVersion] = constants.BOT_API_VERSION_INFO python-telegram-bot-21.1.1/telegram/_videochat.py000066400000000000000000000136061460724040100217600ustar00rootroot00000000000000#!/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: "Bot" ) -> 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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_webappdata.py000066400000000000000000000046141460724040100221210ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_webappinfo.py000066400000000000000000000041141460724040100221360ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/_webhookinfo.py000066400000000000000000000170501460724040100223210ustar00rootroot00000000000000#!/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: "Bot") -> 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) python-telegram-bot-21.1.1/telegram/_writeaccessallowed.py000066400000000000000000000070011460724040100236660ustar00rootroot00000000000000#!/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() python-telegram-bot-21.1.1/telegram/constants.py000066400000000000000000003202451460724040100216670ustar00rootroot00000000000000# 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 __all__ = [ "BOT_API_VERSION", "BOT_API_VERSION_INFO", "SUPPORTED_WEBHOOK_PORTS", "ZERO_DATE", "AccentColor", "BotCommandLimit", "BotCommandScopeType", "BotDescriptionLimit", "BotNameLimit", "BulkRequestLimit", "CallbackQueryLimit", "ChatAction", "ChatBoostSources", "ChatID", "ChatInviteLinkLimit", "ChatLimit", "ChatMemberStatus", "ChatPhotoSize", "ChatType", "ContactLimit", "CustomEmojiStickerLimit", "DiceEmoji", "DiceLimit", "FileSizeLimit", "FloodLimit", "ForumIconColor", "ForumTopicLimit", "GiveawayLimit", "InlineKeyboardButtonLimit", "InlineKeyboardMarkupLimit", "InlineQueryLimit", "InlineQueryResultLimit", "InlineQueryResultType", "InlineQueryResultsButtonLimit", "InputMediaType", "InvoiceLimit", "KeyboardButtonRequestUsersLimit", "LocationLimit", "MaskPosition", "MediaGroupLimit", "MenuButtonType", "MessageAttachmentType", "MessageEntityType", "MessageLimit", "MessageOriginType", "MessageType", "ParseMode", "PollLimit", "PollType", "PollingLimit", "ProfileAccentColor", "ReactionEmoji", "ReactionType", "ReplyLimit", "StickerFormat", "StickerLimit", "StickerSetLimit", "StickerType", "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=2) #: :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.Chat.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 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 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` """ 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`.""" 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__ = () MENTION = "mention" """:obj:`str`: Message entities representing a mention.""" HASHTAG = "hashtag" """:obj:`str`: Message entities representing a hashtag.""" CASHTAG = "cashtag" """:obj:`str`: Message entities representing a cashtag.""" PHONE_NUMBER = "phone_number" """:obj:`str`: Message entities representing a phone number.""" BOT_COMMAND = "bot_command" """:obj:`str`: Message entities representing a bot command.""" URL = "url" """:obj:`str`: Message entities representing a url.""" EMAIL = "email" """:obj:`str`: Message entities representing a email.""" BOLD = "bold" """:obj:`str`: Message entities representing bold text.""" ITALIC = "italic" """:obj:`str`: Message entities representing italic text.""" CODE = "code" """:obj:`str`: Message entities representing monowidth string.""" PRE = "pre" """:obj:`str`: Message entities representing monowidth block.""" 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.""" STRIKETHROUGH = "strikethrough" """:obj:`str`: Message entities representing strikethrough text.""" SPOILER = "spoiler" """:obj:`str`: Message entities representing spoiler text.""" CUSTOM_EMOJI = "custom_emoji" """:obj:`str`: Message entities representing inline custom emoji stickers. .. versionadded:: 20.0 """ BLOCKQUOTE = "blockquote" """:obj:`str`: Message entities representing a block quotation. .. versionadded:: 20.8 """ 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 """ 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`.""" 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`.""" 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`.""" 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 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.Chat.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 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 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.""" 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""" python-telegram-bot-21.1.1/telegram/error.py000066400000000000000000000166261460724040100210110ustar00rootroot00000000000000#!/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,) python-telegram-bot-21.1.1/telegram/ext/000077500000000000000000000000001460724040100200735ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/ext/__init__.py000066400000000000000000000075501460724040100222130ustar00rootroot00000000000000#!/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/]. """Extensions over the Telegram Bot API to facilitate bot making""" __all__ = ( "AIORateLimiter", "Application", "ApplicationBuilder", "ApplicationHandlerStop", "BaseHandler", "BasePersistence", "BaseRateLimiter", "BaseUpdateProcessor", "BusinessConnectionHandler", "BusinessMessagesDeletedHandler", "CallbackContext", "CallbackDataCache", "CallbackQueryHandler", "ChatBoostHandler", "ChatJoinRequestHandler", "ChatMemberHandler", "ChosenInlineResultHandler", "CommandHandler", "ContextTypes", "ConversationHandler", "Defaults", "DictPersistence", "ExtBot", "InlineQueryHandler", "InvalidCallbackData", "Job", "JobQueue", "MessageHandler", "MessageReactionHandler", "PersistenceInput", "PicklePersistence", "PollAnswerHandler", "PollHandler", "PreCheckoutQueryHandler", "PrefixHandler", "ShippingQueryHandler", "SimpleUpdateProcessor", "StringCommandHandler", "StringRegexHandler", "TypeHandler", "Updater", "filters", ) from . import filters from ._aioratelimiter import AIORateLimiter from ._application import Application, ApplicationHandlerStop from ._applicationbuilder import ApplicationBuilder from ._basepersistence import BasePersistence, PersistenceInput from ._baseratelimiter import BaseRateLimiter from ._baseupdateprocessor import BaseUpdateProcessor, SimpleUpdateProcessor from ._callbackcontext import CallbackContext from ._callbackdatacache import CallbackDataCache, InvalidCallbackData from ._contexttypes import ContextTypes from ._defaults import Defaults from ._dictpersistence import DictPersistence from ._extbot import ExtBot from ._handlers.basehandler import BaseHandler from ._handlers.businessconnectionhandler import BusinessConnectionHandler from ._handlers.businessmessagesdeletedhandler import BusinessMessagesDeletedHandler from ._handlers.callbackqueryhandler import CallbackQueryHandler from ._handlers.chatboosthandler import ChatBoostHandler from ._handlers.chatjoinrequesthandler import ChatJoinRequestHandler from ._handlers.chatmemberhandler import ChatMemberHandler from ._handlers.choseninlineresulthandler import ChosenInlineResultHandler from ._handlers.commandhandler import CommandHandler from ._handlers.conversationhandler import ConversationHandler from ._handlers.inlinequeryhandler import InlineQueryHandler from ._handlers.messagehandler import MessageHandler from ._handlers.messagereactionhandler import MessageReactionHandler from ._handlers.pollanswerhandler import PollAnswerHandler from ._handlers.pollhandler import PollHandler from ._handlers.precheckoutqueryhandler import PreCheckoutQueryHandler from ._handlers.prefixhandler import PrefixHandler from ._handlers.shippingqueryhandler import ShippingQueryHandler from ._handlers.stringcommandhandler import StringCommandHandler from ._handlers.stringregexhandler import StringRegexHandler from ._handlers.typehandler import TypeHandler from ._jobqueue import Job, JobQueue from ._picklepersistence import PicklePersistence from ._updater import Updater python-telegram-bot-21.1.1/telegram/ext/_aioratelimiter.py000066400000000000000000000254371460724040100236310ustar00rootroot00000000000000#!/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 implementation of the BaseRateLimiter class based on the aiolimiter library. """ import asyncio import contextlib import sys from typing import Any, AsyncIterator, Callable, Coroutine, Dict, List, Optional, Union try: from aiolimiter import AsyncLimiter AIO_LIMITER_AVAILABLE = True except ImportError: AIO_LIMITER_AVAILABLE = False from telegram._utils.logging import get_logger from telegram._utils.types import JSONDict from telegram.error import RetryAfter from telegram.ext._baseratelimiter import BaseRateLimiter # Useful for something like: # async with group_limiter if group else null_context(): # so we don't have to differentiate between "I'm using a context manager" and "I'm not" if sys.version_info >= (3, 10): null_context = contextlib.nullcontext # pylint: disable=invalid-name else: @contextlib.asynccontextmanager async def null_context() -> AsyncIterator[None]: yield None _LOGGER = get_logger(__name__, class_name="AIORateLimiter") class AIORateLimiter(BaseRateLimiter[int]): """ Implementation of :class:`~telegram.ext.BaseRateLimiter` using the library `aiolimiter `_. Important: If you want to use this class, you must install PTB with the optional requirement ``rate-limiter``, i.e. .. code-block:: bash pip install "python-telegram-bot[rate-limiter]" The rate limiting is applied by combining two levels of throttling and :meth:`process_request` roughly boils down to:: async with group_limiter(group_id): async with overall_limiter: await callback(*args, **kwargs) Here, ``group_id`` is determined by checking if there is a ``chat_id`` parameter in the :paramref:`~telegram.ext.BaseRateLimiter.process_request.data`. The ``overall_limiter`` is applied only if a ``chat_id`` argument is present at all. Attention: * Some bot methods accept a ``chat_id`` parameter in form of a ``@username`` for supergroups and channels. As we can't know which ``@username`` corresponds to which integer ``chat_id``, these will be treated as different groups, which may lead to exceeding the rate limit. * As channels can't be differentiated from supergroups by the ``@username`` or integer ``chat_id``, this also applies the group related rate limits to channels. * A :exc:`~telegram.error.RetryAfter` exception will halt *all* requests for :attr:`~telegram.error.RetryAfter.retry_after` + 0.1 seconds. This may be stricter than necessary in some cases, e.g. the bot may hit a rate limit in one group but might still be allowed to send messages in another group. Note: This class is to be understood as minimal effort reference implementation. If you would like to handle rate limiting in a more sophisticated, fine-tuned way, we welcome you to implement your own subclass of :class:`~telegram.ext.BaseRateLimiter`. Feel free to check out the source code of this class for inspiration. .. seealso:: :wiki:`Avoiding Flood Limits ` .. versionadded:: 20.0 Args: overall_max_rate (:obj:`float`): The maximum number of requests allowed for the entire bot per :paramref:`overall_time_period`. When set to 0, no rate limiting will be applied. Defaults to ``30``. overall_time_period (:obj:`float`): The time period (in seconds) during which the :paramref:`overall_max_rate` is enforced. When set to 0, no rate limiting will be applied. Defaults to 1. group_max_rate (:obj:`float`): The maximum number of requests allowed for requests related to groups and channels per :paramref:`group_time_period`. When set to 0, no rate limiting will be applied. Defaults to 20. group_time_period (:obj:`float`): The time period (in seconds) during which the :paramref:`group_max_rate` is enforced. When set to 0, no rate limiting will be applied. Defaults to 60. max_retries (:obj:`int`): The maximum number of retries to be made in case of a :exc:`~telegram.error.RetryAfter` exception. If set to 0, no retries will be made. Defaults to ``0``. """ __slots__ = ( "_base_limiter", "_group_limiters", "_group_max_rate", "_group_time_period", "_max_retries", "_retry_after_event", ) def __init__( self, overall_max_rate: float = 30, overall_time_period: float = 1, group_max_rate: float = 20, group_time_period: float = 60, max_retries: int = 0, ) -> None: if not AIO_LIMITER_AVAILABLE: raise RuntimeError( "To use `AIORateLimiter`, PTB must be installed via `pip install " '"python-telegram-bot[rate-limiter]"`.' ) if overall_max_rate and overall_time_period: self._base_limiter: Optional[AsyncLimiter] = AsyncLimiter( max_rate=overall_max_rate, time_period=overall_time_period ) else: self._base_limiter = None if group_max_rate and group_time_period: self._group_max_rate: float = group_max_rate self._group_time_period: float = group_time_period else: self._group_max_rate = 0 self._group_time_period = 0 self._group_limiters: Dict[Union[str, int], AsyncLimiter] = {} self._max_retries: int = max_retries self._retry_after_event = asyncio.Event() self._retry_after_event.set() async def initialize(self) -> None: """Does nothing.""" async def shutdown(self) -> None: """Does nothing.""" def _get_group_limiter(self, group_id: Union[str, int, bool]) -> "AsyncLimiter": # Remove limiters that haven't been used for so long that all their capacity is unused # We only do that if we have a lot of limiters lying around to avoid looping on every call # This is a minimal effort approach - a full-fledged cache could use a TTL approach # or at least adapt the threshold dynamically depending on the number of active limiters if len(self._group_limiters) > 512: # We copy to avoid modifying the dict while we iterate over it for key, limiter in self._group_limiters.copy().items(): if key == group_id: continue if limiter.has_capacity(limiter.max_rate): del self._group_limiters[key] if group_id not in self._group_limiters: self._group_limiters[group_id] = AsyncLimiter( max_rate=self._group_max_rate, time_period=self._group_time_period, ) return self._group_limiters[group_id] async def _run_request( self, chat: bool, group: Union[str, int, bool], callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, List[JSONDict]]]], args: Any, kwargs: Dict[str, Any], ) -> Union[bool, JSONDict, List[JSONDict]]: base_context = self._base_limiter if (chat and self._base_limiter) else null_context() group_context = ( self._get_group_limiter(group) if group and self._group_max_rate else null_context() ) async with group_context, base_context: # In case a retry_after was hit, we wait with processing the request await self._retry_after_event.wait() return await callback(*args, **kwargs) # mypy doesn't understand that the last run of the for loop raises an exception async def process_request( self, callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, List[JSONDict]]]], args: Any, kwargs: Dict[str, Any], endpoint: str, data: Dict[str, Any], rate_limit_args: Optional[int], ) -> Union[bool, JSONDict, List[JSONDict]]: """ Processes a request by applying rate limiting. See :meth:`telegram.ext.BaseRateLimiter.process_request` for detailed information on the arguments. Args: rate_limit_args (:obj:`None` | :obj:`int`): If set, specifies the maximum number of retries to be made in case of a :exc:`~telegram.error.RetryAfter` exception. Defaults to :paramref:`AIORateLimiter.max_retries`. """ max_retries = rate_limit_args or self._max_retries group: Union[int, str, bool] = False chat: bool = False chat_id = data.get("chat_id") if chat_id is not None: chat = True # In case user passes integer chat id as string with contextlib.suppress(ValueError, TypeError): chat_id = int(chat_id) if (isinstance(chat_id, int) and chat_id < 0) or isinstance(chat_id, str): # string chat_id only works for channels and supergroups # We can't really tell channels from groups though ... group = chat_id for i in range(max_retries + 1): try: return await self._run_request( chat=chat, group=group, callback=callback, args=args, kwargs=kwargs ) except RetryAfter as exc: if i == max_retries: _LOGGER.exception( "Rate limit hit after maximum of %d retries", max_retries, exc_info=exc ) raise exc sleep = exc.retry_after + 0.1 _LOGGER.info("Rate limit hit. Retrying after %f seconds", sleep) # Make sure we don't allow other requests to be processed self._retry_after_event.clear() await asyncio.sleep(sleep) finally: # Allow other requests to be processed self._retry_after_event.set() return None # type: ignore[return-value] python-telegram-bot-21.1.1/telegram/ext/_application.py000066400000000000000000002361421460724040100231170ustar00rootroot00000000000000#!/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 Application class.""" import asyncio import contextlib import inspect import itertools import platform import signal import sys from collections import defaultdict from copy import deepcopy from pathlib import Path from types import MappingProxyType, TracebackType from typing import ( TYPE_CHECKING, Any, AsyncContextManager, Awaitable, Callable, Coroutine, DefaultDict, Dict, Generator, Generic, List, Mapping, NoReturn, Optional, Sequence, Set, Tuple, Type, TypeVar, Union, ) from telegram._update import Update from telegram._utils.defaultvalue import ( DEFAULT_80, DEFAULT_IP, DEFAULT_NONE, DEFAULT_TRUE, DefaultValue, ) from telegram._utils.logging import get_logger from telegram._utils.repr import build_repr_with_selected_attrs from telegram._utils.types import SCT, DVType, ODVInput from telegram._utils.warnings import warn from telegram.error import TelegramError from telegram.ext._basepersistence import BasePersistence from telegram.ext._contexttypes import ContextTypes from telegram.ext._extbot import ExtBot from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._updater import Updater from telegram.ext._utils.stack import was_called_by from telegram.ext._utils.trackingdict import TrackingDict from telegram.ext._utils.types import BD, BT, CCT, CD, JQ, RT, UD, ConversationKey, HandlerCallback from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from socket import socket from telegram import Message from telegram.ext import ConversationHandler, JobQueue from telegram.ext._applicationbuilder import InitApplicationBuilder from telegram.ext._baseupdateprocessor import BaseUpdateProcessor from telegram.ext._jobqueue import Job DEFAULT_GROUP: int = 0 _AppType = TypeVar("_AppType", bound="Application") # pylint: disable=invalid-name _STOP_SIGNAL = object() _DEFAULT_0 = DefaultValue(0) # Since python 3.12, the coroutine passed to create_task should not be an (async) generator. Remove # this check when we drop support for python 3.11. if sys.version_info >= (3, 12): _CoroType = Awaitable[RT] else: _CoroType = Union[Generator["asyncio.Future[object]", None, RT], Awaitable[RT]] _ErrorCoroType = Optional[_CoroType[RT]] _LOGGER = get_logger(__name__) class ApplicationHandlerStop(Exception): """ Raise this in a handler or an error handler to prevent execution of any other handler (even in different groups). In order to use this exception in a :class:`telegram.ext.ConversationHandler`, pass the optional :paramref:`state` parameter instead of returning the next state: .. code-block:: python async def conversation_callback(update, context): ... raise ApplicationHandlerStop(next_state) Note: Has no effect, if the handler or error handler is run in a non-blocking way. Args: state (:obj:`object`, optional): The next state of the conversation. Attributes: state (:obj:`object`): Optional. The next state of the conversation. """ __slots__ = ("state",) def __init__(self, state: Optional[object] = None) -> None: super().__init__() self.state: Optional[object] = state class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Application"]): """This class dispatches all kinds of updates to its registered handlers, and is the entry point to a PTB application. Tip: This class may not be initialized directly. Use :class:`telegram.ext.ApplicationBuilder` or :meth:`builder` (for convenience). Instances of this class can be used as asyncio context managers, where .. code:: python async with application: # code is roughly equivalent to .. code:: python try: await application.initialize() # code finally: await application.shutdown() .. seealso:: :meth:`__aenter__` and :meth:`__aexit__`. This class is a :class:`~typing.Generic` class and accepts six type variables: 1. The type of :attr:`bot`. Must be :class:`telegram.Bot` or a subclass of that class. 2. The type of the argument ``context`` of callback functions for (error) handlers and jobs. Must be :class:`telegram.ext.CallbackContext` or a subclass of that class. This must be consistent with the following types. 3. The type of the values of :attr:`user_data`. 4. The type of the values of :attr:`chat_data`. 5. The type of :attr:`bot_data`. 6. The type of :attr:`job_queue`. Must either be :class:`telegram.ext.JobQueue` or a subclass of that or :obj:`None`. Examples: :any:`Echo Bot ` .. seealso:: :wiki:`Your First Bot `, :wiki:`Architecture Overview ` .. versionchanged:: 20.0 * Initialization is now done through the :class:`telegram.ext.ApplicationBuilder`. * Removed the attribute ``groups``. Attributes: bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers. update_queue (:class:`asyncio.Queue`): The synchronized queue that will contain the updates. updater (:class:`telegram.ext.Updater`): Optional. The updater used by this application. chat_data (:obj:`types.MappingProxyType`): A dictionary handlers can use to store data for the chat. For each integer chat id, the corresponding value of this mapping is available as :attr:`telegram.ext.CallbackContext.chat_data` in handler callbacks for updates from that chat. .. versionchanged:: 20.0 :attr:`chat_data` is now read-only. Note that the values of the mapping are still mutable, i.e. editing ``context.chat_data`` within a handler callback is possible (and encouraged), but editing the mapping ``application.chat_data`` itself is not. .. tip:: * Manually modifying :attr:`chat_data` is almost never needed and unadvisable. * Entries are never deleted automatically from this mapping. If you want to delete the data associated with a specific chat, e.g. if the bot got removed from that chat, please use :meth:`drop_chat_data`. user_data (:obj:`types.MappingProxyType`): A dictionary handlers can use to store data for the user. For each integer user id, the corresponding value of this mapping is available as :attr:`telegram.ext.CallbackContext.user_data` in handler callbacks for updates from that user. .. versionchanged:: 20.0 :attr:`user_data` is now read-only. Note that the values of the mapping are still mutable, i.e. editing ``context.user_data`` within a handler callback is possible (and encouraged), but editing the mapping ``application.user_data`` itself is not. .. tip:: * Manually modifying :attr:`user_data` is almost never needed and unadvisable. * Entries are never deleted automatically from this mapping. If you want to delete the data associated with a specific user, e.g. if that user blocked the bot, please use :meth:`drop_user_data`. bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot. persistence (:class:`telegram.ext.BasePersistence`): The persistence class to store data that should be persistent over restarts. handlers (Dict[:obj:`int`, List[:class:`telegram.ext.BaseHandler`]]): A dictionary mapping each handler group to the list of handlers registered to that group. .. seealso:: :meth:`add_handler`, :meth:`add_handlers`. error_handlers (Dict[:term:`coroutine function`, :obj:`bool`]): A dictionary where the keys are error handlers and the values indicate whether they are to be run blocking. .. seealso:: :meth:`add_error_handler` context_types (:class:`telegram.ext.ContextTypes`): Specifies the types used by this dispatcher for the ``context`` argument of handler and job callbacks. post_init (:term:`coroutine function`): Optional. A callback that will be executed by :meth:`Application.run_polling` and :meth:`Application.run_webhook` after initializing the application via :meth:`initialize`. post_shutdown (:term:`coroutine function`): Optional. A callback that will be executed by :meth:`Application.run_polling` and :meth:`Application.run_webhook` after shutting down the application via :meth:`shutdown`. post_stop (:term:`coroutine function`): Optional. A callback that will be executed by :meth:`Application.run_polling` and :meth:`Application.run_webhook` after stopping the application via :meth:`stop`. .. versionadded:: 20.1 """ __slots__ = ( "__create_task_tasks", "__update_fetcher_task", "__update_persistence_event", "__update_persistence_lock", "__update_persistence_task", # Allowing '__weakref__' creation here since we need it for the JobQueue # Uncomment if necessary - currently the __weakref__ slot is already created # in the AsyncContextManager base class # "__weakref__", "_chat_data", "_chat_ids_to_be_deleted_in_persistence", "_chat_ids_to_be_updated_in_persistence", "_conversation_handler_conversations", "_initialized", "_job_queue", "_running", "_update_processor", "_user_data", "_user_ids_to_be_deleted_in_persistence", "_user_ids_to_be_updated_in_persistence", "bot", "bot_data", "chat_data", "context_types", "error_handlers", "handlers", "persistence", "post_init", "post_shutdown", "post_stop", "update_queue", "updater", "user_data", ) def __init__( self: "Application[BT, CCT, UD, CD, BD, JQ]", *, bot: BT, update_queue: "asyncio.Queue[object]", updater: Optional[Updater], job_queue: JQ, update_processor: "BaseUpdateProcessor", persistence: Optional[BasePersistence[UD, CD, BD]], context_types: ContextTypes[CCT, UD, CD, BD], post_init: Optional[ Callable[["Application[BT, CCT, UD, CD, BD, JQ]"], Coroutine[Any, Any, None]] ], post_shutdown: Optional[ Callable[["Application[BT, CCT, UD, CD, BD, JQ]"], Coroutine[Any, Any, None]] ], post_stop: Optional[ Callable[["Application[BT, CCT, UD, CD, BD, JQ]"], Coroutine[Any, Any, None]] ], ): if not was_called_by( inspect.currentframe(), Path(__file__).parent.resolve() / "_applicationbuilder.py" ): warn( "`Application` instances should be built via the `ApplicationBuilder`.", stacklevel=2, ) self.bot: BT = bot self.update_queue: asyncio.Queue[object] = update_queue self.context_types: ContextTypes[CCT, UD, CD, BD] = context_types self.updater: Optional[Updater] = updater self.handlers: Dict[int, List[BaseHandler[Any, CCT]]] = {} self.error_handlers: Dict[ HandlerCallback[object, CCT, None], Union[bool, DefaultValue[bool]] ] = {} self.post_init: Optional[ Callable[[Application[BT, CCT, UD, CD, BD, JQ]], Coroutine[Any, Any, None]] ] = post_init self.post_shutdown: Optional[ Callable[[Application[BT, CCT, UD, CD, BD, JQ]], Coroutine[Any, Any, None]] ] = post_shutdown self.post_stop: Optional[ Callable[[Application[BT, CCT, UD, CD, BD, JQ]], Coroutine[Any, Any, None]] ] = post_stop self._update_processor = update_processor self.bot_data: BD = self.context_types.bot_data() self._user_data: DefaultDict[int, UD] = defaultdict(self.context_types.user_data) self._chat_data: DefaultDict[int, CD] = defaultdict(self.context_types.chat_data) # Read only mapping self.user_data: Mapping[int, UD] = MappingProxyType(self._user_data) self.chat_data: Mapping[int, CD] = MappingProxyType(self._chat_data) self.persistence: Optional[BasePersistence[UD, CD, BD]] = None if persistence and not isinstance(persistence, BasePersistence): raise TypeError("persistence must be based on telegram.ext.BasePersistence") self.persistence = persistence # Some bookkeeping for persistence logic self._chat_ids_to_be_updated_in_persistence: Set[int] = set() self._user_ids_to_be_updated_in_persistence: Set[int] = set() self._chat_ids_to_be_deleted_in_persistence: Set[int] = set() self._user_ids_to_be_deleted_in_persistence: Set[int] = set() # This attribute will hold references to the conversation dicts of all conversation # handlers so that we can extract the changed states during `update_persistence` self._conversation_handler_conversations: Dict[ str, TrackingDict[ConversationKey, object] ] = {} # A number of low-level helpers for the internal logic self._initialized = False self._running = False self._job_queue: JQ = job_queue self.__update_fetcher_task: Optional[asyncio.Task] = None self.__update_persistence_task: Optional[asyncio.Task] = None self.__update_persistence_event = asyncio.Event() self.__update_persistence_lock = asyncio.Lock() self.__create_task_tasks: Set[asyncio.Task] = set() # Used for awaiting tasks upon exit async def __aenter__(self: _AppType) -> _AppType: # noqa: PYI019 """|async_context_manager| :meth:`initializes ` the App. Returns: The initialized App instance. Raises: :exc:`Exception`: If an exception is raised during initialization, :meth:`shutdown` is called in this case. """ try: await self.initialize() return self except Exception as exc: await self.shutdown() raise exc 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 App.""" # 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 __repr__(self) -> str: """Give a string representation of the application in the form ``Application[bot=...]``. 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, bot=self.bot) @property def running(self) -> bool: """:obj:`bool`: Indicates if this application is running. .. seealso:: :meth:`start`, :meth:`stop` """ return self._running @property def concurrent_updates(self) -> int: """:obj:`int`: The number of concurrent updates that will be processed in parallel. A value of ``0`` indicates updates are *not* being processed concurrently. .. versionchanged:: 20.4 This is now just a shortcut to :attr:`update_processor.max_concurrent_updates `. .. seealso:: :wiki:`Concurrency` """ return self._update_processor.max_concurrent_updates @property def job_queue(self) -> Optional["JobQueue[CCT]"]: """ :class:`telegram.ext.JobQueue`: The :class:`JobQueue` used by the :class:`telegram.ext.Application`. .. seealso:: :wiki:`Job Queue ` """ if self._job_queue is None: warn( "No `JobQueue` set up. To use `JobQueue`, you must install PTB via " '`pip install "python-telegram-bot[job-queue]"`.', stacklevel=2, ) return self._job_queue @property def update_processor(self) -> "BaseUpdateProcessor": """:class:`telegram.ext.BaseUpdateProcessor`: The update processor used by this application. .. seealso:: :wiki:`Concurrency` .. versionadded:: 20.4 """ return self._update_processor @staticmethod def _raise_system_exit() -> NoReturn: raise SystemExit @staticmethod def builder() -> "InitApplicationBuilder": """Convenience method. Returns a new :class:`telegram.ext.ApplicationBuilder`. .. versionadded:: 20.0 """ # Unfortunately this needs to be here due to cyclical imports from telegram.ext import ApplicationBuilder # pylint: disable=import-outside-toplevel return ApplicationBuilder() def _check_initialized(self) -> None: if not self._initialized: raise RuntimeError( "This Application was not initialized via `Application.initialize`!" ) async def initialize(self) -> None: """Initializes the Application by initializing: * The :attr:`bot`, by calling :meth:`telegram.Bot.initialize`. * The :attr:`updater`, by calling :meth:`telegram.ext.Updater.initialize`. * The :attr:`persistence`, by loading persistent conversations and data. * The :attr:`update_processor` by calling :meth:`telegram.ext.BaseUpdateProcessor.initialize`. Does *not* call :attr:`post_init` - that is only done by :meth:`run_polling` and :meth:`run_webhook`. .. seealso:: :meth:`shutdown` """ if self._initialized: _LOGGER.debug("This Application is already initialized.") return await self.bot.initialize() await self._update_processor.initialize() if self.updater: await self.updater.initialize() if not self.persistence: self._initialized = True return await self._initialize_persistence() # Unfortunately due to circular imports this has to be here # pylint: disable=import-outside-toplevel from telegram.ext._handlers.conversationhandler import ConversationHandler # Initialize the persistent conversation handlers with the stored states for handler in itertools.chain.from_iterable(self.handlers.values()): if isinstance(handler, ConversationHandler) and handler.persistent and handler.name: await self._add_ch_to_persistence(handler) self._initialized = True async def _add_ch_to_persistence(self, handler: "ConversationHandler") -> None: self._conversation_handler_conversations.update( await handler._initialize_persistence(self) # pylint: disable=protected-access ) async def shutdown(self) -> None: """Shuts down the Application by shutting down: * :attr:`bot` by calling :meth:`telegram.Bot.shutdown` * :attr:`updater` by calling :meth:`telegram.ext.Updater.shutdown` * :attr:`persistence` by calling :meth:`update_persistence` and :meth:`BasePersistence.flush` * :attr:`update_processor` by calling :meth:`telegram.ext.BaseUpdateProcessor.shutdown` Does *not* call :attr:`post_shutdown` - that is only done by :meth:`run_polling` and :meth:`run_webhook`. .. seealso:: :meth:`initialize` Raises: :exc:`RuntimeError`: If the application is still :attr:`running`. """ if self.running: raise RuntimeError("This Application is still running!") if not self._initialized: _LOGGER.debug("This Application is already shut down. Returning.") return await self.bot.shutdown() await self._update_processor.shutdown() if self.updater: await self.updater.shutdown() if self.persistence: _LOGGER.debug("Updating & flushing persistence before shutdown") await self.update_persistence() await self.persistence.flush() _LOGGER.debug("Updated and flushed persistence") self._initialized = False async def _initialize_persistence(self) -> None: """This method basically just loads all the data by awaiting the BP methods""" if not self.persistence: return if self.persistence.store_data.user_data: self._user_data.update(await self.persistence.get_user_data()) if self.persistence.store_data.chat_data: self._chat_data.update(await self.persistence.get_chat_data()) if self.persistence.store_data.bot_data: self.bot_data = await self.persistence.get_bot_data() if not isinstance(self.bot_data, self.context_types.bot_data): raise ValueError( f"bot_data must be of type {self.context_types.bot_data.__name__}" ) # Mypy doesn't know that persistence.set_bot (see above) already checks that # self.bot is an instance of ExtBot if callback_data should be stored ... if self.persistence.store_data.callback_data and ( self.bot.callback_data_cache is not None # type: ignore[attr-defined] ): persistent_data = await self.persistence.get_callback_data() if persistent_data is not None: if not isinstance(persistent_data, tuple) or len(persistent_data) != 2: raise ValueError("callback_data must be a tuple of length 2") self.bot.callback_data_cache.load_persistence_data( # type: ignore[attr-defined] persistent_data ) async def start(self) -> None: """Starts * a background task that fetches updates from :attr:`update_queue` and processes them via :meth:`process_update`. * :attr:`job_queue`, if set. * a background task that calls :meth:`update_persistence` in regular intervals, if :attr:`persistence` is set. Note: This does *not* start fetching updates from Telegram. To fetch updates, you need to either start :attr:`updater` manually or use one of :meth:`run_polling` or :meth:`run_webhook`. Tip: When using a custom logic for startup and shutdown of the application, eventual cancellation of pending tasks should happen only `after` :meth:`stop` has been called in order to ensure that the tasks mentioned above are not cancelled prematurely. .. seealso:: :meth:`stop` Raises: :exc:`RuntimeError`: If the application is already running or was not initialized. """ if self.running: raise RuntimeError("This Application is already running!") self._check_initialized() self._running = True self.__update_persistence_event.clear() try: if self.persistence: self.__update_persistence_task = asyncio.create_task( self._persistence_updater(), name=f"Application:{self.bot.id}:persistence_updater", ) _LOGGER.debug("Loop for updating persistence started") if self._job_queue: await self._job_queue.start() # type: ignore[union-attr] _LOGGER.debug("JobQueue started") self.__update_fetcher_task = asyncio.create_task( self._update_fetcher(), name=f"Application:{self.bot.id}:update_fetcher" ) _LOGGER.info("Application started") except Exception as exc: self._running = False raise exc async def stop(self) -> None: """Stops the process after processing any pending updates or tasks created by :meth:`create_task`. Also stops :attr:`job_queue`, if set. Finally, calls :meth:`update_persistence` and :meth:`BasePersistence.flush` on :attr:`persistence`, if set. Warning: Once this method is called, no more updates will be fetched from :attr:`update_queue`, even if it's not empty. .. seealso:: :meth:`start` Note: * This does *not* stop :attr:`updater`. You need to either manually call :meth:`telegram.ext.Updater.stop` or use one of :meth:`run_polling` or :meth:`run_webhook`. * Does *not* call :attr:`post_stop` - that is only done by :meth:`run_polling` and :meth:`run_webhook`. Raises: :exc:`RuntimeError`: If the application is not running. """ if not self.running: raise RuntimeError("This Application is not running!") self._running = False _LOGGER.info("Application is stopping. This might take a moment.") # Stop listening for new updates and handle all pending ones await self.update_queue.put(_STOP_SIGNAL) _LOGGER.debug("Waiting for update_queue to join") await self.update_queue.join() if self.__update_fetcher_task: await self.__update_fetcher_task _LOGGER.debug("Application stopped fetching of updates.") if self._job_queue: _LOGGER.debug("Waiting for running jobs to finish") await self._job_queue.stop(wait=True) # type: ignore[union-attr] _LOGGER.debug("JobQueue stopped") _LOGGER.debug("Waiting for `create_task` calls to be processed") await asyncio.gather(*self.__create_task_tasks, return_exceptions=True) # Make sure that this is the *last* step of stopping the application! if self.persistence and self.__update_persistence_task: _LOGGER.debug("Waiting for persistence loop to finish") self.__update_persistence_event.set() await self.__update_persistence_task self.__update_persistence_event.clear() _LOGGER.info("Application.stop() complete") def stop_running(self) -> None: """This method can be used to stop the execution of :meth:`run_polling` or :meth:`run_webhook` from within a handler, job or error callback. This allows a graceful shutdown of the application, i.e. the methods listed in :attr:`run_polling` and :attr:`run_webhook` will still be executed. Note: If the application is not running, this method does nothing. .. versionadded:: 20.5 """ if self.running: # This works because `__run` is using `loop.run_forever()`. If that changes, this # method needs to be adapted. asyncio.get_running_loop().stop() else: _LOGGER.debug("Application is not running, stop_running() does nothing.") def run_polling( self, poll_interval: float = 0.0, timeout: int = 10, bootstrap_retries: int = -1, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, allowed_updates: Optional[List[str]] = None, drop_pending_updates: Optional[bool] = None, close_loop: bool = True, stop_signals: ODVInput[Sequence[int]] = DEFAULT_NONE, ) -> None: """Convenience method that takes care of initializing and starting the app, polling updates from Telegram using :meth:`telegram.ext.Updater.start_polling` and a graceful shutdown of the app on exit. The app will shut down when :exc:`KeyboardInterrupt` or :exc:`SystemExit` is raised. On unix, the app will also shut down on receiving the signals specified by :paramref:`stop_signals`. The order of execution by :meth:`run_polling` is roughly as follows: - :meth:`initialize` - :meth:`post_init` - :meth:`telegram.ext.Updater.start_polling` - :meth:`start` - Run the application until the users stops it - :meth:`telegram.ext.Updater.stop` - :meth:`stop` - :meth:`post_stop` - :meth:`shutdown` - :meth:`post_shutdown` .. include:: inclusions/application_run_tip.rst Args: poll_interval (:obj:`float`, optional): Time to wait between polling updates from Telegram in seconds. Default is ``0.0``. timeout (:obj:`int`, optional): Passed to :paramref:`telegram.Bot.get_updates.timeout`. Default is ``10`` seconds. bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the :class:`telegram.ext.Updater` will retry on failures on the Telegram server. * < 0 - retry indefinitely (default) * 0 - no retries * > 0 - retry up to X times read_timeout (:obj:`float`, optional): Value to pass to :paramref:`telegram.Bot.get_updates.read_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. versionchanged:: 20.7 Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE` instead of ``2``. .. deprecated:: 20.7 Deprecated in favor of setting the timeout via :meth:`telegram.ext.ApplicationBuilder.get_updates_read_timeout`. write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.Bot.get_updates.write_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. deprecated:: 20.7 Deprecated in favor of setting the timeout via :meth:`telegram.ext.ApplicationBuilder.get_updates_write_timeout`. connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.Bot.get_updates.connect_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. deprecated:: 20.7 Deprecated in favor of setting the timeout via :meth:`telegram.ext.ApplicationBuilder.get_updates_connect_timeout`. pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.Bot.get_updates.pool_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. deprecated:: 20.7 Deprecated in favor of setting the timeout via :meth:`telegram.ext.ApplicationBuilder.get_updates_pool_timeout`. drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers before actually starting to poll. Default is :obj:`False`. allowed_updates (List[:obj:`str`], optional): Passed to :meth:`telegram.Bot.get_updates`. close_loop (:obj:`bool`, optional): If :obj:`True`, the current event loop will be closed upon shutdown. Defaults to :obj:`True`. .. seealso:: :meth:`asyncio.loop.close` stop_signals (Sequence[:obj:`int`] | :obj:`None`, optional): Signals that will shut down the app. Pass :obj:`None` to not use stop signals. Defaults to :data:`signal.SIGINT`, :data:`signal.SIGTERM` and :data:`signal.SIGABRT` on non Windows platforms. Caution: Not every :class:`asyncio.AbstractEventLoop` implements :meth:`asyncio.loop.add_signal_handler`. Most notably, the standard event loop on Windows, :class:`asyncio.ProactorEventLoop`, does not implement this method. If this method is not available, stop signals can not be set. Raises: :exc:`RuntimeError`: If the Application does not have an :class:`telegram.ext.Updater`. """ if not self.updater: raise RuntimeError( "Application.run_polling is only available if the application has an Updater." ) if (read_timeout, write_timeout, connect_timeout, pool_timeout) != ((DEFAULT_NONE,) * 4): warn( "Setting timeouts via `Application.run_polling` is deprecated. " "Please use `ApplicationBuilder.get_updates_*_timeout` instead.", PTBDeprecationWarning, stacklevel=2, ) def error_callback(exc: TelegramError) -> None: self.create_task(self.process_error(error=exc, update=None)) return self.__run( updater_coroutine=self.updater.start_polling( poll_interval=poll_interval, timeout=timeout, bootstrap_retries=bootstrap_retries, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, allowed_updates=allowed_updates, drop_pending_updates=drop_pending_updates, error_callback=error_callback, # if there is an error in fetching updates ), close_loop=close_loop, stop_signals=stop_signals, ) def run_webhook( self, listen: DVType[str] = DEFAULT_IP, port: DVType[int] = DEFAULT_80, url_path: str = "", cert: Optional[Union[str, Path]] = None, key: Optional[Union[str, Path]] = None, bootstrap_retries: int = 0, webhook_url: Optional[str] = None, allowed_updates: Optional[List[str]] = None, drop_pending_updates: Optional[bool] = None, ip_address: Optional[str] = None, max_connections: int = 40, close_loop: bool = True, stop_signals: ODVInput[Sequence[int]] = DEFAULT_NONE, secret_token: Optional[str] = None, unix: Optional[Union[str, Path, "socket"]] = None, ) -> None: """Convenience method that takes care of initializing and starting the app, listening for updates from Telegram using :meth:`telegram.ext.Updater.start_webhook` and a graceful shutdown of the app on exit. The app will shut down when :exc:`KeyboardInterrupt` or :exc:`SystemExit` is raised. On unix, the app will also shut down on receiving the signals specified by :paramref:`stop_signals`. If :paramref:`cert` and :paramref:`key` are not provided, the webhook will be started directly on ``http://listen:port/url_path``, so SSL can be handled by another application. Else, the webhook will be started on ``https://listen:port/url_path``. Also calls :meth:`telegram.Bot.set_webhook` as required. The order of execution by :meth:`run_webhook` is roughly as follows: - :meth:`initialize` - :meth:`post_init` - :meth:`telegram.ext.Updater.start_webhook` - :meth:`start` - Run the application until the users stops it - :meth:`telegram.ext.Updater.stop` - :meth:`stop` - :meth:`post_stop` - :meth:`shutdown` - :meth:`post_shutdown` Important: If you want to use this method, you must install PTB with the optional requirement ``webhooks``, i.e. .. code-block:: bash pip install "python-telegram-bot[webhooks]" .. include:: inclusions/application_run_tip.rst .. seealso:: :wiki:`Webhooks` Args: listen (:obj:`str`, optional): IP-Address to listen on. Defaults to `127.0.0.1 `_. port (:obj:`int`, optional): Port the bot should be listening on. Must be one of :attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS` unless the bot is running behind a proxy. Defaults to ``80``. url_path (:obj:`str`, optional): Path inside url. Defaults to `` '' `` cert (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL certificate file. key (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL key file. bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the :class:`telegram.ext.Updater` will retry on failures on the Telegram server. * < 0 - retry indefinitely * 0 - no retries (default) * > 0 - retry up to X times webhook_url (:obj:`str`, optional): Explicitly specify the webhook url. Useful behind NAT, reverse proxy, etc. Default is derived from :paramref:`listen`, :paramref:`port`, :paramref:`url_path`, :paramref:`cert`, and :paramref:`key`. allowed_updates (List[:obj:`str`], optional): Passed to :meth:`telegram.Bot.set_webhook`. drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers before actually starting to poll. Default is :obj:`False`. ip_address (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`. max_connections (:obj:`int`, optional): Passed to :meth:`telegram.Bot.set_webhook`. Defaults to ``40``. close_loop (:obj:`bool`, optional): If :obj:`True`, the current event loop will be closed upon shutdown. Defaults to :obj:`True`. .. seealso:: :meth:`asyncio.loop.close` stop_signals (Sequence[:obj:`int`] | :obj:`None`, optional): Signals that will shut down the app. Pass :obj:`None` to not use stop signals. Defaults to :data:`signal.SIGINT`, :data:`signal.SIGTERM` and :data:`signal.SIGABRT`. Caution: Not every :class:`asyncio.AbstractEventLoop` implements :meth:`asyncio.loop.add_signal_handler`. Most notably, the standard event loop on Windows, :class:`asyncio.ProactorEventLoop`, does not implement this method. If this method is not available, stop signals can not be set. secret_token (:obj:`str`, optional): Secret token to ensure webhook requests originate from Telegram. See :paramref:`telegram.Bot.set_webhook.secret_token` for more details. When added, the web server started by this call will expect the token to be set in the ``X-Telegram-Bot-Api-Secret-Token`` header of an incoming request and will raise a :class:`http.HTTPStatus.FORBIDDEN ` error if either the header isn't set or it is set to a wrong token. .. versionadded:: 20.0 unix (:class:`pathlib.Path` | :obj:`str` | :class:`socket.socket`, optional): Can be either: * the path to the unix socket file as :class:`pathlib.Path` or :obj:`str`. This will be passed to `tornado.netutil.bind_unix_socket `_ to create the socket. If the Path does not exist, the file will be created. * or the socket itself. This option allows you to e.g. restrict the permissions of the socket for improved security. Note that you need to pass the correct family, type and socket options yourself. Caution: This parameter is a replacement for the default TCP bind. Therefore, it is mutually exclusive with :paramref:`listen` and :paramref:`port`. When using this param, you must also run a reverse proxy to the unix socket and set the appropriate :paramref:`webhook_url`. .. versionadded:: 20.8 .. versionchanged:: 21.1 Added support to pass a socket instance itself. """ if not self.updater: raise RuntimeError( "Application.run_webhook is only available if the application has an Updater." ) return self.__run( updater_coroutine=self.updater.start_webhook( listen=listen, port=port, url_path=url_path, cert=cert, key=key, bootstrap_retries=bootstrap_retries, drop_pending_updates=drop_pending_updates, webhook_url=webhook_url, allowed_updates=allowed_updates, ip_address=ip_address, max_connections=max_connections, secret_token=secret_token, unix=unix, ), close_loop=close_loop, stop_signals=stop_signals, ) def __run( self, updater_coroutine: Coroutine, stop_signals: ODVInput[Sequence[int]], close_loop: bool = True, ) -> None: # Calling get_event_loop() should still be okay even in py3.10+ as long as there is a # running event loop or we are in the main thread, which are the intended use cases. # See the docs of get_event_loop() and get_running_loop() for more info loop = asyncio.get_event_loop() if stop_signals is DEFAULT_NONE and platform.system() != "Windows": stop_signals = (signal.SIGINT, signal.SIGTERM, signal.SIGABRT) try: if not isinstance(stop_signals, DefaultValue): for sig in stop_signals or []: loop.add_signal_handler(sig, self._raise_system_exit) except NotImplementedError as exc: warn( f"Could not add signal handlers for the stop signals {stop_signals} due to " f"exception `{exc!r}`. If your event loop does not implement `add_signal_handler`," " please pass `stop_signals=None`.", stacklevel=3, ) try: loop.run_until_complete(self.initialize()) if self.post_init: loop.run_until_complete(self.post_init(self)) loop.run_until_complete(updater_coroutine) # one of updater.start_webhook/polling loop.run_until_complete(self.start()) loop.run_forever() except (KeyboardInterrupt, SystemExit): _LOGGER.debug("Application received stop signal. Shutting down.") except Exception as exc: # In case the coroutine wasn't awaited, we don't need to bother the user with a warning updater_coroutine.close() raise exc finally: # We arrive here either by catching the exceptions above or if the loop gets stopped try: # Mypy doesn't know that we already check if updater is None if self.updater.running: # type: ignore[union-attr] loop.run_until_complete(self.updater.stop()) # type: ignore[union-attr] if self.running: loop.run_until_complete(self.stop()) if self.post_stop: loop.run_until_complete(self.post_stop(self)) loop.run_until_complete(self.shutdown()) if self.post_shutdown: loop.run_until_complete(self.post_shutdown(self)) finally: if close_loop: loop.close() def create_task( self, coroutine: _CoroType[RT], update: Optional[object] = None, *, name: Optional[str] = None, ) -> "asyncio.Task[RT]": """Thin wrapper around :func:`asyncio.create_task` that handles exceptions raised by the :paramref:`coroutine` with :meth:`process_error`. Note: * If :paramref:`coroutine` raises an exception, it will be set on the task created by this method even though it's handled by :meth:`process_error`. * If the application is currently running, tasks created by this method will be awaited with :meth:`stop`. .. seealso:: :wiki:`Concurrency` Args: coroutine (:term:`awaitable`): The awaitable to run as task. .. versionchanged:: 20.2 Accepts :class:`asyncio.Future` and generator-based coroutine functions. .. deprecated:: 20.4 Since Python 3.12, generator-based coroutine functions are no longer accepted. update (:obj:`object`, optional): If set, will be passed to :meth:`process_error` as additional information for the error handlers. Moreover, the corresponding :attr:`chat_data` and :attr:`user_data` entries will be updated in the next run of :meth:`update_persistence` after the :paramref:`coroutine` is finished. Keyword Args: name (:obj:`str`, optional): The name of the task. .. versionadded:: 20.4 Returns: :class:`asyncio.Task`: The created task. """ return self.__create_task(coroutine=coroutine, update=update, name=name) def __create_task( self, coroutine: _CoroType[RT], update: Optional[object] = None, is_error_handler: bool = False, name: Optional[str] = None, ) -> "asyncio.Task[RT]": # Unfortunately, we can't know if `coroutine` runs one of the error handler functions # but by passing `is_error_handler=True` from `process_error`, we can make sure that we # get at most one recursion of the user calls `create_task` manually with an error handler # function task: asyncio.Task[RT] = asyncio.create_task( self.__create_task_callback( coroutine=coroutine, update=update, is_error_handler=is_error_handler ), name=name, ) if self.running: self.__create_task_tasks.add(task) task.add_done_callback(self.__create_task_done_callback) else: warn( "Tasks created via `Application.create_task` while the application is not " "running won't be automatically awaited!", stacklevel=3, ) return task def __create_task_done_callback(self, task: asyncio.Task) -> None: self.__create_task_tasks.discard(task) # Discard from our set since we are done with it # We just retrieve the eventual exception so that asyncio doesn't complain in case # it's not retrieved somewhere else with contextlib.suppress(asyncio.CancelledError, asyncio.InvalidStateError): task.exception() async def __create_task_callback( self, coroutine: _CoroType[RT], update: Optional[object] = None, is_error_handler: bool = False, ) -> RT: try: # Generator-based coroutines are not supported in Python 3.12+ if sys.version_info < (3, 12) and isinstance(coroutine, Generator): warn( "Generator-based coroutines are deprecated in create_task and will not work" " in Python 3.12+", category=PTBDeprecationWarning, ) return await asyncio.create_task(coroutine) # If user uses generator in python 3.12+, Exception will happen and we cannot do # anything about it. (hence the type ignore if mypy is run on python 3.12-) return await coroutine # type: ignore[misc] except Exception as exception: if isinstance(exception, ApplicationHandlerStop): warn( "ApplicationHandlerStop is not supported with handlers running non-blocking.", stacklevel=1, ) # Avoid infinite recursion of error handlers. elif is_error_handler: _LOGGER.exception( "An error was raised and an uncaught error was raised while " "handling the error with an error_handler.", exc_info=exception, ) else: # If we arrive here, an exception happened in the task and was neither # ApplicationHandlerStop nor raised by an error handler. # So we can and must handle it await self.process_error(update=update, error=exception, coroutine=coroutine) # Raise exception so that it can be set on the task and retrieved by task.exception() raise exception finally: self._mark_for_persistence_update(update=update) async def _update_fetcher(self) -> None: # Continuously fetch updates from the queue. Exit only once the signal object is found. while True: try: update = await self.update_queue.get() if update is _STOP_SIGNAL: _LOGGER.debug("Dropping pending updates") while not self.update_queue.empty(): self.update_queue.task_done() # For the _STOP_SIGNAL self.update_queue.task_done() return _LOGGER.debug("Processing update %s", update) if self._update_processor.max_concurrent_updates > 1: # We don't await the below because it has to be run concurrently self.create_task( self.__process_update_wrapper(update), update=update, name=f"Application:{self.bot.id}:process_concurrent_update", ) else: await self.__process_update_wrapper(update) except asyncio.CancelledError: # This may happen if the application is manually run via application.start() and # then a KeyboardInterrupt is sent. We must prevent this loop to die since # application.stop() will wait for it's clean shutdown. _LOGGER.warning( "Fetching updates got a asyncio.CancelledError. Ignoring as this task may only" "be closed via `Application.stop`." ) async def __process_update_wrapper(self, update: object) -> None: await self._update_processor.process_update(update, self.process_update(update)) self.update_queue.task_done() async def process_update(self, update: object) -> None: """Processes a single update and marks the update to be updated by the persistence later. Exceptions raised by handler callbacks will be processed by :meth:`process_error`. .. seealso:: :wiki:`Concurrency` .. versionchanged:: 20.0 Persistence is now updated in an interval set by :attr:`telegram.ext.BasePersistence.update_interval`. Args: update (:class:`telegram.Update` | :obj:`object` | \ :class:`telegram.error.TelegramError`): The update to process. Raises: :exc:`RuntimeError`: If the application was not initialized. """ # Processing updates before initialize() is a problem e.g. if persistence is used self._check_initialized() context = None any_blocking = False # Flag which is set to True if any handler specifies block=True for handlers in self.handlers.values(): try: for handler in handlers: check = handler.check_update(update) # Should the handler handle this update? if not (check is None or check is False): # if yes, if not context: # build a context if not already built context = self.context_types.context.from_update(update, self) await context.refresh_data() coroutine: Coroutine = handler.handle_update(update, self, check, context) if not handler.block or ( # if handler is running with block=False, handler.block is DEFAULT_TRUE and isinstance(self.bot, ExtBot) and self.bot.defaults and not self.bot.defaults.block ): self.create_task( coroutine, update=update, name=( f"Application:{self.bot.id}:process_update_non_blocking" f":{handler}" ), ) else: any_blocking = True await coroutine break # Only a max of 1 handler per group is handled # Stop processing with any other handler. except ApplicationHandlerStop: _LOGGER.debug("Stopping further handlers due to ApplicationHandlerStop") break # Dispatch any error. except Exception as exc: if await self.process_error(update=update, error=exc): _LOGGER.debug("Error handler stopped further handlers.") break if any_blocking: # Only need to mark the update for persistence if there was at least one # blocking handler - the non-blocking handlers mark the update again when finished # (in __create_task_callback) self._mark_for_persistence_update(update=update) def add_handler(self, handler: BaseHandler[Any, CCT], group: int = DEFAULT_GROUP) -> None: """Register a handler. TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. End handling of update with :class:`telegram.ext.ApplicationHandlerStop`. A handler must be an instance of a subclass of :class:`telegram.ext.BaseHandler`. All handlers are organized in groups with a numeric value. The default group is 0. All groups will be evaluated for handling an update, but only 0 or 1 handler per group will be used. If :class:`telegram.ext.ApplicationHandlerStop` is raised from one of the handlers, no further handlers (regardless of the group) will be called. The priority/order of handlers is determined as follows: * Priority of the group (lower group number == higher priority) * The first handler in a group which can handle an update (see :attr:`telegram.ext.BaseHandler.check_update`) will be used. Other handlers from the group will not be used. The order in which handlers were added to the group defines the priority. Warning: Adding persistent :class:`telegram.ext.ConversationHandler` after the application has been initialized is discouraged. This is because the persisted conversation states need to be loaded into memory while the application is already processing updates, which might lead to race conditions and undesired behavior. In particular, current conversation states may be overridden by the loaded data. Args: handler (:class:`telegram.ext.BaseHandler`): A BaseHandler instance. group (:obj:`int`, optional): The group identifier. Default is ``0``. """ # Unfortunately due to circular imports this has to be here # pylint: disable=import-outside-toplevel from telegram.ext._handlers.conversationhandler import ConversationHandler if not isinstance(handler, BaseHandler): raise TypeError(f"handler is not an instance of {BaseHandler.__name__}") if not isinstance(group, int): raise TypeError("group is not int") if isinstance(handler, ConversationHandler) and handler.persistent and handler.name: if not self.persistence: raise ValueError( f"ConversationHandler {handler.name} " "can not be persistent if application has no persistence" ) if self._initialized: self.create_task( self._add_ch_to_persistence(handler), name=f"Application:{self.bot.id}:add_handler:conversation_handler_after_init", ) warn( "A persistent `ConversationHandler` was passed to `add_handler`, " "after `Application.initialize` was called. This is discouraged." "See the docs of `Application.add_handler` for details.", stacklevel=2, ) if group not in self.handlers: self.handlers[group] = [] self.handlers = dict(sorted(self.handlers.items())) # lower -> higher groups self.handlers[group].append(handler) def add_handlers( self, handlers: Union[ Union[List[BaseHandler[Any, CCT]], Tuple[BaseHandler[Any, CCT]]], Dict[int, Union[List[BaseHandler[Any, CCT]], Tuple[BaseHandler[Any, CCT]]]], ], group: Union[int, DefaultValue[int]] = _DEFAULT_0, ) -> None: """Registers multiple handlers at once. The order of the handlers in the passed sequence(s) matters. See :meth:`add_handler` for details. .. versionadded:: 20.0 Args: handlers (List[:class:`telegram.ext.BaseHandler`] | \ Dict[int, List[:class:`telegram.ext.BaseHandler`]]): \ Specify a sequence of handlers *or* a dictionary where the keys are groups and values are handlers. group (:obj:`int`, optional): Specify which group the sequence of :paramref:`handlers` should be added to. Defaults to ``0``. Example:: app.add_handlers(handlers={ -1: [MessageHandler(...)], 1: [CallbackQueryHandler(...), CommandHandler(...)] } """ if isinstance(handlers, dict) and not isinstance(group, DefaultValue): raise ValueError("The `group` argument can only be used with a sequence of handlers.") if isinstance(handlers, dict): for handler_group, grp_handlers in handlers.items(): if not isinstance(grp_handlers, (list, tuple)): raise ValueError(f"Handlers for group {handler_group} must be a list or tuple") for handler in grp_handlers: self.add_handler(handler, handler_group) elif isinstance(handlers, (list, tuple)): for handler in handlers: self.add_handler(handler, DefaultValue.get_value(group)) else: raise ValueError( "The `handlers` argument must be a sequence of handlers or a " "dictionary where the keys are groups and values are sequences of handlers." ) def remove_handler(self, handler: BaseHandler[Any, CCT], group: int = DEFAULT_GROUP) -> None: """Remove a handler from the specified group. Args: handler (:class:`telegram.ext.BaseHandler`): A :class:`telegram.ext.BaseHandler` instance. group (:obj:`object`, optional): The group identifier. Default is ``0``. """ if handler in self.handlers[group]: self.handlers[group].remove(handler) if not self.handlers[group]: del self.handlers[group] def drop_chat_data(self, chat_id: int) -> None: """Drops the corresponding entry from the :attr:`chat_data`. Will also be deleted from the persistence on the next run of :meth:`update_persistence`, if applicable. Warning: When using :attr:`concurrent_updates` or the :attr:`job_queue`, :meth:`process_update` or :meth:`telegram.ext.Job.run` may re-create this entry due to the asynchronous nature of these features. Please make sure that your program can avoid or handle such situations. .. versionadded:: 20.0 Args: chat_id (:obj:`int`): The chat id to delete. The entry will be deleted even if it is not empty. """ self._chat_data.pop(chat_id, None) self._chat_ids_to_be_deleted_in_persistence.add(chat_id) def drop_user_data(self, user_id: int) -> None: """Drops the corresponding entry from the :attr:`user_data`. Will also be deleted from the persistence on the next run of :meth:`update_persistence`, if applicable. Warning: When using :attr:`concurrent_updates` or the :attr:`job_queue`, :meth:`process_update` or :meth:`telegram.ext.Job.run` may re-create this entry due to the asynchronous nature of these features. Please make sure that your program can avoid or handle such situations. .. versionadded:: 20.0 Args: user_id (:obj:`int`): The user id to delete. The entry will be deleted even if it is not empty. """ self._user_data.pop(user_id, None) self._user_ids_to_be_deleted_in_persistence.add(user_id) def migrate_chat_data( self, message: Optional["Message"] = None, old_chat_id: Optional[int] = None, new_chat_id: Optional[int] = None, ) -> None: """Moves the contents of :attr:`chat_data` at key :paramref:`old_chat_id` to the key :paramref:`new_chat_id`. Also marks the entries to be updated accordingly in the next run of :meth:`update_persistence`. Warning: * Any data stored in :attr:`chat_data` at key :paramref:`new_chat_id` will be overridden * The key :paramref:`old_chat_id` of :attr:`chat_data` will be deleted * This does not update the :attr:`~telegram.ext.Job.chat_id` attribute of any scheduled :class:`telegram.ext.Job`. When using :attr:`concurrent_updates` or the :attr:`job_queue`, :meth:`process_update` or :meth:`telegram.ext.Job.run` may re-create the old entry due to the asynchronous nature of these features. Please make sure that your program can avoid or handle such situations. .. seealso:: :wiki:`Storing Bot, User and Chat Related Data\ ` Args: message (:class:`telegram.Message`, optional): A message with either :attr:`~telegram.Message.migrate_from_chat_id` or :attr:`~telegram.Message.migrate_to_chat_id`. Mutually exclusive with passing :paramref:`old_chat_id` and :paramref:`new_chat_id`. .. seealso:: :attr:`telegram.ext.filters.StatusUpdate.MIGRATE` old_chat_id (:obj:`int`, optional): The old chat ID. Mutually exclusive with passing :paramref:`message` new_chat_id (:obj:`int`, optional): The new chat ID. Mutually exclusive with passing :paramref:`message` Raises: ValueError: Raised if the input is invalid. """ if message and (old_chat_id or new_chat_id): raise ValueError("Message and chat_id pair are mutually exclusive") if not any((message, old_chat_id, new_chat_id)): raise ValueError("chat_id pair or message must be passed") if message: if message.migrate_from_chat_id is None and message.migrate_to_chat_id is None: raise ValueError( "Invalid message instance. The message must have either " "`Message.migrate_from_chat_id` or `Message.migrate_to_chat_id`." ) old_chat_id = message.migrate_from_chat_id or message.chat.id new_chat_id = message.migrate_to_chat_id or message.chat.id elif not (isinstance(old_chat_id, int) and isinstance(new_chat_id, int)): raise ValueError("old_chat_id and new_chat_id must be integers") self._chat_data[new_chat_id] = self._chat_data[old_chat_id] self.drop_chat_data(old_chat_id) self._chat_ids_to_be_updated_in_persistence.add(new_chat_id) # old_chat_id is marked for deletion by drop_chat_data above def _mark_for_persistence_update( self, *, update: Optional[object] = None, job: Optional["Job"] = None ) -> None: if isinstance(update, Update): if update.effective_chat: self._chat_ids_to_be_updated_in_persistence.add(update.effective_chat.id) if update.effective_user: self._user_ids_to_be_updated_in_persistence.add(update.effective_user.id) if job: if job.chat_id: self._chat_ids_to_be_updated_in_persistence.add(job.chat_id) if job.user_id: self._user_ids_to_be_updated_in_persistence.add(job.user_id) def mark_data_for_update_persistence( self, chat_ids: Optional[SCT[int]] = None, user_ids: Optional[SCT[int]] = None ) -> None: """Mark entries of :attr:`chat_data` and :attr:`user_data` to be updated on the next run of :meth:`update_persistence`. Tip: Use this method sparingly. If you have to use this method, it likely means that you access and modify ``context.application.chat/user_data[some_id]`` within a callback. Note that for data which should be available globally in all handler callbacks independent of the chat/user, it is recommended to use :attr:`bot_data` instead. .. versionadded:: 20.3 Args: chat_ids (:obj:`int` | Collection[:obj:`int`], optional): Chat IDs to mark. user_ids (:obj:`int` | Collection[:obj:`int`], optional): User IDs to mark. """ if chat_ids: if isinstance(chat_ids, int): self._chat_ids_to_be_updated_in_persistence.add(chat_ids) else: self._chat_ids_to_be_updated_in_persistence.update(chat_ids) if user_ids: if isinstance(user_ids, int): self._user_ids_to_be_updated_in_persistence.add(user_ids) else: self._user_ids_to_be_updated_in_persistence.update(user_ids) async def _persistence_updater(self) -> None: # Update the persistence in regular intervals. Exit only when the stop event has been set while not self.__update_persistence_event.is_set(): if not self.persistence: return # asyncio synchronization primitives don't accept a timeout argument, it is recommended # to use wait_for instead try: await asyncio.wait_for( self.__update_persistence_event.wait(), timeout=self.persistence.update_interval, ) return except asyncio.TimeoutError: pass # putting this *after* the wait_for so we don't immediately update on startup as # that would make little sense await self.update_persistence() async def update_persistence(self) -> None: """Updates :attr:`user_data`, :attr:`chat_data`, :attr:`bot_data` in :attr:`persistence` along with :attr:`~telegram.ext.ExtBot.callback_data_cache` and the conversation states of any persistent :class:`~telegram.ext.ConversationHandler` registered for this application. For :attr:`user_data` and :attr:`chat_data`, only those entries are updated which either were used or have been manually marked via :meth:`mark_data_for_update_persistence` since the last run of this method. Tip: This method will be called in regular intervals by the application. There is usually no need to call it manually. Note: Any data is deep copied with :func:`copy.deepcopy` before handing it over to the persistence in order to avoid race conditions, so all persisted data must be copyable. .. seealso:: :attr:`telegram.ext.BasePersistence.update_interval`, :meth:`mark_data_for_update_persistence` """ async with self.__update_persistence_lock: await self.__update_persistence() async def __update_persistence(self) -> None: if not self.persistence: return _LOGGER.debug("Starting next run of updating the persistence.") coroutines: Set[Coroutine] = set() # Mypy doesn't know that persistence.set_bot (see above) already checks that # self.bot is an instance of ExtBot if callback_data should be stored ... if self.persistence.store_data.callback_data and ( self.bot.callback_data_cache is not None # type: ignore[attr-defined] ): coroutines.add( self.persistence.update_callback_data( deepcopy( self.bot.callback_data_cache.persistence_data # type: ignore[attr-defined] ) ) ) if self.persistence.store_data.bot_data: coroutines.add(self.persistence.update_bot_data(deepcopy(self.bot_data))) if self.persistence.store_data.chat_data: update_ids = self._chat_ids_to_be_updated_in_persistence self._chat_ids_to_be_updated_in_persistence = set() delete_ids = self._chat_ids_to_be_deleted_in_persistence self._chat_ids_to_be_deleted_in_persistence = set() # We don't want to update any data that has been deleted! update_ids -= delete_ids for chat_id in update_ids: coroutines.add( self.persistence.update_chat_data(chat_id, deepcopy(self.chat_data[chat_id])) ) for chat_id in delete_ids: coroutines.add(self.persistence.drop_chat_data(chat_id)) if self.persistence.store_data.user_data: update_ids = self._user_ids_to_be_updated_in_persistence self._user_ids_to_be_updated_in_persistence = set() delete_ids = self._user_ids_to_be_deleted_in_persistence self._user_ids_to_be_deleted_in_persistence = set() # We don't want to update any data that has been deleted! update_ids -= delete_ids for user_id in update_ids: coroutines.add( self.persistence.update_user_data(user_id, deepcopy(self.user_data[user_id])) ) for user_id in delete_ids: coroutines.add(self.persistence.drop_user_data(user_id)) # Unfortunately due to circular imports this has to be here # pylint: disable=import-outside-toplevel from telegram.ext._handlers.conversationhandler import PendingState for name, (key, new_state) in itertools.chain.from_iterable( zip(itertools.repeat(name), states_dict.pop_accessed_write_items()) for name, states_dict in self._conversation_handler_conversations.items() ): if isinstance(new_state, PendingState): # If the handler was running non-blocking, we check if the new state is already # available. Otherwise, we update with the old state, which is the next best # guess. # Note that when updating the persistence one last time during self.stop(), # *all* tasks will be done. if not new_state.done(): if self.running: _LOGGER.debug( "A ConversationHandlers state was not yet resolved. Updating the " "persistence with the current state. Will check again on next run of " "Application.update_persistence." ) else: _LOGGER.warning( "A ConversationHandlers state was not yet resolved. Updating the " "persistence with the current state." ) result = new_state.old_state # We need to check again on the next run if the state is done self._conversation_handler_conversations[name].mark_as_accessed(key) else: result = new_state.resolve() else: result = new_state effective_new_state = None if result is TrackingDict.DELETED else result coroutines.add( self.persistence.update_conversation( name=name, key=key, new_state=effective_new_state ) ) results = await asyncio.gather(*coroutines, return_exceptions=True) _LOGGER.debug("Finished updating persistence.") # dispatch any errors await asyncio.gather( *( self.process_error(error=result, update=None) for result in results if isinstance(result, Exception) ) ) def add_error_handler( self, callback: HandlerCallback[object, CCT, None], block: DVType[bool] = DEFAULT_TRUE, ) -> None: """Registers an error handler in the Application. This handler will receive every error which happens in your bot. See the docs of :meth:`process_error` for more details on how errors are handled. Note: Attempts to add the same callback multiple times will be ignored. Examples: :any:`Errorhandler Bot ` .. seealso:: :wiki:`Exceptions, Warnings and Logging ` Args: callback (:term:`coroutine function`): The callback function for this error handler. Will be called when an error is raised. Callback signature:: async def callback(update: Optional[object], context: CallbackContext) The error that happened will be present in :attr:`telegram.ext.CallbackContext.error`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next error handler in :meth:`process_error`. Defaults to :obj:`True`. """ if callback in self.error_handlers: _LOGGER.warning("The callback is already registered as an error handler. Ignoring.") return self.error_handlers[callback] = block def remove_error_handler(self, callback: HandlerCallback[object, CCT, None]) -> None: """Removes an error handler. Args: callback (:term:`coroutine function`): The error handler to remove. """ self.error_handlers.pop(callback, None) async def process_error( self, update: Optional[object], error: Exception, job: Optional["Job[CCT]"] = None, coroutine: Optional[_ErrorCoroType[RT]] = None, ) -> bool: """Processes an error by passing it to all error handlers registered with :meth:`add_error_handler`. If one of the error handlers raises :class:`telegram.ext.ApplicationHandlerStop`, the error will not be handled by other error handlers. Raising :class:`telegram.ext.ApplicationHandlerStop` also stops processing of the update when this method is called by :meth:`process_update`, i.e. no further handlers (even in other groups) will handle the update. All other exceptions raised by an error handler will just be logged. .. versionchanged:: 20.0 * ``dispatch_error`` was renamed to :meth:`process_error`. * Exceptions raised by error handlers are now properly logged. * :class:`telegram.ext.ApplicationHandlerStop` is no longer reraised but converted into the return value. Args: update (:obj:`object` | :class:`telegram.Update`): The update that caused the error. error (:obj:`Exception`): The error that was raised. job (:class:`telegram.ext.Job`, optional): The job that caused the error. .. versionadded:: 20.0 coroutine (:term:`coroutine function`, optional): The coroutine that caused the error. Returns: :obj:`bool`: :obj:`True`, if one of the error handlers raised :class:`telegram.ext.ApplicationHandlerStop`. :obj:`False`, otherwise. """ if self.error_handlers: for ( callback, block, ) in self.error_handlers.items(): context = self.context_types.context.from_error( update=update, error=error, application=self, job=job, coroutine=coroutine, ) if not block or ( # If error handler has `block=False`, create a Task to run cb block is DEFAULT_TRUE and isinstance(self.bot, ExtBot) and self.bot.defaults and not self.bot.defaults.block ): self.__create_task( callback(update, context), update=update, is_error_handler=True, name=f"Application:{self.bot.id}:process_error:non_blocking", ) else: try: await callback(update, context) except ApplicationHandlerStop: return True except Exception as exc: _LOGGER.exception( "An error was raised and an uncaught error was raised while " "handling the error with an error_handler.", exc_info=exc, ) return False _LOGGER.exception("No error handlers are registered, logging exception.", exc_info=error) return False python-telegram-bot-21.1.1/telegram/ext/_applicationbuilder.py000066400000000000000000001612601460724040100244640ustar00rootroot00000000000000#!/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 Builder classes for the telegram.ext module.""" from asyncio import Queue from pathlib import Path from typing import ( TYPE_CHECKING, Any, Callable, Collection, Coroutine, Dict, Generic, Optional, Type, TypeVar, Union, ) import httpx from telegram._bot import Bot from telegram._utils.defaultvalue import DEFAULT_FALSE, DEFAULT_NONE, DefaultValue from telegram._utils.types import DVInput, DVType, FilePathInput, HTTPVersion, ODVInput, SocketOpt from telegram._utils.warnings import warn from telegram.ext._application import Application from telegram.ext._baseupdateprocessor import BaseUpdateProcessor, SimpleUpdateProcessor from telegram.ext._contexttypes import ContextTypes from telegram.ext._extbot import ExtBot from telegram.ext._jobqueue import JobQueue from telegram.ext._updater import Updater from telegram.ext._utils.types import BD, BT, CCT, CD, JQ, UD from telegram.request import BaseRequest from telegram.request._httpxrequest import HTTPXRequest from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import Update from telegram.ext import BasePersistence, BaseRateLimiter, CallbackContext, Defaults from telegram.ext._utils.types import RLARGS # Type hinting is a bit complicated here because we try to get to a sane level of # leveraging generics and therefore need a number of type variables. # 'In' stands for input - used in parameters of methods below # pylint: disable=invalid-name InBT = TypeVar("InBT", bound=Bot) InJQ = TypeVar("InJQ", bound=Union[None, JobQueue]) InCCT = TypeVar("InCCT", bound="CallbackContext") InUD = TypeVar("InUD") InCD = TypeVar("InCD") InBD = TypeVar("InBD") BuilderType = TypeVar("BuilderType", bound="ApplicationBuilder") _BOT_CHECKS = [ ("request", "request instance"), ("get_updates_request", "get_updates_request instance"), ("connection_pool_size", "connection_pool_size"), ("proxy", "proxy"), ("socket_options", "socket_options"), ("pool_timeout", "pool_timeout"), ("connect_timeout", "connect_timeout"), ("read_timeout", "read_timeout"), ("write_timeout", "write_timeout"), ("media_write_timeout", "media_write_timeout"), ("http_version", "http_version"), ("get_updates_connection_pool_size", "get_updates_connection_pool_size"), ("get_updates_proxy", "get_updates_proxy"), ("get_updates_socket_options", "get_updates_socket_options"), ("get_updates_pool_timeout", "get_updates_pool_timeout"), ("get_updates_connect_timeout", "get_updates_connect_timeout"), ("get_updates_read_timeout", "get_updates_read_timeout"), ("get_updates_write_timeout", "get_updates_write_timeout"), ("get_updates_http_version", "get_updates_http_version"), ("base_file_url", "base_file_url"), ("base_url", "base_url"), ("token", "token"), ("defaults", "defaults"), ("arbitrary_callback_data", "arbitrary_callback_data"), ("private_key", "private_key"), ("rate_limiter", "rate_limiter instance"), ("local_mode", "local_mode setting"), ] _TWO_ARGS_REQ = "The parameter `{}` may only be set, if no {} was set." class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]): """This class serves as initializer for :class:`telegram.ext.Application` via the so called `builder pattern`_. To build a :class:`telegram.ext.Application`, one first initializes an instance of this class. Arguments for the :class:`telegram.ext.Application` to build are then added by subsequently calling the methods of the builder. Finally, the :class:`telegram.ext.Application` is built by calling :meth:`build`. In the simplest case this can look like the following example. Example: .. code:: python application = ApplicationBuilder().token("TOKEN").build() Please see the description of the individual methods for information on which arguments can be set and what the defaults are when not called. When no default is mentioned, the argument will not be used by default. Note: * Some arguments are mutually exclusive. E.g. after calling :meth:`token`, you can't set a custom bot with :meth:`bot` and vice versa. * Unless a custom :class:`telegram.Bot` instance is set via :meth:`bot`, :meth:`build` will use :class:`telegram.ext.ExtBot` for the bot. .. seealso:: :wiki:`Your First Bot `, :wiki:`Builder Pattern ` .. _`builder pattern`: https://en.wikipedia.org/wiki/Builder_pattern """ __slots__ = ( "_application_class", "_application_kwargs", "_arbitrary_callback_data", "_base_file_url", "_base_url", "_bot", "_connect_timeout", "_connection_pool_size", "_context_types", "_defaults", "_get_updates_connect_timeout", "_get_updates_connection_pool_size", "_get_updates_http_version", "_get_updates_pool_timeout", "_get_updates_proxy", "_get_updates_read_timeout", "_get_updates_request", "_get_updates_socket_options", "_get_updates_write_timeout", "_http_version", "_job_queue", "_local_mode", "_media_write_timeout", "_persistence", "_pool_timeout", "_post_init", "_post_shutdown", "_post_stop", "_private_key", "_private_key_password", "_proxy", "_rate_limiter", "_read_timeout", "_request", "_socket_options", "_token", "_update_processor", "_update_queue", "_updater", "_write_timeout", ) def __init__(self: "InitApplicationBuilder"): self._token: DVType[str] = DefaultValue("") self._base_url: DVType[str] = DefaultValue("https://api.telegram.org/bot") self._base_file_url: DVType[str] = DefaultValue("https://api.telegram.org/file/bot") self._connection_pool_size: DVInput[int] = DEFAULT_NONE self._proxy: DVInput[Union[str, httpx.Proxy, httpx.URL]] = DEFAULT_NONE self._socket_options: DVInput[Collection[SocketOpt]] = DEFAULT_NONE self._connect_timeout: ODVInput[float] = DEFAULT_NONE self._read_timeout: ODVInput[float] = DEFAULT_NONE self._write_timeout: ODVInput[float] = DEFAULT_NONE self._media_write_timeout: ODVInput[float] = DEFAULT_NONE self._pool_timeout: ODVInput[float] = DEFAULT_NONE self._request: DVInput[BaseRequest] = DEFAULT_NONE self._get_updates_connection_pool_size: DVInput[int] = DEFAULT_NONE self._get_updates_proxy: DVInput[Union[str, httpx.Proxy, httpx.URL]] = DEFAULT_NONE self._get_updates_socket_options: DVInput[Collection[SocketOpt]] = DEFAULT_NONE self._get_updates_connect_timeout: ODVInput[float] = DEFAULT_NONE self._get_updates_read_timeout: ODVInput[float] = DEFAULT_NONE self._get_updates_write_timeout: ODVInput[float] = DEFAULT_NONE self._get_updates_pool_timeout: ODVInput[float] = DEFAULT_NONE self._get_updates_request: DVInput[BaseRequest] = DEFAULT_NONE self._get_updates_http_version: DVInput[str] = DefaultValue("1.1") self._private_key: ODVInput[bytes] = DEFAULT_NONE self._private_key_password: ODVInput[bytes] = DEFAULT_NONE self._defaults: ODVInput[Defaults] = DEFAULT_NONE self._arbitrary_callback_data: Union[DefaultValue[bool], int] = DEFAULT_FALSE self._local_mode: DVType[bool] = DEFAULT_FALSE self._bot: DVInput[Bot] = DEFAULT_NONE self._update_queue: DVType[Queue[Union[Update, object]]] = DefaultValue(Queue()) try: self._job_queue: ODVInput[JobQueue] = DefaultValue(JobQueue()) except RuntimeError as exc: if "PTB must be installed via" not in str(exc): raise exc self._job_queue = DEFAULT_NONE self._persistence: ODVInput[BasePersistence] = DEFAULT_NONE self._context_types: DVType[ContextTypes] = DefaultValue(ContextTypes()) self._application_class: DVType[Type[Application]] = DefaultValue(Application) self._application_kwargs: Dict[str, object] = {} self._update_processor: BaseUpdateProcessor = SimpleUpdateProcessor( max_concurrent_updates=1 ) self._updater: ODVInput[Updater] = DEFAULT_NONE self._post_init: Optional[Callable[[Application], Coroutine[Any, Any, None]]] = None self._post_shutdown: Optional[Callable[[Application], Coroutine[Any, Any, None]]] = None self._post_stop: Optional[Callable[[Application], Coroutine[Any, Any, None]]] = None self._rate_limiter: ODVInput[BaseRateLimiter] = DEFAULT_NONE self._http_version: DVInput[str] = DefaultValue("1.1") def _build_request(self, get_updates: bool) -> BaseRequest: prefix = "_get_updates_" if get_updates else "_" if not isinstance(getattr(self, f"{prefix}request"), DefaultValue): return getattr(self, f"{prefix}request") proxy = DefaultValue.get_value(getattr(self, f"{prefix}proxy")) socket_options = DefaultValue.get_value(getattr(self, f"{prefix}socket_options")) if get_updates: connection_pool_size = ( DefaultValue.get_value(getattr(self, f"{prefix}connection_pool_size")) or 1 ) else: connection_pool_size = ( DefaultValue.get_value(getattr(self, f"{prefix}connection_pool_size")) or 256 ) timeouts = { "connect_timeout": getattr(self, f"{prefix}connect_timeout"), "read_timeout": getattr(self, f"{prefix}read_timeout"), "write_timeout": getattr(self, f"{prefix}write_timeout"), "pool_timeout": getattr(self, f"{prefix}pool_timeout"), } if not get_updates: timeouts["media_write_timeout"] = self._media_write_timeout # Get timeouts that were actually set- effective_timeouts = { key: value for key, value in timeouts.items() if not isinstance(value, DefaultValue) } http_version = DefaultValue.get_value(getattr(self, f"{prefix}http_version")) or "1.1" return HTTPXRequest( connection_pool_size=connection_pool_size, proxy=proxy, http_version=http_version, # type: ignore[arg-type] socket_options=socket_options, **effective_timeouts, ) def _build_ext_bot(self) -> ExtBot: if isinstance(self._token, DefaultValue): raise RuntimeError("No bot token was set.") return ExtBot( token=self._token, base_url=DefaultValue.get_value(self._base_url), base_file_url=DefaultValue.get_value(self._base_file_url), private_key=DefaultValue.get_value(self._private_key), private_key_password=DefaultValue.get_value(self._private_key_password), defaults=DefaultValue.get_value(self._defaults), arbitrary_callback_data=DefaultValue.get_value(self._arbitrary_callback_data), request=self._build_request(get_updates=False), get_updates_request=self._build_request(get_updates=True), rate_limiter=DefaultValue.get_value(self._rate_limiter), local_mode=DefaultValue.get_value(self._local_mode), ) def _bot_check(self, name: str) -> None: if self._bot is not DEFAULT_NONE: raise RuntimeError(_TWO_ARGS_REQ.format(name, "bot instance")) def _updater_check(self, name: str) -> None: if self._updater not in (DEFAULT_NONE, None): raise RuntimeError(_TWO_ARGS_REQ.format(name, "updater")) def build( self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]", ) -> Application[BT, CCT, UD, CD, BD, JQ]: """Builds a :class:`telegram.ext.Application` with the provided arguments. Calls :meth:`telegram.ext.JobQueue.set_application` and :meth:`telegram.ext.BasePersistence.set_bot` if appropriate. Returns: :class:`telegram.ext.Application` """ job_queue = DefaultValue.get_value(self._job_queue) persistence = DefaultValue.get_value(self._persistence) # If user didn't set updater if isinstance(self._updater, DefaultValue) or self._updater is None: if isinstance(self._bot, DefaultValue): # and didn't set a bot bot: Bot = self._build_ext_bot() # build a bot else: bot = self._bot # now also build an updater/update_queue for them update_queue = DefaultValue.get_value(self._update_queue) if self._updater is None: updater = None else: updater = Updater(bot=bot, update_queue=update_queue) else: # if they set an updater, get all necessary attributes for Application from Updater: updater = self._updater bot = self._updater.bot update_queue = self._updater.update_queue application: Application[ BT, CCT, UD, CD, BD, JQ ] = DefaultValue.get_value( # pylint: disable=not-callable self._application_class )( bot=bot, update_queue=update_queue, updater=updater, update_processor=self._update_processor, job_queue=job_queue, persistence=persistence, context_types=DefaultValue.get_value(self._context_types), post_init=self._post_init, post_shutdown=self._post_shutdown, post_stop=self._post_stop, **self._application_kwargs, # For custom Application subclasses ) if job_queue is not None: job_queue.set_application(application) # type: ignore[arg-type] if persistence is not None: # This raises an exception if persistence.store_data.callback_data is True # but self.bot is not an instance of ExtBot - so no need to check that later on persistence.set_bot(bot) return application def application_class( self: BuilderType, application_class: Type[Application[Any, Any, Any, Any, Any, Any]], kwargs: Optional[Dict[str, object]] = None, ) -> BuilderType: """Sets a custom subclass instead of :class:`telegram.ext.Application`. The subclass's ``__init__`` should look like this .. code:: python def __init__(self, custom_arg_1, custom_arg_2, ..., **kwargs): super().__init__(**kwargs) self.custom_arg_1 = custom_arg_1 self.custom_arg_2 = custom_arg_2 Args: application_class (:obj:`type`): A subclass of :class:`telegram.ext.Application` kwargs (Dict[:obj:`str`, :obj:`object`], optional): Keyword arguments for the initialization. Defaults to an empty dict. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._application_class = application_class self._application_kwargs = kwargs or {} return self def token(self: BuilderType, token: str) -> BuilderType: """Sets the token for :attr:`telegram.ext.Application.bot`. Args: token (:obj:`str`): The token. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._bot_check("token") self._updater_check("token") self._token = token return self def base_url(self: BuilderType, base_url: str) -> BuilderType: """Sets the base URL for :attr:`telegram.ext.Application.bot`. If not called, will default to ``'https://api.telegram.org/bot'``. .. seealso:: :paramref:`telegram.Bot.base_url`, :wiki:`Local Bot API Server `, :meth:`base_file_url` Args: base_url (:obj:`str`): The URL. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._bot_check("base_url") self._updater_check("base_url") self._base_url = base_url return self def base_file_url(self: BuilderType, base_file_url: str) -> BuilderType: """Sets the base file URL for :attr:`telegram.ext.Application.bot`. If not called, will default to ``'https://api.telegram.org/file/bot'``. .. seealso:: :paramref:`telegram.Bot.base_file_url`, :wiki:`Local Bot API Server `, :meth:`base_url` Args: base_file_url (:obj:`str`): The URL. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._bot_check("base_file_url") self._updater_check("base_file_url") self._base_file_url = base_file_url return self def _request_check(self, get_updates: bool) -> None: prefix = "get_updates_" if get_updates else "" name = prefix + "request" timeouts = ["connect_timeout", "read_timeout", "write_timeout", "pool_timeout"] if not get_updates: timeouts.append("media_write_timeout") # Code below tests if it's okay to set a Request object. Only okay if no other request args # or instances containing a Request were set previously for attr in timeouts: if not isinstance(getattr(self, f"_{prefix}{attr}"), DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format(name, attr)) if not isinstance(getattr(self, f"_{prefix}connection_pool_size"), DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format(name, "connection_pool_size")) if not isinstance(getattr(self, f"_{prefix}proxy"), DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format(name, "proxy")) if not isinstance(getattr(self, f"_{prefix}socket_options"), DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format(name, "socket_options")) if not isinstance(getattr(self, f"_{prefix}http_version"), DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format(name, "http_version")) self._bot_check(name) if self._updater not in (DEFAULT_NONE, None): raise RuntimeError(_TWO_ARGS_REQ.format(name, "updater instance")) def _request_param_check(self, name: str, get_updates: bool) -> None: if get_updates and self._get_updates_request is not DEFAULT_NONE: raise RuntimeError( # disallow request args for get_updates if Request for that is set _TWO_ARGS_REQ.format(f"get_updates_{name}", "get_updates_request instance") ) if self._request is not DEFAULT_NONE: # disallow request args if request is set raise RuntimeError(_TWO_ARGS_REQ.format(name, "request instance")) if self._bot is not DEFAULT_NONE: # disallow request args if bot is set (has Request) raise RuntimeError( _TWO_ARGS_REQ.format( f"get_updates_{name}" if get_updates else name, "bot instance" ) ) if self._updater not in (DEFAULT_NONE, None): # disallow request args for updater(has bot) raise RuntimeError( _TWO_ARGS_REQ.format(f"get_updates_{name}" if get_updates else name, "updater") ) def request(self: BuilderType, request: BaseRequest) -> BuilderType: """Sets a :class:`telegram.request.BaseRequest` instance for the :paramref:`telegram.Bot.request` parameter of :attr:`telegram.ext.Application.bot`. .. seealso:: :meth:`get_updates_request` Args: request (:class:`telegram.request.BaseRequest`): The request instance. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_check(get_updates=False) self._request = request return self def connection_pool_size(self: BuilderType, connection_pool_size: int) -> BuilderType: """Sets the size of the connection pool for the :paramref:`~telegram.request.HTTPXRequest.connection_pool_size` parameter of :attr:`telegram.Bot.request`. Defaults to ``256``. .. include:: inclusions/pool_size_tip.rst .. seealso:: :meth:`get_updates_connection_pool_size` Args: connection_pool_size (:obj:`int`): The size of the connection pool. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="connection_pool_size", get_updates=False) self._connection_pool_size = connection_pool_size return self def proxy_url(self: BuilderType, proxy_url: str) -> BuilderType: """Legacy name for :meth:`proxy`, kept for backward compatibility. .. seealso:: :meth:`get_updates_proxy` .. deprecated:: 20.7 Args: proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See :paramref:`telegram.ext.ApplicationBuilder.proxy.proxy`. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ warn( "`ApplicationBuilder.proxy_url` is deprecated since version " "20.7. Use `ApplicationBuilder.proxy` instead.", PTBDeprecationWarning, stacklevel=2, ) return self.proxy(proxy_url) def proxy(self: BuilderType, proxy: Union[str, httpx.Proxy, httpx.URL]) -> BuilderType: """Sets the proxy for the :paramref:`~telegram.request.HTTPXRequest.proxy` parameter of :attr:`telegram.Bot.request`. Defaults to :obj:`None`. .. seealso:: :meth:`get_updates_proxy` .. versionadded:: 20.7 Args: proxy (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): The URL to a proxy server, a ``httpx.Proxy`` object or a ``httpx.URL`` object. See :paramref:`telegram.request.HTTPXRequest.proxy` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="proxy", get_updates=False) self._proxy = proxy return self def socket_options(self: BuilderType, socket_options: Collection[SocketOpt]) -> BuilderType: """Sets the options for the :paramref:`~telegram.request.HTTPXRequest.socket_options` parameter of :attr:`telegram.Bot.request`. Defaults to :obj:`None`. .. seealso:: :meth:`get_updates_socket_options` .. versionadded:: 20.7 Args: socket_options (Collection[:obj:`tuple`], optional): Socket options. See :paramref:`telegram.request.HTTPXRequest.socket_options` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="socket_options", get_updates=False) self._socket_options = socket_options return self def connect_timeout(self: BuilderType, connect_timeout: Optional[float]) -> BuilderType: """Sets the connection attempt timeout for the :paramref:`~telegram.request.HTTPXRequest.connect_timeout` parameter of :attr:`telegram.Bot.request`. Defaults to ``5.0``. .. seealso:: :meth:`get_updates_connect_timeout` Args: connect_timeout (:obj:`float`): See :paramref:`telegram.request.HTTPXRequest.connect_timeout` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="connect_timeout", get_updates=False) self._connect_timeout = connect_timeout return self def read_timeout(self: BuilderType, read_timeout: Optional[float]) -> BuilderType: """Sets the waiting timeout for the :paramref:`~telegram.request.HTTPXRequest.read_timeout` parameter of :attr:`telegram.Bot.request`. Defaults to ``5.0``. .. seealso:: :meth:`get_updates_read_timeout` Args: read_timeout (:obj:`float`): See :paramref:`telegram.request.HTTPXRequest.read_timeout` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="read_timeout", get_updates=False) self._read_timeout = read_timeout return self def write_timeout(self: BuilderType, write_timeout: Optional[float]) -> BuilderType: """Sets the write operation timeout for the :paramref:`~telegram.request.HTTPXRequest.write_timeout` parameter of :attr:`telegram.Bot.request`. Defaults to ``5.0``. .. seealso:: :meth:`get_updates_write_timeout` Args: write_timeout (:obj:`float`): See :paramref:`telegram.request.HTTPXRequest.write_timeout` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="write_timeout", get_updates=False) self._write_timeout = write_timeout return self def media_write_timeout( self: BuilderType, media_write_timeout: Optional[float] ) -> BuilderType: """Sets the media write operation timeout for the :paramref:`~telegram.request.HTTPXRequest.media_write_timeout` parameter of :attr:`telegram.Bot.request`. Defaults to ``20``. .. versionadded:: 21.0 Args: media_write_timeout (:obj:`float`): See :paramref:`telegram.request.HTTPXRequest.media_write_timeout` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="media_write_timeout", get_updates=False) self._media_write_timeout = media_write_timeout return self def pool_timeout(self: BuilderType, pool_timeout: Optional[float]) -> BuilderType: """Sets the connection pool's connection freeing timeout for the :paramref:`~telegram.request.HTTPXRequest.pool_timeout` parameter of :attr:`telegram.Bot.request`. Defaults to ``1.0``. .. include:: inclusions/pool_size_tip.rst .. seealso:: :meth:`get_updates_pool_timeout` Args: pool_timeout (:obj:`float`): See :paramref:`telegram.request.HTTPXRequest.pool_timeout` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="pool_timeout", get_updates=False) self._pool_timeout = pool_timeout return self def http_version(self: BuilderType, http_version: HTTPVersion) -> BuilderType: """Sets the HTTP protocol version which is used for the :paramref:`~telegram.request.HTTPXRequest.http_version` parameter of :attr:`telegram.Bot.request`. By default, HTTP/1.1 is used. .. seealso:: :meth:`get_updates_http_version` Note: Users have observed stability issues with HTTP/2, which happen due to how the `h2 library handles `_ cancellations of keepalive connections. See `#3556 `_ for a discussion. If you want to use HTTP/2, you must install PTB with the optional requirement ``http2``, i.e. .. code-block:: bash pip install "python-telegram-bot[http2]" Keep in mind that the HTTP/1.1 implementation may be considered the `"more robust option at this time" `_. .. versionadded:: 20.1 .. versionchanged:: 20.2 Reset the default version to 1.1. Args: http_version (:obj:`str`): Pass ``"2"`` or ``"2.0"`` if you'd like to use HTTP/2 for making requests to Telegram. Defaults to ``"1.1"``, in which case HTTP/1.1 is used. .. versionchanged:: 20.5 Accept ``"2"`` as a valid value. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="http_version", get_updates=False) self._http_version = http_version return self def get_updates_request(self: BuilderType, get_updates_request: BaseRequest) -> BuilderType: """Sets a :class:`telegram.request.BaseRequest` instance for the :paramref:`~telegram.Bot.get_updates_request` parameter of :attr:`telegram.ext.Application.bot`. .. seealso:: :meth:`request` Args: get_updates_request (:class:`telegram.request.BaseRequest`): The request instance. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_check(get_updates=True) self._get_updates_request = get_updates_request return self def get_updates_connection_pool_size( self: BuilderType, get_updates_connection_pool_size: int ) -> BuilderType: """Sets the size of the connection pool for the :paramref:`telegram.request.HTTPXRequest.connection_pool_size` parameter which is used for the :meth:`telegram.Bot.get_updates` request. Defaults to ``1``. .. seealso:: :meth:`connection_pool_size` Args: get_updates_connection_pool_size (:obj:`int`): The size of the connection pool. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="connection_pool_size", get_updates=True) self._get_updates_connection_pool_size = get_updates_connection_pool_size return self def get_updates_proxy_url(self: BuilderType, get_updates_proxy_url: str) -> BuilderType: """Legacy name for :meth:`get_updates_proxy`, kept for backward compatibility. .. seealso:: :meth:`proxy` .. deprecated:: 20.7 Args: get_updates_proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See :paramref:`telegram.ext.ApplicationBuilder.get_updates_proxy.get_updates_proxy`. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ warn( "`ApplicationBuilder.get_updates_proxy_url` is deprecated since version " "20.7. Use `ApplicationBuilder.get_updates_proxy` instead.", PTBDeprecationWarning, stacklevel=2, ) return self.get_updates_proxy(get_updates_proxy_url) def get_updates_proxy( self: BuilderType, get_updates_proxy: Union[str, httpx.Proxy, httpx.URL] ) -> BuilderType: """Sets the proxy for the :paramref:`telegram.request.HTTPXRequest.proxy` parameter which is used for :meth:`telegram.Bot.get_updates`. Defaults to :obj:`None`. .. seealso:: :meth:`proxy` .. versionadded:: 20.7 Args: proxy (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): The URL to a proxy server, a ``httpx.Proxy`` object or a ``httpx.URL`` object. See :paramref:`telegram.request.HTTPXRequest.proxy` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="proxy", get_updates=True) self._get_updates_proxy = get_updates_proxy return self def get_updates_socket_options( self: BuilderType, get_updates_socket_options: Collection[SocketOpt] ) -> BuilderType: """Sets the options for the :paramref:`~telegram.request.HTTPXRequest.socket_options` parameter of :paramref:`telegram.Bot.get_updates_request`. Defaults to :obj:`None`. .. seealso:: :meth:`socket_options` .. versionadded:: 20.7 Args: get_updates_socket_options (Collection[:obj:`tuple`], optional): Socket options. See :paramref:`telegram.request.HTTPXRequest.socket_options` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="socket_options", get_updates=True) self._get_updates_socket_options = get_updates_socket_options return self def get_updates_connect_timeout( self: BuilderType, get_updates_connect_timeout: Optional[float] ) -> BuilderType: """Sets the connection attempt timeout for the :paramref:`telegram.request.HTTPXRequest.connect_timeout` parameter which is used for the :meth:`telegram.Bot.get_updates` request. Defaults to ``5.0``. .. seealso:: :meth:`connect_timeout` Args: get_updates_connect_timeout (:obj:`float`): See :paramref:`telegram.request.HTTPXRequest.connect_timeout` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="connect_timeout", get_updates=True) self._get_updates_connect_timeout = get_updates_connect_timeout return self def get_updates_read_timeout( self: BuilderType, get_updates_read_timeout: Optional[float] ) -> BuilderType: """Sets the waiting timeout for the :paramref:`telegram.request.HTTPXRequest.read_timeout` parameter which is used for the :meth:`telegram.Bot.get_updates` request. Defaults to ``5.0``. .. seealso:: :meth:`read_timeout` Args: get_updates_read_timeout (:obj:`float`): See :paramref:`telegram.request.HTTPXRequest.read_timeout` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="read_timeout", get_updates=True) self._get_updates_read_timeout = get_updates_read_timeout return self def get_updates_write_timeout( self: BuilderType, get_updates_write_timeout: Optional[float] ) -> BuilderType: """Sets the write operation timeout for the :paramref:`telegram.request.HTTPXRequest.write_timeout` parameter which is used for the :meth:`telegram.Bot.get_updates` request. Defaults to ``5.0``. .. seealso:: :meth:`write_timeout` Args: get_updates_write_timeout (:obj:`float`): See :paramref:`telegram.request.HTTPXRequest.write_timeout` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="write_timeout", get_updates=True) self._get_updates_write_timeout = get_updates_write_timeout return self def get_updates_pool_timeout( self: BuilderType, get_updates_pool_timeout: Optional[float] ) -> BuilderType: """Sets the connection pool's connection freeing timeout for the :paramref:`~telegram.request.HTTPXRequest.pool_timeout` parameter which is used for the :meth:`telegram.Bot.get_updates` request. Defaults to ``1.0``. .. seealso:: :meth:`pool_timeout` Args: get_updates_pool_timeout (:obj:`float`): See :paramref:`telegram.request.HTTPXRequest.pool_timeout` for more information. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="pool_timeout", get_updates=True) self._get_updates_pool_timeout = get_updates_pool_timeout return self def get_updates_http_version( self: BuilderType, get_updates_http_version: HTTPVersion ) -> BuilderType: """Sets the HTTP protocol version which is used for the :paramref:`~telegram.request.HTTPXRequest.http_version` parameter which is used in the :meth:`telegram.Bot.get_updates` request. By default, HTTP/1.1 is used. .. seealso:: :meth:`http_version` Note: Users have observed stability issues with HTTP/2, which happen due to how the `h2 library handles `_ cancellations of keepalive connections. See `#3556 `_ for a discussion. You will also need to install the http2 dependency. Keep in mind that the HTTP/1.1 implementation may be considered the `"more robust option at this time" `_. .. code-block:: bash pip install httpx[http2] .. versionadded:: 20.1 .. versionchanged:: 20.2 Reset the default version to 1.1. Args: get_updates_http_version (:obj:`str`): Pass ``"2"`` or ``"2.0"`` if you'd like to use HTTP/2 for making requests to Telegram. Defaults to ``"1.1"``, in which case HTTP/1.1 is used. .. versionchanged:: 20.5 Accept ``"2"`` as a valid value. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._request_param_check(name="http_version", get_updates=True) self._get_updates_http_version = get_updates_http_version return self def private_key( self: BuilderType, private_key: Union[bytes, FilePathInput], password: Optional[Union[bytes, FilePathInput]] = None, ) -> BuilderType: """Sets the private key and corresponding password for decryption of telegram passport data for :attr:`telegram.ext.Application.bot`. Examples: :any:`Passport Bot ` .. seealso:: :wiki:`Telegram Passports ` Args: private_key (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`): The private key or the file path of a file that contains the key. In the latter case, the file's content will be read automatically. password (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`, optional): The corresponding password or the file path of a file that contains the password. In the latter case, the file's content will be read automatically. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._bot_check("private_key") self._updater_check("private_key") self._private_key = ( private_key if isinstance(private_key, bytes) else Path(private_key).read_bytes() ) if password is None or isinstance(password, bytes): self._private_key_password = password else: self._private_key_password = Path(password).read_bytes() return self def defaults(self: BuilderType, defaults: "Defaults") -> BuilderType: """Sets the :class:`telegram.ext.Defaults` instance for :attr:`telegram.ext.Application.bot`. .. seealso:: :wiki:`Adding Defaults to Your Bot ` Args: defaults (:class:`telegram.ext.Defaults`): The defaults instance. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._bot_check("defaults") self._updater_check("defaults") self._defaults = defaults return self def arbitrary_callback_data( self: BuilderType, arbitrary_callback_data: Union[bool, int] ) -> BuilderType: """Specifies whether :attr:`telegram.ext.Application.bot` should allow arbitrary objects as callback data for :class:`telegram.InlineKeyboardButton` and how many keyboards should be cached in memory. If not called, only strings can be used as callback data and no data will be stored in memory. Important: If you want to use this feature, you must install PTB with the optional requirement ``callback-data``, i.e. .. code-block:: bash pip install "python-telegram-bot[callback-data]" Examples: :any:`Arbitrary callback_data Bot ` .. seealso:: :wiki:`Arbitrary callback_data ` Args: arbitrary_callback_data (:obj:`bool` | :obj:`int`): If :obj:`True` is passed, the default cache size of ``1024`` will be used. Pass an integer to specify a different cache size. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._bot_check("arbitrary_callback_data") self._updater_check("arbitrary_callback_data") self._arbitrary_callback_data = arbitrary_callback_data return self def local_mode(self: BuilderType, local_mode: bool) -> BuilderType: """Specifies the value for :paramref:`~telegram.Bot.local_mode` for the :attr:`telegram.ext.Application.bot`. If not called, will default to :obj:`False`. .. seealso:: :wiki:`Local Bot API Server ` Args: local_mode (:obj:`bool`): Whether the bot should run in local mode. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._bot_check("local_mode") self._updater_check("local_mode") self._local_mode = local_mode return self def bot( self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]", bot: InBT, ) -> "ApplicationBuilder[InBT, CCT, UD, CD, BD, JQ]": """Sets a :class:`telegram.Bot` instance for :attr:`telegram.ext.Application.bot`. Instances of subclasses like :class:`telegram.ext.ExtBot` are also valid. Args: bot (:class:`telegram.Bot`): The bot. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._updater_check("bot") for attr, error in _BOT_CHECKS: if not isinstance(getattr(self, f"_{attr}"), DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format("bot", error)) self._bot = bot return self # type: ignore[return-value] def update_queue(self: BuilderType, update_queue: "Queue[object]") -> BuilderType: """Sets a :class:`asyncio.Queue` instance for :attr:`telegram.ext.Application.update_queue`, i.e. the queue that the application will fetch updates from. Will also be used for the :attr:`telegram.ext.Application.updater`. If not called, a queue will be instantiated. .. seealso:: :attr:`telegram.ext.Updater.update_queue` Args: update_queue (:class:`asyncio.Queue`): The queue. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ if self._updater not in (DEFAULT_NONE, None): raise RuntimeError(_TWO_ARGS_REQ.format("update_queue", "updater instance")) self._update_queue = update_queue return self def concurrent_updates( self: BuilderType, concurrent_updates: Union[bool, int, "BaseUpdateProcessor"] ) -> BuilderType: """Specifies if and how many updates may be processed concurrently instead of one by one. If not called, updates will be processed one by one. Warning: Processing updates concurrently is not recommended when stateful handlers like :class:`telegram.ext.ConversationHandler` are used. Only use this if you are sure that your bot does not (explicitly or implicitly) rely on updates being processed sequentially. .. include:: inclusions/pool_size_tip.rst .. seealso:: :attr:`telegram.ext.Application.concurrent_updates` Args: concurrent_updates (:obj:`bool` | :obj:`int` | :class:`BaseUpdateProcessor`): Passing :obj:`True` will allow for ``256`` updates to be processed concurrently using :class:`telegram.ext.SimpleUpdateProcessor`. Pass an integer to specify a different number of updates that may be processed concurrently. Pass an instance of :class:`telegram.ext.BaseUpdateProcessor` to use that instance for handling updates concurrently. .. versionchanged:: 20.4 Now accepts :class:`BaseUpdateProcessor` instances. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ # Check if concurrent updates is bool and convert to integer if concurrent_updates is True: concurrent_updates = 256 elif concurrent_updates is False: concurrent_updates = 1 # If `concurrent_updates` is an integer, create a `SimpleUpdateProcessor` # instance with that integer value; otherwise, raise an error if the value # is negative if isinstance(concurrent_updates, int): concurrent_updates = SimpleUpdateProcessor(concurrent_updates) # Assign default value of concurrent_updates if it is instance of # `BaseUpdateProcessor` self._update_processor: BaseUpdateProcessor = concurrent_updates # type: ignore[no-redef] return self def job_queue( self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]", job_queue: InJQ, ) -> "ApplicationBuilder[BT, CCT, UD, CD, BD, InJQ]": """Sets a :class:`telegram.ext.JobQueue` instance for :attr:`telegram.ext.Application.job_queue`. If not called, a job queue will be instantiated if the requirements of :class:`telegram.ext.JobQueue` are installed. Examples: :any:`Timer Bot ` .. seealso:: :wiki:`Job Queue ` Note: * :meth:`telegram.ext.JobQueue.set_application` will be called automatically by :meth:`build`. * The job queue will be automatically started and stopped by :meth:`telegram.ext.Application.start` and :meth:`telegram.ext.Application.stop`, respectively. * When passing :obj:`None` or when the requirements of :class:`telegram.ext.JobQueue` are not installed, :attr:`telegram.ext.ConversationHandler.conversation_timeout` can not be used, as this uses :attr:`telegram.ext.Application.job_queue` internally. Args: job_queue (:class:`telegram.ext.JobQueue`): The job queue. Pass :obj:`None` if you don't want to use a job queue. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._job_queue = job_queue return self # type: ignore[return-value] def persistence( self: BuilderType, persistence: "BasePersistence[Any, Any, Any]" ) -> BuilderType: """Sets a :class:`telegram.ext.BasePersistence` instance for :attr:`telegram.ext.Application.persistence`. Note: When using a persistence, note that all data stored in :attr:`context.user_data `, :attr:`context.chat_data `, :attr:`context.bot_data ` and in :attr:`telegram.ext.ExtBot.callback_data_cache` must be copyable with :func:`copy.deepcopy`. This is due to the data being deep copied before handing it over to the persistence in order to avoid race conditions. Examples: :any:`Persistent Conversation Bot ` .. seealso:: :wiki:`Making Your Bot Persistent ` Warning: If a :class:`telegram.ext.ContextTypes` instance is set via :meth:`context_types`, the persistence instance must use the same types! Args: persistence (:class:`telegram.ext.BasePersistence`): The persistence instance. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._persistence = persistence return self def context_types( self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]", context_types: "ContextTypes[InCCT, InUD, InCD, InBD]", ) -> "ApplicationBuilder[BT, InCCT, InUD, InCD, InBD, JQ]": """Sets a :class:`telegram.ext.ContextTypes` instance for :attr:`telegram.ext.Application.context_types`. Examples: :any:`Context Types Bot ` Args: context_types (:class:`telegram.ext.ContextTypes`): The context types. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._context_types = context_types return self # type: ignore[return-value] def updater(self: BuilderType, updater: Optional[Updater]) -> BuilderType: """Sets a :class:`telegram.ext.Updater` instance for :attr:`telegram.ext.Application.updater`. The :attr:`telegram.ext.Updater.bot` and :attr:`telegram.ext.Updater.update_queue` will be used for :attr:`telegram.ext.Application.bot` and :attr:`telegram.ext.Application.update_queue`, respectively. Args: updater (:class:`telegram.ext.Updater` | :obj:`None`): The updater instance or :obj:`None` if no updater should be used. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ if updater is None: self._updater = updater return self for attr, error in ( (self._bot, "bot instance"), (self._update_queue, "update_queue"), ): if not isinstance(attr, DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format("updater", error)) for attr_name, error in _BOT_CHECKS: if not isinstance(getattr(self, f"_{attr_name}"), DefaultValue): raise RuntimeError(_TWO_ARGS_REQ.format("updater", error)) self._updater = updater return self def post_init( self: BuilderType, post_init: Callable[[Application], Coroutine[Any, Any, None]] ) -> BuilderType: """ Sets a callback to be executed by :meth:`Application.run_polling` and :meth:`Application.run_webhook` *after* executing :meth:`Application.initialize` but *before* executing :meth:`Updater.start_polling` or :meth:`Updater.start_webhook`, respectively. Tip: This can be used for custom startup logic that requires to await coroutines, e.g. setting up the bots commands via :meth:`~telegram.Bot.set_my_commands`. Example: .. code:: async def post_init(application: Application) -> None: await application.bot.set_my_commands([('start', 'Starts the bot')]) application = Application.builder().token("TOKEN").post_init(post_init).build() Note: |post_methods_note| .. seealso:: :meth:`post_stop`, :meth:`post_shutdown` Args: post_init (:term:`coroutine function`): The custom callback. Must be a :term:`coroutine function` and must accept exactly one positional argument, which is the :class:`~telegram.ext.Application`:: async def post_init(application: Application) -> None: Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._post_init = post_init return self def post_shutdown( self: BuilderType, post_shutdown: Callable[[Application], Coroutine[Any, Any, None]] ) -> BuilderType: """ Sets a callback to be executed by :meth:`Application.run_polling` and :meth:`Application.run_webhook` *after* executing :meth:`Updater.shutdown` and :meth:`Application.shutdown`. Tip: This can be used for custom shutdown logic that requires to await coroutines, e.g. closing a database connection Example: .. code:: async def post_shutdown(application: Application) -> None: await application.bot_data['database'].close() application = Application.builder() .token("TOKEN") .post_shutdown(post_shutdown) .build() Note: |post_methods_note| .. seealso:: :meth:`post_init`, :meth:`post_stop` Args: post_shutdown (:term:`coroutine function`): The custom callback. Must be a :term:`coroutine function` and must accept exactly one positional argument, which is the :class:`~telegram.ext.Application`:: async def post_shutdown(application: Application) -> None: Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._post_shutdown = post_shutdown return self def post_stop( self: BuilderType, post_stop: Callable[[Application], Coroutine[Any, Any, None]] ) -> BuilderType: """ Sets a callback to be executed by :meth:`Application.run_polling` and :meth:`Application.run_webhook` *after* executing :meth:`Updater.stop` and :meth:`Application.stop`. .. versionadded:: 20.1 Tip: This can be used for custom stop logic that requires to await coroutines, e.g. sending message to a chat before shutting down the bot Example: .. code:: async def post_stop(application: Application) -> None: await application.bot.send_message(123456, "Shutting down...") application = Application.builder() .token("TOKEN") .post_stop(post_stop) .build() Note: |post_methods_note| .. seealso:: :meth:`post_init`, :meth:`post_shutdown` Args: post_stop (:term:`coroutine function`): The custom callback. Must be a :term:`coroutine function` and must accept exactly one positional argument, which is the :class:`~telegram.ext.Application`:: async def post_stop(application: Application) -> None: Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._post_stop = post_stop return self def rate_limiter( self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]", rate_limiter: "BaseRateLimiter[RLARGS]", ) -> "ApplicationBuilder[ExtBot[RLARGS], CCT, UD, CD, BD, JQ]": """Sets a :class:`telegram.ext.BaseRateLimiter` instance for the :paramref:`telegram.ext.ExtBot.rate_limiter` parameter of :attr:`telegram.ext.Application.bot`. Args: rate_limiter (:class:`telegram.ext.BaseRateLimiter`): The rate limiter. Returns: :class:`ApplicationBuilder`: The same builder with the updated argument. """ self._bot_check("rate_limiter") self._updater_check("rate_limiter") self._rate_limiter = rate_limiter return self # type: ignore[return-value] InitApplicationBuilder = ( # This is defined all the way down here so that its type is inferred ApplicationBuilder[ # by Pylance correctly. ExtBot[None], ContextTypes.DEFAULT_TYPE, Dict[Any, Any], Dict[Any, Any], Dict[Any, Any], JobQueue[ContextTypes.DEFAULT_TYPE], ] ) python-telegram-bot-21.1.1/telegram/ext/_basepersistence.py000066400000000000000000000427771460724040100240040ustar00rootroot00000000000000#!/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 BasePersistence class.""" from abc import ABC, abstractmethod from typing import Dict, Generic, NamedTuple, NoReturn, Optional from telegram._bot import Bot from telegram.ext._extbot import ExtBot from telegram.ext._utils.types import BD, CD, UD, CDCData, ConversationDict, ConversationKey class PersistenceInput(NamedTuple): """Convenience wrapper to group boolean input for the :paramref:`~BasePersistence.store_data` parameter for :class:`BasePersistence`. Args: bot_data (:obj:`bool`, optional): Whether the setting should be applied for ``bot_data``. Defaults to :obj:`True`. chat_data (:obj:`bool`, optional): Whether the setting should be applied for ``chat_data``. Defaults to :obj:`True`. user_data (:obj:`bool`, optional): Whether the setting should be applied for ``user_data``. Defaults to :obj:`True`. callback_data (:obj:`bool`, optional): Whether the setting should be applied for ``callback_data``. Defaults to :obj:`True`. Attributes: bot_data (:obj:`bool`): Whether the setting should be applied for ``bot_data``. chat_data (:obj:`bool`): Whether the setting should be applied for ``chat_data``. user_data (:obj:`bool`): Whether the setting should be applied for ``user_data``. callback_data (:obj:`bool`): Whether the setting should be applied for ``callback_data``. """ bot_data: bool = True chat_data: bool = True user_data: bool = True callback_data: bool = True class BasePersistence(Generic[UD, CD, BD], ABC): """Interface class for adding persistence to your bot. Subclass this object for different implementations of a persistent bot. Attention: The interface provided by this class is intended to be accessed exclusively by :class:`~telegram.ext.Application`. Calling any of the methods below manually might interfere with the integration of persistence into :class:`~telegram.ext.Application`. All relevant methods must be overwritten. This includes: * :meth:`get_bot_data` * :meth:`update_bot_data` * :meth:`refresh_bot_data` * :meth:`get_chat_data` * :meth:`update_chat_data` * :meth:`refresh_chat_data` * :meth:`drop_chat_data` * :meth:`get_user_data` * :meth:`update_user_data` * :meth:`refresh_user_data` * :meth:`drop_user_data` * :meth:`get_callback_data` * :meth:`update_callback_data` * :meth:`get_conversations` * :meth:`update_conversation` * :meth:`flush` If you don't actually need one of those methods, a simple :keyword:`pass` is enough. For example, if you don't store ``bot_data``, you don't need :meth:`get_bot_data`, :meth:`update_bot_data` or :meth:`refresh_bot_data`. Note: You should avoid saving :class:`telegram.Bot` instances. This is because if you change e.g. the bots token, this won't propagate to the serialized instances and may lead to exceptions. To prevent this, the implementation may use :attr:`bot` to replace bot instances with a placeholder before serialization and insert :attr:`bot` back when loading the data. Since :attr:`bot` will be set when the process starts, this will be the up-to-date bot instance. If the persistence implementation does not take care of this, you should make sure not to store any bot instances in the data that will be persisted. E.g. in case of :class:`telegram.TelegramObject`, one may call :meth:`set_bot` to ensure that shortcuts like :meth:`telegram.Message.reply_text` are available. This class is a :class:`~typing.Generic` class and accepts three type variables: 1. The type of the second argument of :meth:`update_user_data`, which must coincide with the type of the second argument of :meth:`refresh_user_data` and the values in the dictionary returned by :meth:`get_user_data`. 2. The type of the second argument of :meth:`update_chat_data`, which must coincide with the type of the second argument of :meth:`refresh_chat_data` and the values in the dictionary returned by :meth:`get_chat_data`. 3. The type of the argument of :meth:`update_bot_data`, which must coincide with the type of the argument of :meth:`refresh_bot_data` and the return value of :meth:`get_bot_data`. .. seealso:: :wiki:`Architecture Overview `, :wiki:`Making Your Bot Persistent ` .. versionchanged:: 20.0 * The parameters and attributes ``store_*_data`` were replaced by :attr:`store_data`. * ``insert/replace_bot`` was dropped. Serialization of bot instances now needs to be handled by the specific implementation - see above note. Args: store_data (:class:`~telegram.ext.PersistenceInput`, optional): Specifies which kinds of data will be saved by this persistence instance. By default, all available kinds of data will be saved. update_interval (:obj:`int` | :obj:`float`, optional): The :class:`~telegram.ext.Application` will update the persistence in regular intervals. This parameter specifies the time (in seconds) to wait between two consecutive runs of updating the persistence. Defaults to ``60`` seconds. .. versionadded:: 20.0 Attributes: store_data (:class:`~telegram.ext.PersistenceInput`): Specifies which kinds of data will be saved by this persistence instance. bot (:class:`telegram.Bot`): The bot associated with the persistence. """ __slots__ = ( "_update_interval", "bot", "store_data", ) def __init__( self, store_data: Optional[PersistenceInput] = None, update_interval: float = 60, ): self.store_data: PersistenceInput = store_data or PersistenceInput() self._update_interval: float = update_interval self.bot: Bot = None # type: ignore[assignment] @property def update_interval(self) -> float: """:obj:`float`: Time (in seconds) that the :class:`~telegram.ext.Application` will wait between two consecutive runs of updating the persistence. .. versionadded:: 20.0 """ return self._update_interval @update_interval.setter def update_interval(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to update_interval after initialization." ) def set_bot(self, bot: Bot) -> None: """Set the Bot to be used by this persistence instance. Args: bot (:class:`telegram.Bot`): The bot. Raises: :exc:`TypeError`: If :attr:`PersistenceInput.callback_data` is :obj:`True` and the :paramref:`bot` is not an instance of :class:`telegram.ext.ExtBot`. """ if self.store_data.callback_data and (not isinstance(bot, ExtBot)): raise TypeError("callback_data can only be stored when using telegram.ext.ExtBot.") self.bot = bot @abstractmethod async def get_user_data(self) -> Dict[int, UD]: """Will be called by :class:`telegram.ext.Application` upon creation with a persistence object. It should return the ``user_data`` if stored, or an empty :obj:`dict`. In the latter case, the dictionary should produce values corresponding to one of the following: - :obj:`dict` - The type from :attr:`telegram.ext.ContextTypes.user_data` if :class:`telegram.ext.ContextTypes` is used. .. versionchanged:: 20.0 This method may now return a :obj:`dict` instead of a :obj:`collections.defaultdict` Returns: Dict[:obj:`int`, :obj:`dict` | :attr:`telegram.ext.ContextTypes.user_data`]: The restored user data. """ @abstractmethod async def get_chat_data(self) -> Dict[int, CD]: """Will be called by :class:`telegram.ext.Application` upon creation with a persistence object. It should return the ``chat_data`` if stored, or an empty :obj:`dict`. In the latter case, the dictionary should produce values corresponding to one of the following: - :obj:`dict` - The type from :attr:`telegram.ext.ContextTypes.chat_data` if :class:`telegram.ext.ContextTypes` is used. .. versionchanged:: 20.0 This method may now return a :obj:`dict` instead of a :obj:`collections.defaultdict` Returns: Dict[:obj:`int`, :obj:`dict` | :attr:`telegram.ext.ContextTypes.chat_data`]: The restored chat data. """ @abstractmethod async def get_bot_data(self) -> BD: """Will be called by :class:`telegram.ext.Application` upon creation with a persistence object. It should return the ``bot_data`` if stored, or an empty :obj:`dict`. In the latter case, the :obj:`dict` should produce values corresponding to one of the following: - :obj:`dict` - The type from :attr:`telegram.ext.ContextTypes.bot_data` if :class:`telegram.ext.ContextTypes` are used. Returns: Dict[:obj:`int`, :obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`]: The restored bot data. """ @abstractmethod async def get_callback_data(self) -> Optional[CDCData]: """Will be called by :class:`telegram.ext.Application` upon creation with a persistence object. If callback data was stored, it should be returned. .. versionadded:: 13.6 .. versionchanged:: 20.0 Changed this method into an :external:func:`~abc.abstractmethod`. Returns: Tuple[List[Tuple[:obj:`str`, :obj:`float`, Dict[:obj:`str`, :class:`object`]]], Dict[:obj:`str`, :obj:`str`]] | :obj:`None`: The restored metadata or :obj:`None`, if no data was stored. """ @abstractmethod async def get_conversations(self, name: str) -> ConversationDict: """Will be called by :class:`telegram.ext.Application` when a :class:`telegram.ext.ConversationHandler` is added if :attr:`telegram.ext.ConversationHandler.persistent` is :obj:`True`. It should return the conversations for the handler with :paramref:`name` or an empty :obj:`dict`. Args: name (:obj:`str`): The handlers name. Returns: :obj:`dict`: The restored conversations for the handler. """ @abstractmethod async def update_conversation( self, name: str, key: ConversationKey, new_state: Optional[object] ) -> None: """Will be called when a :class:`telegram.ext.ConversationHandler` changes states. This allows the storage of the new state in the persistence. Args: name (:obj:`str`): The handler's name. key (:obj:`tuple`): The key the state is changed for. new_state (:class:`object`): The new state for the given key. """ @abstractmethod async def update_user_data(self, user_id: int, data: UD) -> None: """Will be called by the :class:`telegram.ext.Application` after a handler has handled an update. Args: user_id (:obj:`int`): The user the data might have been changed for. data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.user_data`): The :attr:`telegram.ext.Application.user_data` ``[user_id]``. """ @abstractmethod async def update_chat_data(self, chat_id: int, data: CD) -> None: """Will be called by the :class:`telegram.ext.Application` after a handler has handled an update. Args: chat_id (:obj:`int`): The chat the data might have been changed for. data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.chat_data`): The :attr:`telegram.ext.Application.chat_data` ``[chat_id]``. """ @abstractmethod async def update_bot_data(self, data: BD) -> None: """Will be called by the :class:`telegram.ext.Application` after a handler has handled an update. Args: data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`): The :attr:`telegram.ext.Application.bot_data`. """ @abstractmethod async def update_callback_data(self, data: CDCData) -> None: """Will be called by the :class:`telegram.ext.Application` after a handler has handled an update. .. versionadded:: 13.6 .. versionchanged:: 20.0 Changed this method into an :external:func:`~abc.abstractmethod`. Args: data (Tuple[List[Tuple[:obj:`str`, :obj:`float`, \ Dict[:obj:`str`, :obj:`Any`]]], Dict[:obj:`str`, :obj:`str`]] | :obj:`None`): The relevant data to restore :class:`telegram.ext.CallbackDataCache`. """ @abstractmethod async def drop_chat_data(self, chat_id: int) -> None: """Will be called by the :class:`telegram.ext.Application`, when using :meth:`~telegram.ext.Application.drop_chat_data`. .. versionadded:: 20.0 Args: chat_id (:obj:`int`): The chat id to delete from the persistence. """ @abstractmethod async def drop_user_data(self, user_id: int) -> None: """Will be called by the :class:`telegram.ext.Application`, when using :meth:`~telegram.ext.Application.drop_user_data`. .. versionadded:: 20.0 Args: user_id (:obj:`int`): The user id to delete from the persistence. """ @abstractmethod async def refresh_user_data(self, user_id: int, user_data: UD) -> None: """Will be called by the :class:`telegram.ext.Application` before passing the :attr:`~telegram.ext.Application.user_data` to a callback. Can be used to update data stored in :attr:`~telegram.ext.Application.user_data` from an external source. Warning: When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method may be called while a handler callback is still running. This might lead to race conditions. .. versionadded:: 13.6 .. versionchanged:: 20.0 Changed this method into an :external:func:`~abc.abstractmethod`. Args: user_id (:obj:`int`): The user ID this :attr:`~telegram.ext.Application.user_data` is associated with. user_data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.user_data`): The ``user_data`` of a single user. """ @abstractmethod async def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: """Will be called by the :class:`telegram.ext.Application` before passing the :attr:`~telegram.ext.Application.chat_data` to a callback. Can be used to update data stored in :attr:`~telegram.ext.Application.chat_data` from an external source. Warning: When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method may be called while a handler callback is still running. This might lead to race conditions. .. versionadded:: 13.6 .. versionchanged:: 20.0 Changed this method into an :external:func:`~abc.abstractmethod`. Args: chat_id (:obj:`int`): The chat ID this :attr:`~telegram.ext.Application.chat_data` is associated with. chat_data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.chat_data`): The ``chat_data`` of a single chat. """ @abstractmethod async def refresh_bot_data(self, bot_data: BD) -> None: """Will be called by the :class:`telegram.ext.Application` before passing the :attr:`~telegram.ext.Application.bot_data` to a callback. Can be used to update data stored in :attr:`~telegram.ext.Application.bot_data` from an external source. Warning: When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method may be called while a handler callback is still running. This might lead to race conditions. .. versionadded:: 13.6 .. versionchanged:: 20.0 Changed this method into an :external:func:`~abc.abstractmethod`. Args: bot_data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`): The ``bot_data``. """ @abstractmethod async def flush(self) -> None: """Will be called by :meth:`telegram.ext.Application.stop`. Gives the persistence a chance to finish up saving or close a database connection gracefully. .. versionchanged:: 20.0 Changed this method into an :external:func:`~abc.abstractmethod`. """ python-telegram-bot-21.1.1/telegram/ext/_baseratelimiter.py000066400000000000000000000145211460724040100237630ustar00rootroot00000000000000#!/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 allows to rate limit requests to the Bot API.""" from abc import ABC, abstractmethod from typing import Any, Callable, Coroutine, Dict, Generic, List, Optional, Union from telegram._utils.types import JSONDict from telegram.ext._utils.types import RLARGS class BaseRateLimiter(ABC, Generic[RLARGS]): """ Abstract interface class that allows to rate limit the requests that python-telegram-bot sends to the Telegram Bot API. An implementation of this class must implement all abstract methods and properties. This class is a :class:`~typing.Generic` class and accepts one type variable that specifies the type of the argument :paramref:`~process_request.rate_limit_args` of :meth:`process_request` and the methods of :class:`~telegram.ext.ExtBot`. Hint: Requests to :meth:`~telegram.Bot.get_updates` are never rate limited. .. seealso:: :wiki:`Architecture Overview `, :wiki:`Avoiding Flood Limits ` .. versionadded:: 20.0 """ __slots__ = () @abstractmethod async def initialize(self) -> None: """Initialize resources used by this class. Must be implemented by a subclass.""" @abstractmethod async def shutdown(self) -> None: """Stop & clear resources used by this class. Must be implemented by a subclass.""" @abstractmethod async def process_request( self, callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, List[JSONDict]]]], args: Any, kwargs: Dict[str, Any], endpoint: str, data: Dict[str, Any], rate_limit_args: Optional[RLARGS], ) -> Union[bool, JSONDict, List[JSONDict]]: """ Process a request. Must be implemented by a subclass. This method must call :paramref:`callback` and return the result of the call. `When` the callback is called is up to the implementation. Important: This method must only return once the result of :paramref:`callback` is known! If a :exc:`~telegram.error.RetryAfter` error is raised, this method may try to make a new request by calling the callback again. Warning: This method *should not* handle any other exception raised by :paramref:`callback`! There are basically two different approaches how a rate limiter can be implemented: 1. React only if necessary. In this case, the :paramref:`callback` is called without any precautions. If a :exc:`~telegram.error.RetryAfter` error is raised, processing requests is halted for the :attr:`~telegram.error.RetryAfter.retry_after` and finally the :paramref:`callback` is called again. This approach is often amendable for bots that don't have a large user base and/or don't send more messages than they get updates. 2. Throttle all outgoing requests. In this case the implementation makes sure that the requests are spread out over a longer time interval in order to stay below the rate limits. This approach is often amendable for bots that have a large user base and/or send more messages than they get updates. An implementation can use the information provided by :paramref:`data`, :paramref:`endpoint` and :paramref:`rate_limit_args` to handle each request differently. Examples: * It is usually desirable to call :meth:`telegram.Bot.answer_inline_query` as quickly as possible, while delaying :meth:`telegram.Bot.send_message` is acceptable. * There are `different `_ rate limits for group chats and private chats. * When sending broadcast messages to a large number of users, these requests can typically be delayed for a longer time than messages that are direct replies to a user input. Args: callback (Callable[..., :term:`coroutine`]): The coroutine function that must be called to make the request. args (Tuple[:obj:`object`]): The positional arguments for the :paramref:`callback` function. kwargs (Dict[:obj:`str`, :obj:`object`]): The keyword arguments for the :paramref:`callback` function. endpoint (:obj:`str`): The endpoint that the request is made for, e.g. ``"sendMessage"``. data (Dict[:obj:`str`, :obj:`object`]): The parameters that were passed to the method of :class:`~telegram.ext.ExtBot`. Any ``api_kwargs`` are included in this and any :paramref:`~telegram.ext.ExtBot.defaults` are already applied. Example: When calling:: await ext_bot.send_message( chat_id=1, text="Hello world!", api_kwargs={"custom": "arg"} ) then :paramref:`data` will be:: {"chat_id": 1, "text": "Hello world!", "custom": "arg"} rate_limit_args (:obj:`None` | :class:`object`): Custom arguments passed to the methods of :class:`~telegram.ext.ExtBot`. Can e.g. be used to specify the priority of the request. Returns: :obj:`bool` | Dict[:obj:`str`, :obj:`object`] | :obj:`None`: The result of the callback function. """ python-telegram-bot-21.1.1/telegram/ext/_baseupdateprocessor.py000066400000000000000000000136731460724040100246730ustar00rootroot00000000000000#!/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 BaseProcessor class.""" from abc import ABC, abstractmethod from asyncio import BoundedSemaphore from types import TracebackType from typing import Any, AsyncContextManager, Awaitable, Optional, Type, TypeVar, final _BUPT = TypeVar("_BUPT", bound="BaseUpdateProcessor") class BaseUpdateProcessor(AsyncContextManager["BaseUpdateProcessor"], ABC): """An abstract base class for update processors. You can use this class to implement your own update processor. Instances of this class can be used as asyncio context managers, where .. code:: python async with processor: # code is roughly equivalent to .. code:: python try: await processor.initialize() # code finally: await processor.shutdown() .. seealso:: :meth:`__aenter__` and :meth:`__aexit__`. .. seealso:: :wiki:`Concurrency` .. versionadded:: 20.4 Args: max_concurrent_updates (:obj:`int`): The maximum number of updates to be processed concurrently. If this number is exceeded, new updates will be queued until the number of currently processed updates decreases. Raises: :exc:`ValueError`: If :paramref:`max_concurrent_updates` is a non-positive integer. """ __slots__ = ("_max_concurrent_updates", "_semaphore") def __init__(self, max_concurrent_updates: int): self._max_concurrent_updates = max_concurrent_updates if self.max_concurrent_updates < 1: raise ValueError("`max_concurrent_updates` must be a positive integer!") self._semaphore = BoundedSemaphore(self.max_concurrent_updates) async def __aenter__(self: _BUPT) -> _BUPT: # noqa: PYI019 """|async_context_manager| :meth:`initializes ` the Processor. Returns: The initialized Processor instance. Raises: :exc:`Exception`: If an exception is raised during initialization, :meth:`shutdown` is called in this case. """ try: await self.initialize() return self except Exception as exc: await self.shutdown() raise exc 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 Processor.""" await self.shutdown() @property def max_concurrent_updates(self) -> int: """:obj:`int`: The maximum number of updates that can be processed concurrently.""" return self._max_concurrent_updates @abstractmethod async def do_process_update( self, update: object, coroutine: "Awaitable[Any]", ) -> None: """Custom implementation of how to process an update. Must be implemented by a subclass. Warning: This method will be called by :meth:`process_update`. It should *not* be called manually. Args: update (:obj:`object`): The update to be processed. coroutine (:term:`Awaitable`): The coroutine that will be awaited to process the update. """ @abstractmethod async def initialize(self) -> None: """Initializes the processor so resources can be allocated. Must be implemented by a subclass. .. seealso:: :meth:`shutdown` """ @abstractmethod async def shutdown(self) -> None: """Shutdown the processor so resources can be freed. Must be implemented by a subclass. .. seealso:: :meth:`initialize` """ @final async def process_update( self, update: object, coroutine: "Awaitable[Any]", ) -> None: """Calls :meth:`do_process_update` with a semaphore to limit the number of concurrent updates. Args: update (:obj:`object`): The update to be processed. coroutine (:term:`Awaitable`): The coroutine that will be awaited to process the update. """ async with self._semaphore: await self.do_process_update(update, coroutine) class SimpleUpdateProcessor(BaseUpdateProcessor): """Instance of :class:`telegram.ext.BaseUpdateProcessor` that immediately awaits the coroutine, i.e. does not apply any additional processing. This is used by default when :attr:`telegram.ext.ApplicationBuilder.concurrent_updates` is :obj:`int`. .. versionadded:: 20.4 """ __slots__ = () async def do_process_update( self, update: object, coroutine: "Awaitable[Any]", ) -> None: """Immediately awaits the coroutine, i.e. does not apply any additional processing. Args: update (:obj:`object`): The update to be processed. coroutine (:term:`Awaitable`): The coroutine that will be awaited to process the update. """ await coroutine async def initialize(self) -> None: """Does nothing.""" async def shutdown(self) -> None: """Does nothing.""" python-telegram-bot-21.1.1/telegram/ext/_callbackcontext.py000066400000000000000000000424771460724040100237630ustar00rootroot00000000000000#!/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 CallbackContext class.""" from typing import ( TYPE_CHECKING, Any, Awaitable, Dict, Generator, Generic, List, Match, NoReturn, Optional, Type, Union, ) from telegram._callbackquery import CallbackQuery from telegram._update import Update from telegram._utils.warnings import warn from telegram.ext._extbot import ExtBot from telegram.ext._utils.types import BD, BT, CD, UD if TYPE_CHECKING: from asyncio import Future, Queue from telegram.ext import Application, Job, JobQueue from telegram.ext._utils.types import CCT _STORING_DATA_WIKI = ( "https://github.com/python-telegram-bot/python-telegram-bot" "/wiki/Storing-bot%2C-user-and-chat-related-data" ) class CallbackContext(Generic[BT, UD, CD, BD]): """ This is a context object passed to the callback called by :class:`telegram.ext.BaseHandler` or by the :class:`telegram.ext.Application` in an error handler added by :attr:`telegram.ext.Application.add_error_handler` or to the callback of a :class:`telegram.ext.Job`. Note: :class:`telegram.ext.Application` will create a single context for an entire update. This means that if you got 2 handlers in different groups and they both get called, they will receive the same :class:`CallbackContext` object (of course with proper attributes like :attr:`matches` differing). This allows you to add custom attributes in a lower handler group callback, and then subsequently access those attributes in a higher handler group callback. Note that the attributes on :class:`CallbackContext` might change in the future, so make sure to use a fairly unique name for the attributes. Warning: Do not combine custom attributes with :paramref:`telegram.ext.BaseHandler.block` set to :obj:`False` or :attr:`telegram.ext.Application.concurrent_updates` set to :obj:`True`. Due to how those work, it will almost certainly execute the callbacks for an update out of order, and the attributes that you think you added will not be present. This class is a :class:`~typing.Generic` class and accepts four type variables: 1. The type of :attr:`bot`. Must be :class:`telegram.Bot` or a subclass of that class. 2. The type of :attr:`user_data` (if :attr:`user_data` is not :obj:`None`). 3. The type of :attr:`chat_data` (if :attr:`chat_data` is not :obj:`None`). 4. The type of :attr:`bot_data` (if :attr:`bot_data` is not :obj:`None`). Examples: * :any:`Context Types Bot ` * :any:`Custom Webhook Bot ` .. seealso:: :attr:`telegram.ext.ContextTypes.DEFAULT_TYPE`, :wiki:`Job Queue ` Args: application (:class:`telegram.ext.Application`): The application associated with this context. chat_id (:obj:`int`, optional): The ID of the chat associated with this object. Used to provide :attr:`chat_data`. .. versionadded:: 20.0 user_id (:obj:`int`, optional): The ID of the user associated with this object. Used to provide :attr:`user_data`. .. versionadded:: 20.0 Attributes: coroutine (:term:`awaitable`): Optional. Only present in error handlers if the error was caused by an awaitable run with :meth:`Application.create_task` or a handler callback with :attr:`block=False `. matches (List[:meth:`re.Match `]): Optional. If the associated update originated from a :class:`filters.Regex`, this will contain a list of match objects for every pattern where ``re.search(pattern, string)`` returned a match. Note that filters short circuit, so combined regex filters will not always be evaluated. args (List[:obj:`str`]): Optional. Arguments passed to a command if the associated update is handled by :class:`telegram.ext.CommandHandler`, :class:`telegram.ext.PrefixHandler` or :class:`telegram.ext.StringCommandHandler`. It contains a list of the words in the text after the command, using any whitespace string as a delimiter. error (:exc:`Exception`): Optional. The error that was raised. Only present when passed to an error handler registered with :attr:`telegram.ext.Application.add_error_handler`. job (:class:`telegram.ext.Job`): Optional. The job which originated this callback. Only present when passed to the callback of :class:`telegram.ext.Job` or in error handlers if the error is caused by a job. .. versionchanged:: 20.0 :attr:`job` is now also present in error handlers if the error is caused by a job. """ __slots__ = ( "__dict__", "_application", "_chat_id", "_user_id", "args", "coroutine", "error", "job", "matches", ) def __init__( self: "CCT", application: "Application[BT, CCT, UD, CD, BD, Any]", chat_id: Optional[int] = None, user_id: Optional[int] = None, ): self._application: Application[BT, CCT, UD, CD, BD, Any] = application self._chat_id: Optional[int] = chat_id self._user_id: Optional[int] = user_id self.args: Optional[List[str]] = None self.matches: Optional[List[Match[str]]] = None self.error: Optional[Exception] = None self.job: Optional[Job[CCT]] = None self.coroutine: Optional[ Union[Generator[Optional[Future[object]], None, Any], Awaitable[Any]] ] = None @property def application(self) -> "Application[BT, CCT, UD, CD, BD, Any]": """:class:`telegram.ext.Application`: The application associated with this context.""" return self._application @property def bot_data(self) -> BD: """:obj:`ContextTypes.bot_data`: Optional. An object that can be used to keep any data in. For each update it will be the same :attr:`ContextTypes.bot_data`. Defaults to :obj:`dict`. .. seealso:: :wiki:`Storing Bot, User and Chat Related Data\ ` """ return self.application.bot_data @bot_data.setter def bot_data(self, value: object) -> NoReturn: raise AttributeError( f"You can not assign a new value to bot_data, see {_STORING_DATA_WIKI}" ) @property def chat_data(self) -> Optional[CD]: """:obj:`ContextTypes.chat_data`: Optional. An object that can be used to keep any data in. For each update from the same chat id it will be the same :obj:`ContextTypes.chat_data`. Defaults to :obj:`dict`. Warning: When a group chat migrates to a supergroup, its chat id will change and the ``chat_data`` needs to be transferred. For details see our :wiki:`wiki page `. .. seealso:: :wiki:`Storing Bot, User and Chat Related Data\ ` .. versionchanged:: 20.0 The chat data is now also present in error handlers if the error is caused by a job. """ if self._chat_id is not None: return self._application.chat_data[self._chat_id] return None @chat_data.setter def chat_data(self, value: object) -> NoReturn: raise AttributeError( f"You can not assign a new value to chat_data, see {_STORING_DATA_WIKI}" ) @property def user_data(self) -> Optional[UD]: """:obj:`ContextTypes.user_data`: Optional. An object that can be used to keep any data in. For each update from the same user it will be the same :obj:`ContextTypes.user_data`. Defaults to :obj:`dict`. .. seealso:: :wiki:`Storing Bot, User and Chat Related Data\ ` .. versionchanged:: 20.0 The user data is now also present in error handlers if the error is caused by a job. """ if self._user_id is not None: return self._application.user_data[self._user_id] return None @user_data.setter def user_data(self, value: object) -> NoReturn: raise AttributeError( f"You can not assign a new value to user_data, see {_STORING_DATA_WIKI}" ) async def refresh_data(self) -> None: """If :attr:`application` uses persistence, calls :meth:`telegram.ext.BasePersistence.refresh_bot_data` on :attr:`bot_data`, :meth:`telegram.ext.BasePersistence.refresh_chat_data` on :attr:`chat_data` and :meth:`telegram.ext.BasePersistence.refresh_user_data` on :attr:`user_data`, if appropriate. Will be called by :meth:`telegram.ext.Application.process_update` and :meth:`telegram.ext.Job.run`. .. versionadded:: 13.6 """ if self.application.persistence: if self.application.persistence.store_data.bot_data: await self.application.persistence.refresh_bot_data(self.bot_data) if self.application.persistence.store_data.chat_data and self._chat_id is not None: await self.application.persistence.refresh_chat_data( chat_id=self._chat_id, chat_data=self.chat_data, # type: ignore[arg-type] ) if self.application.persistence.store_data.user_data and self._user_id is not None: await self.application.persistence.refresh_user_data( user_id=self._user_id, user_data=self.user_data, # type: ignore[arg-type] ) def drop_callback_data(self, callback_query: CallbackQuery) -> None: """ Deletes the cached data for the specified callback query. .. versionadded:: 13.6 Note: Will *not* raise exceptions in case the data is not found in the cache. *Will* raise :exc:`KeyError` in case the callback query can not be found in the cache. .. seealso:: :wiki:`Arbitrary callback_data ` Args: callback_query (:class:`telegram.CallbackQuery`): The callback query. Raises: KeyError | RuntimeError: :exc:`KeyError`, if the callback query can not be found in the cache and :exc:`RuntimeError`, if the bot doesn't allow for arbitrary callback data. """ if isinstance(self.bot, ExtBot): if self.bot.callback_data_cache is None: raise RuntimeError( "This telegram.ext.ExtBot instance does not use arbitrary callback data." ) self.bot.callback_data_cache.drop_data(callback_query) else: raise RuntimeError("telegram.Bot does not allow for arbitrary callback data.") @classmethod def from_error( cls: Type["CCT"], update: object, error: Exception, application: "Application[BT, CCT, UD, CD, BD, Any]", job: Optional["Job[Any]"] = None, coroutine: Optional[ Union[Generator[Optional["Future[object]"], None, Any], Awaitable[Any]] ] = None, ) -> "CCT": """ Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the error handlers. .. seealso:: :meth:`telegram.ext.Application.add_error_handler` .. versionchanged:: 20.0 Removed arguments ``async_args`` and ``async_kwargs``. Args: update (:obj:`object` | :class:`telegram.Update`): The update associated with the error. May be :obj:`None`, e.g. for errors in job callbacks. error (:obj:`Exception`): The error. application (:class:`telegram.ext.Application`): The application associated with this context. job (:class:`telegram.ext.Job`, optional): The job associated with the error. .. versionadded:: 20.0 coroutine (:term:`awaitable`, optional): The awaitable associated with this error if the error was caused by a coroutine run with :meth:`Application.create_task` or a handler callback with :attr:`block=False `. .. versionadded:: 20.0 .. versionchanged:: 20.2 Accepts :class:`asyncio.Future` and generator-based coroutine functions. Returns: :class:`telegram.ext.CallbackContext` """ # update and job will never be present at the same time if update is not None: self = cls.from_update(update, application) elif job is not None: self = cls.from_job(job, application) else: self = cls(application) # type: ignore self.error = error self.coroutine = coroutine return self @classmethod def from_update( cls: Type["CCT"], update: object, application: "Application[Any, CCT, Any, Any, Any, Any]", ) -> "CCT": """ Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the handlers. .. seealso:: :meth:`telegram.ext.Application.add_handler` Args: update (:obj:`object` | :class:`telegram.Update`): The update. application (:class:`telegram.ext.Application`): The application associated with this context. Returns: :class:`telegram.ext.CallbackContext` """ if isinstance(update, Update): chat = update.effective_chat user = update.effective_user chat_id = chat.id if chat else None user_id = user.id if user else None return cls(application, chat_id=chat_id, user_id=user_id) # type: ignore return cls(application) # type: ignore @classmethod def from_job( cls: Type["CCT"], job: "Job[CCT]", application: "Application[Any, CCT, Any, Any, Any, Any]", ) -> "CCT": """ Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to a job callback. .. seealso:: :meth:`telegram.ext.JobQueue` Args: job (:class:`telegram.ext.Job`): The job. application (:class:`telegram.ext.Application`): The application associated with this context. Returns: :class:`telegram.ext.CallbackContext` """ self = cls(application, chat_id=job.chat_id, user_id=job.user_id) # type: ignore self.job = job return self def update(self, data: Dict[str, object]) -> None: """Updates ``self.__slots__`` with the passed data. Args: data (Dict[:obj:`str`, :obj:`object`]): The data. """ for key, value in data.items(): setattr(self, key, value) @property def bot(self) -> BT: """:class:`telegram.Bot`: The bot associated with this context.""" return self._application.bot @property def job_queue(self) -> Optional["JobQueue[CCT]"]: """ :class:`telegram.ext.JobQueue`: The :class:`JobQueue` used by the :class:`telegram.ext.Application`. .. seealso:: :wiki:`Job Queue ` """ if self._application._job_queue is None: # pylint: disable=protected-access warn( "No `JobQueue` set up. To use `JobQueue`, you must install PTB via " '`pip install "python-telegram-bot[job-queue]"`.', stacklevel=2, ) return self._application._job_queue # pylint: disable=protected-access @property def update_queue(self) -> "Queue[object]": """ :class:`asyncio.Queue`: The :class:`asyncio.Queue` instance used by the :class:`telegram.ext.Application` and (usually) the :class:`telegram.ext.Updater` associated with this context. """ return self._application.update_queue @property def match(self) -> Optional[Match[str]]: """ :meth:`re.Match `: The first match from :attr:`matches`. Useful if you are only filtering using a single regex filter. Returns :obj:`None` if :attr:`matches` is empty. """ try: return self.matches[0] # type: ignore[index] # pylint: disable=unsubscriptable-object except (IndexError, TypeError): return None python-telegram-bot-21.1.1/telegram/ext/_callbackdatacache.py000066400000000000000000000440151460724040100241620ustar00rootroot00000000000000#!/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 CallbackDataCache class.""" import time from datetime import datetime from typing import TYPE_CHECKING, Any, Dict, MutableMapping, Optional, Tuple, Union, cast from uuid import uuid4 try: from cachetools import LRUCache CACHE_TOOLS_AVAILABLE = True except ImportError: CACHE_TOOLS_AVAILABLE = False from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, User from telegram._utils.datetime import to_float_timestamp from telegram.error import TelegramError from telegram.ext._utils.types import CDCData if TYPE_CHECKING: from telegram.ext import ExtBot class InvalidCallbackData(TelegramError): """ Raised when the received callback data has been tampered with or deleted from cache. Examples: :any:`Arbitrary Callback Data Bot ` .. seealso:: :wiki:`Arbitrary callback_data ` .. versionadded:: 13.6 Args: callback_data (:obj:`int`, optional): The button data of which the callback data could not be found. Attributes: callback_data (:obj:`int`): Optional. The button data of which the callback data could not be found. """ __slots__ = ("callback_data",) def __init__(self, callback_data: Optional[str] = None) -> None: super().__init__( "The object belonging to this callback_data was deleted or the callback_data was " "manipulated." ) self.callback_data: Optional[str] = callback_data def __reduce__(self) -> Tuple[type, Tuple[Optional[str]]]: # type: ignore[override] """Defines how to serialize the exception for pickle. See :py:meth:`object.__reduce__` for more info. Returns: :obj:`tuple` """ return self.__class__, (self.callback_data,) class _KeyboardData: __slots__ = ("access_time", "button_data", "keyboard_uuid") def __init__( self, keyboard_uuid: str, access_time: Optional[float] = None, button_data: Optional[Dict[str, object]] = None, ): self.keyboard_uuid = keyboard_uuid self.button_data = button_data or {} self.access_time = access_time or time.time() def update_access_time(self) -> None: """Updates the access time with the current time.""" self.access_time = time.time() def to_tuple(self) -> Tuple[str, float, Dict[str, object]]: """Gives a tuple representation consisting of the keyboard uuid, the access time and the button data. """ return self.keyboard_uuid, self.access_time, self.button_data class CallbackDataCache: """A custom cache for storing the callback data of a :class:`telegram.ext.ExtBot`. Internally, it keeps two mappings with fixed maximum size: * One for mapping the data received in callback queries to the cached objects * One for mapping the IDs of received callback queries to the cached objects The second mapping allows to manually drop data that has been cached for keyboards of messages sent via inline mode. If necessary, will drop the least recently used items. Important: If you want to use this class, you must install PTB with the optional requirement ``callback-data``, i.e. .. code-block:: bash pip install "python-telegram-bot[callback-data]" Examples: :any:`Arbitrary Callback Data Bot ` .. seealso:: :wiki:`Architecture Overview `, :wiki:`Arbitrary callback_data ` .. versionadded:: 13.6 .. versionchanged:: 20.0 To use this class, PTB must be installed via ``pip install "python-telegram-bot[callback-data]"``. Args: bot (:class:`telegram.ext.ExtBot`): The bot this cache is for. maxsize (:obj:`int`, optional): Maximum number of items in each of the internal mappings. Defaults to ``1024``. persistent_data (Tuple[List[Tuple[:obj:`str`, :obj:`float`, \ Dict[:obj:`str`, :class:`object`]]], Dict[:obj:`str`, :obj:`str`]], optional): \ Data to initialize the cache with, as returned by \ :meth:`telegram.ext.BasePersistence.get_callback_data`. Attributes: bot (:class:`telegram.ext.ExtBot`): The bot this cache is for. """ __slots__ = ("_callback_queries", "_keyboard_data", "_maxsize", "bot") def __init__( self, bot: "ExtBot[Any]", maxsize: int = 1024, persistent_data: Optional[CDCData] = None, ): if not CACHE_TOOLS_AVAILABLE: raise RuntimeError( "To use `CallbackDataCache`, PTB must be installed via `pip install " '"python-telegram-bot[callback-data]"`.' ) self.bot: ExtBot[Any] = bot self._maxsize: int = maxsize self._keyboard_data: MutableMapping[str, _KeyboardData] = LRUCache(maxsize=maxsize) self._callback_queries: MutableMapping[str, str] = LRUCache(maxsize=maxsize) if persistent_data: self.load_persistence_data(persistent_data) def load_persistence_data(self, persistent_data: CDCData) -> None: """Loads data into the cache. Warning: This method is not intended to be called by users directly. .. versionadded:: 20.0 Args: persistent_data (Tuple[List[Tuple[:obj:`str`, :obj:`float`, \ Dict[:obj:`str`, :class:`object`]]], Dict[:obj:`str`, :obj:`str`]], optional): \ Data to load, as returned by \ :meth:`telegram.ext.BasePersistence.get_callback_data`. """ keyboard_data, callback_queries = persistent_data for key, value in callback_queries.items(): self._callback_queries[key] = value for uuid, access_time, data in keyboard_data: self._keyboard_data[uuid] = _KeyboardData( keyboard_uuid=uuid, access_time=access_time, button_data=data ) @property def maxsize(self) -> int: """:obj:`int`: The maximum size of the cache. .. versionchanged:: 20.0 This property is now read-only. """ return self._maxsize @property def persistence_data(self) -> CDCData: """Tuple[List[Tuple[:obj:`str`, :obj:`float`, Dict[:obj:`str`, :class:`object`]]], Dict[:obj:`str`, :obj:`str`]]: The data that needs to be persisted to allow caching callback data across bot reboots. """ # While building a list/dict from the LRUCaches has linear runtime (in the number of # entries), the runtime is bounded by maxsize and it has the big upside of not throwing a # highly customized data structure at users trying to implement a custom persistence class return [data.to_tuple() for data in self._keyboard_data.values()], dict( self._callback_queries.items() ) def process_keyboard(self, reply_markup: InlineKeyboardMarkup) -> InlineKeyboardMarkup: """Registers the reply markup to the cache. If any of the buttons have :attr:`~telegram.InlineKeyboardButton.callback_data`, stores that data and builds a new keyboard with the correspondingly replaced buttons. Otherwise, does nothing and returns the original reply markup. Args: reply_markup (:class:`telegram.InlineKeyboardMarkup`): The keyboard. Returns: :class:`telegram.InlineKeyboardMarkup`: The keyboard to be passed to Telegram. """ keyboard_uuid = uuid4().hex keyboard_data = _KeyboardData(keyboard_uuid) # Built a new nested list of buttons by replacing the callback data if needed buttons = [ [ ( # We create a new button instead of replacing callback_data in case the # same object is used elsewhere InlineKeyboardButton( btn.text, callback_data=self.__put_button(btn.callback_data, keyboard_data), ) if btn.callback_data else btn ) for btn in column ] for column in reply_markup.inline_keyboard ] if not keyboard_data.button_data: # If we arrive here, no data had to be replaced and we can return the input return reply_markup self._keyboard_data[keyboard_uuid] = keyboard_data return InlineKeyboardMarkup(buttons) @staticmethod def __put_button(callback_data: object, keyboard_data: _KeyboardData) -> str: """Stores the data for a single button in :attr:`keyboard_data`. Returns the string that should be passed instead of the callback_data, which is ``keyboard_uuid + button_uuids``. """ uuid = uuid4().hex keyboard_data.button_data[uuid] = callback_data return f"{keyboard_data.keyboard_uuid}{uuid}" def __get_keyboard_uuid_and_button_data( self, callback_data: str ) -> Union[Tuple[str, object], Tuple[None, InvalidCallbackData]]: keyboard, button = self.extract_uuids(callback_data) try: # we get the values before calling update() in case KeyErrors are raised # we don't want to update in that case keyboard_data = self._keyboard_data[keyboard] button_data = keyboard_data.button_data[button] # Update the timestamp for the LRU keyboard_data.update_access_time() return keyboard, button_data except KeyError: return None, InvalidCallbackData(callback_data) @staticmethod def extract_uuids(callback_data: str) -> Tuple[str, str]: """Extracts the keyboard uuid and the button uuid from the given :paramref:`callback_data`. Args: callback_data (:obj:`str`): The :paramref:`~telegram.InlineKeyboardButton.callback_data` as present in the button. Returns: (:obj:`str`, :obj:`str`): Tuple of keyboard and button uuid """ # Extract the uuids as put in __put_button return callback_data[:32], callback_data[32:] def process_message(self, message: Message) -> None: """Replaces the data in the inline keyboard attached to the message with the cached objects, if necessary. If the data could not be found, :class:`telegram.ext.InvalidCallbackData` will be inserted. Note: Checks :attr:`telegram.Message.via_bot` and :attr:`telegram.Message.from_user` to check if the reply markup (if any) was actually sent by this cache's bot. If it was not, the message will be returned unchanged. Note that this will fail for channel posts, as :attr:`telegram.Message.from_user` is :obj:`None` for those! In the corresponding reply markups the callback data will be replaced by :class:`telegram.ext.InvalidCallbackData`. Warning: * Does *not* consider :attr:`telegram.Message.reply_to_message` and :attr:`telegram.Message.pinned_message`. Pass them to this method separately. * *In place*, i.e. the passed :class:`telegram.Message` will be changed! Args: message (:class:`telegram.Message`): The message. """ self.__process_message(message) def __process_message(self, message: Message) -> Optional[str]: """As documented in process_message, but returns the uuid of the attached keyboard, if any, which is relevant for process_callback_query. **IN PLACE** """ if not message.reply_markup: return None if message.via_bot: sender: Optional[User] = message.via_bot elif message.from_user: sender = message.from_user else: sender = None if sender is not None and sender != self.bot.bot: return None keyboard_uuid = None for row in message.reply_markup.inline_keyboard: for button in row: if button.callback_data: button_data = cast(str, button.callback_data) keyboard_id, callback_data = self.__get_keyboard_uuid_and_button_data( button_data ) # update_callback_data makes sure that the _id_attrs are updated button.update_callback_data(callback_data) # This is lazy loaded. The firsts time we find a button # we load the associated keyboard - afterwards, there is if not keyboard_uuid and not isinstance(callback_data, InvalidCallbackData): keyboard_uuid = keyboard_id return keyboard_uuid def process_callback_query(self, callback_query: CallbackQuery) -> None: """Replaces the data in the callback query and the attached messages keyboard with the cached objects, if necessary. If the data could not be found, :class:`telegram.ext.InvalidCallbackData` will be inserted. If :attr:`telegram.CallbackQuery.data` or :attr:`telegram.CallbackQuery.message` is present, this also saves the callback queries ID in order to be able to resolve it to the stored data. Note: Also considers inserts data into the buttons of :attr:`telegram.Message.reply_to_message` and :attr:`telegram.Message.pinned_message` if necessary. Warning: *In place*, i.e. the passed :class:`telegram.CallbackQuery` will be changed! Args: callback_query (:class:`telegram.CallbackQuery`): The callback query. """ mapped = False if callback_query.data: data = callback_query.data # Get the cached callback data for the CallbackQuery keyboard_uuid, button_data = self.__get_keyboard_uuid_and_button_data(data) with callback_query._unfrozen(): callback_query.data = button_data # type: ignore[assignment] # Map the callback queries ID to the keyboards UUID for later use if not mapped and not isinstance(button_data, InvalidCallbackData): self._callback_queries[callback_query.id] = keyboard_uuid # type: ignore mapped = True # Get the cached callback data for the inline keyboard attached to the # CallbackQuery. if isinstance(callback_query.message, Message): self.__process_message(callback_query.message) for maybe_message in ( callback_query.message.pinned_message, callback_query.message.reply_to_message, ): if isinstance(maybe_message, Message): self.__process_message(maybe_message) def drop_data(self, callback_query: CallbackQuery) -> None: """Deletes the data for the specified callback query. Note: Will *not* raise exceptions in case the callback data is not found in the cache. *Will* raise :exc:`KeyError` in case the callback query can not be found in the cache. Args: callback_query (:class:`telegram.CallbackQuery`): The callback query. Raises: KeyError: If the callback query can not be found in the cache """ try: keyboard_uuid = self._callback_queries.pop(callback_query.id) self.__drop_keyboard(keyboard_uuid) except KeyError as exc: raise KeyError("CallbackQuery was not found in cache.") from exc def __drop_keyboard(self, keyboard_uuid: str) -> None: try: self._keyboard_data.pop(keyboard_uuid) except KeyError: return def clear_callback_data(self, time_cutoff: Optional[Union[float, datetime]] = None) -> None: """Clears the stored callback data. Args: time_cutoff (:obj:`float` | :obj:`datetime.datetime`, optional): Pass a UNIX timestamp or a :obj:`datetime.datetime` to clear only entries which are older. For timezone naive :obj:`datetime.datetime` objects, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used. """ self.__clear(self._keyboard_data, time_cutoff=time_cutoff) def clear_callback_queries(self) -> None: """Clears the stored callback query IDs.""" self.__clear(self._callback_queries) def __clear( self, mapping: MutableMapping, time_cutoff: Optional[Union[float, datetime]] = None ) -> None: if not time_cutoff: mapping.clear() return if isinstance(time_cutoff, datetime): effective_cutoff = to_float_timestamp( time_cutoff, tzinfo=self.bot.defaults.tzinfo if self.bot.defaults else None ) else: effective_cutoff = time_cutoff # We need a list instead of a generator here, as the list doesn't change it's size # during the iteration to_drop = [key for key, data in mapping.items() if data.access_time < effective_cutoff] for key in to_drop: mapping.pop(key) python-telegram-bot-21.1.1/telegram/ext/_contexttypes.py000066400000000000000000000171221460724040100233600ustar00rootroot00000000000000#!/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 auxiliary class ContextTypes.""" from typing import Any, Dict, Generic, Type, overload from telegram.ext._callbackcontext import CallbackContext from telegram.ext._extbot import ExtBot from telegram.ext._utils.types import BD, CCT, CD, UD ADict = Dict[Any, Any] class ContextTypes(Generic[CCT, UD, CD, BD]): """ Convenience class to gather customizable types of the :class:`telegram.ext.CallbackContext` interface. Examples: :any:`ContextTypes Bot ` .. seealso:: :wiki:`Architecture Overview `, :wiki:`Storing Bot, User and Chat Related Data ` .. versionadded:: 13.6 Args: context (:obj:`type`, optional): Determines the type of the ``context`` argument of all (error-)handler callbacks and job callbacks. Must be a subclass of :class:`telegram.ext.CallbackContext`. Defaults to :class:`telegram.ext.CallbackContext`. bot_data (:obj:`type`, optional): Determines the type of :attr:`context.bot_data ` of all (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support instantiating without arguments. chat_data (:obj:`type`, optional): Determines the type of :attr:`context.chat_data ` of all (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support instantiating without arguments. user_data (:obj:`type`, optional): Determines the type of :attr:`context.user_data ` of all (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support instantiating without arguments. """ DEFAULT_TYPE = CallbackContext[ExtBot[None], ADict, ADict, ADict] """Shortcut for the type annotation for the ``context`` argument that's correct for the default settings, i.e. if :class:`telegram.ext.ContextTypes` is not used. Example: .. code:: python async def callback(update: Update, context: ContextTypes.DEFAULT_TYPE): ... .. versionadded: 20.0 """ __slots__ = ("_bot_data", "_chat_data", "_context", "_user_data") # overload signatures generated with # https://gist.github.com/Bibo-Joshi/399382cda537fb01bd86b13c3d03a956 @overload def __init__( self: "ContextTypes[CallbackContext[ExtBot[Any], ADict, ADict, ADict], ADict, ADict, ADict]", # pylint: disable=line-too-long # noqa: E501 ): ... @overload def __init__(self: "ContextTypes[CCT, ADict, ADict, ADict]", context: Type[CCT]): ... @overload def __init__( self: "ContextTypes[CallbackContext[ExtBot[Any], UD, ADict, ADict], UD, ADict, ADict]", user_data: Type[UD], ): ... @overload def __init__( self: "ContextTypes[CallbackContext[ExtBot[Any], ADict, CD, ADict], ADict, CD, ADict]", chat_data: Type[CD], ): ... @overload def __init__( self: "ContextTypes[CallbackContext[ExtBot[Any], ADict, ADict, BD], ADict, ADict, BD]", bot_data: Type[BD], ): ... @overload def __init__( self: "ContextTypes[CCT, UD, ADict, ADict]", context: Type[CCT], user_data: Type[UD] ): ... @overload def __init__( self: "ContextTypes[CCT, ADict, CD, ADict]", context: Type[CCT], chat_data: Type[CD] ): ... @overload def __init__( self: "ContextTypes[CCT, ADict, ADict, BD]", context: Type[CCT], bot_data: Type[BD] ): ... @overload def __init__( self: "ContextTypes[CallbackContext[ExtBot[Any], UD, CD, ADict], UD, CD, ADict]", user_data: Type[UD], chat_data: Type[CD], ): ... @overload def __init__( self: "ContextTypes[CallbackContext[ExtBot[Any], UD, ADict, BD], UD, ADict, BD]", user_data: Type[UD], bot_data: Type[BD], ): ... @overload def __init__( self: "ContextTypes[CallbackContext[ExtBot[Any], ADict, CD, BD], ADict, CD, BD]", chat_data: Type[CD], bot_data: Type[BD], ): ... @overload def __init__( self: "ContextTypes[CCT, UD, CD, ADict]", context: Type[CCT], user_data: Type[UD], chat_data: Type[CD], ): ... @overload def __init__( self: "ContextTypes[CCT, UD, ADict, BD]", context: Type[CCT], user_data: Type[UD], bot_data: Type[BD], ): ... @overload def __init__( self: "ContextTypes[CCT, ADict, CD, BD]", context: Type[CCT], chat_data: Type[CD], bot_data: Type[BD], ): ... @overload def __init__( self: "ContextTypes[CallbackContext[ExtBot[Any], UD, CD, BD], UD, CD, BD]", user_data: Type[UD], chat_data: Type[CD], bot_data: Type[BD], ): ... @overload def __init__( self: "ContextTypes[CCT, UD, CD, BD]", context: Type[CCT], user_data: Type[UD], chat_data: Type[CD], bot_data: Type[BD], ): ... def __init__( # type: ignore[misc] self, context: "Type[CallbackContext[ExtBot[Any], ADict, ADict, ADict]]" = CallbackContext, bot_data: Type[ADict] = dict, chat_data: Type[ADict] = dict, user_data: Type[ADict] = dict, ): if not issubclass(context, CallbackContext): raise ValueError("context must be a subclass of CallbackContext.") # We make all those only accessible via properties because we don't currently support # changing this at runtime, so overriding the attributes doesn't make sense self._context = context self._bot_data = bot_data self._chat_data = chat_data self._user_data = user_data @property def context(self) -> Type[CCT]: """The type of the ``context`` argument of all (error-)handler callbacks and job callbacks. """ return self._context # type: ignore[return-value] @property def bot_data(self) -> Type[BD]: """The type of :attr:`context.bot_data ` of all (error-)handler callbacks and job callbacks. """ return self._bot_data # type: ignore[return-value] @property def chat_data(self) -> Type[CD]: """The type of :attr:`context.chat_data ` of all (error-)handler callbacks and job callbacks. """ return self._chat_data # type: ignore[return-value] @property def user_data(self) -> Type[UD]: """The type of :attr:`context.user_data ` of all (error-)handler callbacks and job callbacks. """ return self._user_data # type: ignore[return-value] python-telegram-bot-21.1.1/telegram/ext/_defaults.py000066400000000000000000000354201460724040100224170ustar00rootroot00000000000000#!/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 Defaults, which allows passing default values to Application.""" import datetime from typing import Any, Dict, NoReturn, Optional, final from telegram import LinkPreviewOptions from telegram._utils.datetime import UTC from telegram._utils.types import ODVInput from telegram._utils.warnings import warn from telegram.warnings import PTBDeprecationWarning @final class Defaults: """Convenience Class to gather all parameters with a (user defined) default value .. seealso:: :wiki:`Architecture Overview `, :wiki:`Adding Defaults to Your Bot ` .. versionchanged:: 20.0 Removed the argument and attribute ``timeout``. Specify default timeout behavior for the networking backend directly via :class:`telegram.ext.ApplicationBuilder` instead. Parameters: parse_mode (:obj:`str`, optional): |parse_mode| disable_notification (:obj:`bool`, optional): |disable_notification| disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this message. Mutually exclusive with :paramref:`link_preview_options`. .. deprecated:: 20.8 Use :paramref:`link_preview_options` instead. This parameter will be removed in future versions. allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|. Will be used for :attr:`telegram.ReplyParameters.allow_sending_without_reply`. quote (:obj:`bool`, optional): |reply_quote| .. deprecated:: 20.8 Use :paramref:`do_quote` instead. This parameter will be removed in future versions. tzinfo (:class:`datetime.tzinfo`, optional): A timezone to be used for all date(time) inputs appearing throughout PTB, i.e. if a timezone naive date(time) object is passed somewhere, it will be assumed to be in :paramref:`tzinfo`. If the :class:`telegram.ext.JobQueue` is used, this must be a timezone provided by the ``pytz`` module. Defaults to ``pytz.utc``, if available, and :attr:`datetime.timezone.utc` otherwise. block (:obj:`bool`, optional): Default setting for the :paramref:`BaseHandler.block` parameter of handlers and error handlers registered through :meth:`Application.add_handler` and :meth:`Application.add_error_handler`. Defaults to :obj:`True`. protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 20.0 link_preview_options (:class:`telegram.LinkPreviewOptions`, optional): Link preview generation options for all outgoing messages. Mutually exclusive with :paramref:`disable_web_page_preview`. This object is used for the corresponding parameter of :meth:`telegram.Bot.send_message`, :meth:`telegram.Bot.edit_message_text`, and :class:`telegram.InputTextMessageContent` if not specified. If a value is specified for the corresponding parameter, only those parameters of :class:`telegram.LinkPreviewOptions` will be overridden that are not explicitly set. Example: .. code-block:: python from telegram import LinkPreviewOptions from telegram.ext import Defaults, ExtBot defaults = Defaults( link_preview_options=LinkPreviewOptions(show_above_text=True) ) chat_id = 123 async def main(): async with ExtBot("Token", defaults=defaults) as bot: # The link preview will be shown above the text. await bot.send_message(chat_id, "https://python-telegram-bot.org") # The link preview will be shown below the text. await bot.send_message( chat_id, "https://python-telegram-bot.org", link_preview_options=LinkPreviewOptions(show_above_text=False) ) # The link preview will be shown above the text, but the preview will # show Telegram. await bot.send_message( chat_id, "https://python-telegram-bot.org", link_preview_options=LinkPreviewOptions(url="https://telegram.org") ) .. versionadded:: 20.8 do_quote(:obj:`bool`, optional): |reply_quote| .. versionadded:: 20.8 """ __slots__ = ( "_allow_sending_without_reply", "_api_defaults", "_block", "_disable_notification", "_do_quote", "_link_preview_options", "_parse_mode", "_protect_content", "_tzinfo", ) def __init__( self, parse_mode: Optional[str] = None, disable_notification: Optional[bool] = None, disable_web_page_preview: Optional[bool] = None, quote: Optional[bool] = None, tzinfo: datetime.tzinfo = UTC, block: bool = True, allow_sending_without_reply: Optional[bool] = None, protect_content: Optional[bool] = None, link_preview_options: Optional["LinkPreviewOptions"] = None, do_quote: Optional[bool] = None, ): self._parse_mode: Optional[str] = parse_mode self._disable_notification: Optional[bool] = disable_notification self._allow_sending_without_reply: Optional[bool] = allow_sending_without_reply self._tzinfo: datetime.tzinfo = tzinfo self._block: bool = block self._protect_content: Optional[bool] = protect_content if disable_web_page_preview is not None and link_preview_options is not None: raise ValueError( "`disable_web_page_preview` and `link_preview_options` are mutually exclusive." ) if quote is not None and do_quote is not None: raise ValueError("`quote` and `do_quote` are mutually exclusive") if disable_web_page_preview is not None: warn( "`Defaults.disable_web_page_preview` is deprecated. Use " "`Defaults.link_preview_options` instead.", category=PTBDeprecationWarning, stacklevel=2, ) self._link_preview_options: Optional[LinkPreviewOptions] = LinkPreviewOptions( is_disabled=disable_web_page_preview ) else: self._link_preview_options = link_preview_options if quote is not None: warn( "`Defaults.quote` is deprecated. Use `Defaults.do_quote` instead.", category=PTBDeprecationWarning, stacklevel=2, ) self._do_quote: Optional[bool] = quote else: self._do_quote = do_quote # Gather all defaults that actually have a default value self._api_defaults = {} for kwarg in ( "parse_mode", "explanation_parse_mode", "disable_notification", "allow_sending_without_reply", "protect_content", "link_preview_options", "do_quote", ): value = getattr(self, kwarg) if value is not None: self._api_defaults[kwarg] = value 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` The hash value of the object. """ return hash( ( self._parse_mode, self._disable_notification, self.disable_web_page_preview, self._allow_sending_without_reply, self.quote, self._tzinfo, self._block, self._protect_content, ) ) def __eq__(self, other: object) -> bool: """Defines equality condition for the :class:`Defaults` object. Two objects of this class are considered to be equal if all their parameters are identical. Returns: :obj:`True` if both objects have all parameters identical. :obj:`False` otherwise. """ if isinstance(other, Defaults): return all(getattr(self, attr) == getattr(other, attr) for attr in self.__slots__) return False @property def api_defaults(self) -> Dict[str, Any]: # skip-cq: PY-D0003 return self._api_defaults @property def parse_mode(self) -> Optional[str]: """:obj:`str`: Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or URLs in your bot's message. """ return self._parse_mode @parse_mode.setter def parse_mode(self, value: object) -> NoReturn: raise AttributeError("You can not assign a new value to parse_mode after initialization.") @property def explanation_parse_mode(self) -> Optional[str]: """:obj:`str`: Optional. Alias for :attr:`parse_mode`, used for the corresponding parameter of :meth:`telegram.Bot.send_poll`. """ return self._parse_mode @explanation_parse_mode.setter def explanation_parse_mode(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to explanation_parse_mode after initialization." ) @property def quote_parse_mode(self) -> Optional[str]: """:obj:`str`: Optional. Alias for :attr:`parse_mode`, used for the corresponding parameter of :meth:`telegram.ReplyParameters`. """ return self._parse_mode @quote_parse_mode.setter def quote_parse_mode(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to quote_parse_mode after initialization." ) @property def disable_notification(self) -> Optional[bool]: """:obj:`bool`: Optional. Sends the message silently. Users will receive a notification with no sound. """ return self._disable_notification @disable_notification.setter def disable_notification(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to disable_notification after initialization." ) @property def disable_web_page_preview(self) -> ODVInput[bool]: """:obj:`bool`: Optional. Disables link previews for links in all outgoing messages. .. deprecated:: 20.8 Use :attr:`link_preview_options` instead. This attribute will be removed in future versions. """ return self._link_preview_options.is_disabled if self._link_preview_options else None @disable_web_page_preview.setter def disable_web_page_preview(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to disable_web_page_preview after initialization." ) @property def allow_sending_without_reply(self) -> Optional[bool]: """:obj:`bool`: Optional. Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. """ return self._allow_sending_without_reply @allow_sending_without_reply.setter def allow_sending_without_reply(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to allow_sending_without_reply after initialization." ) @property def quote(self) -> Optional[bool]: """:obj:`bool`: Optional. |reply_quote| .. deprecated:: 20.8 Use :attr:`do_quote` instead. This attribute will be removed in future versions. """ return self._do_quote if self._do_quote is not None else None @quote.setter def quote(self, value: object) -> NoReturn: raise AttributeError("You can not assign a new value to quote after initialization.") @property def tzinfo(self) -> datetime.tzinfo: """:obj:`tzinfo`: A timezone to be used for all date(time) objects appearing throughout PTB. """ return self._tzinfo @tzinfo.setter def tzinfo(self, value: object) -> NoReturn: raise AttributeError("You can not assign a new value to tzinfo after initialization.") @property def block(self) -> bool: """:obj:`bool`: Optional. Default setting for the :paramref:`BaseHandler.block` parameter of handlers and error handlers registered through :meth:`Application.add_handler` and :meth:`Application.add_error_handler`. """ return self._block @block.setter def block(self, value: object) -> NoReturn: raise AttributeError("You can not assign a new value to block after initialization.") @property def protect_content(self) -> Optional[bool]: """:obj:`bool`: Optional. Protects the contents of the sent message from forwarding and saving. .. versionadded:: 20.0 """ return self._protect_content @protect_content.setter def protect_content(self, value: object) -> NoReturn: raise AttributeError( "You can't assign a new value to protect_content after initialization." ) @property def link_preview_options(self) -> Optional["LinkPreviewOptions"]: """:class:`telegram.LinkPreviewOptions`: Optional. Link preview generation options for all outgoing messages. .. versionadded:: 20.8 """ return self._link_preview_options @property def do_quote(self) -> Optional[bool]: """:obj:`bool`: Optional. |reply_quote| .. versionadded:: 20.8 """ return self._do_quote python-telegram-bot-21.1.1/telegram/ext/_dictpersistence.py000066400000000000000000000452031460724040100240000ustar00rootroot00000000000000#!/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 DictPersistence class.""" import json from copy import deepcopy from typing import TYPE_CHECKING, Any, Dict, Optional, cast from telegram.ext import BasePersistence, PersistenceInput from telegram.ext._utils.types import CDCData, ConversationDict, ConversationKey if TYPE_CHECKING: from telegram._utils.types import JSONDict class DictPersistence(BasePersistence[Dict[Any, Any], Dict[Any, Any], Dict[Any, Any]]): """Using Python's :obj:`dict` and :mod:`json` for making your bot persistent. Attention: The interface provided by this class is intended to be accessed exclusively by :class:`~telegram.ext.Application`. Calling any of the methods below manually might interfere with the integration of persistence into :class:`~telegram.ext.Application`. Note: * Data managed by :class:`DictPersistence` is in-memory only and will be lost when the bot shuts down. This is, because :class:`DictPersistence` is mainly intended as starting point for custom persistence classes that need to JSON-serialize the stored data before writing them to file/database. * This implementation of :class:`BasePersistence` does not handle data that cannot be serialized by :func:`json.dumps`. .. seealso:: :wiki:`Making Your Bot Persistent ` .. versionchanged:: 20.0 The parameters and attributes ``store_*_data`` were replaced by :attr:`store_data`. Args: store_data (:class:`~telegram.ext.PersistenceInput`, optional): Specifies which kinds of data will be saved by this persistence instance. By default, all available kinds of data will be saved. user_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct user_data on creating this persistence. Default is ``""``. chat_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct chat_data on creating this persistence. Default is ``""``. bot_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct bot_data on creating this persistence. Default is ``""``. conversations_json (:obj:`str`, optional): JSON string that will be used to reconstruct conversation on creating this persistence. Default is ``""``. callback_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct callback_data on creating this persistence. Default is ``""``. .. versionadded:: 13.6 update_interval (:obj:`int` | :obj:`float`, optional): The :class:`~telegram.ext.Application` will update the persistence in regular intervals. This parameter specifies the time (in seconds) to wait between two consecutive runs of updating the persistence. Defaults to 60 seconds. .. versionadded:: 20.0 Attributes: store_data (:class:`~telegram.ext.PersistenceInput`): Specifies which kinds of data will be saved by this persistence instance. """ __slots__ = ( "_bot_data", "_bot_data_json", "_callback_data", "_callback_data_json", "_chat_data", "_chat_data_json", "_conversations", "_conversations_json", "_user_data", "_user_data_json", ) def __init__( self, store_data: Optional[PersistenceInput] = None, user_data_json: str = "", chat_data_json: str = "", bot_data_json: str = "", conversations_json: str = "", callback_data_json: str = "", update_interval: float = 60, ): super().__init__(store_data=store_data, update_interval=update_interval) self._user_data = None self._chat_data = None self._bot_data = None self._callback_data = None self._conversations = None self._user_data_json: Optional[str] = None self._chat_data_json: Optional[str] = None self._bot_data_json: Optional[str] = None self._callback_data_json: Optional[str] = None self._conversations_json: Optional[str] = None if user_data_json: try: self._user_data = self._decode_user_chat_data_from_json(user_data_json) self._user_data_json = user_data_json except (ValueError, AttributeError) as exc: raise TypeError("Unable to deserialize user_data_json. Not valid JSON") from exc if chat_data_json: try: self._chat_data = self._decode_user_chat_data_from_json(chat_data_json) self._chat_data_json = chat_data_json except (ValueError, AttributeError) as exc: raise TypeError("Unable to deserialize chat_data_json. Not valid JSON") from exc if bot_data_json: try: self._bot_data = json.loads(bot_data_json) self._bot_data_json = bot_data_json except (ValueError, AttributeError) as exc: raise TypeError("Unable to deserialize bot_data_json. Not valid JSON") from exc if not isinstance(self._bot_data, dict): raise TypeError("bot_data_json must be serialized dict") if callback_data_json: try: data = json.loads(callback_data_json) except (ValueError, AttributeError) as exc: raise TypeError( "Unable to deserialize callback_data_json. Not valid JSON" ) from exc # We are a bit more thorough with the checking of the format here, because it's # more complicated than for the other things try: if data is None: self._callback_data = None else: self._callback_data = cast( CDCData, ([(one, float(two), three) for one, two, three in data[0]], data[1]), ) self._callback_data_json = callback_data_json except (ValueError, IndexError) as exc: raise TypeError("callback_data_json is not in the required format") from exc if self._callback_data is not None and ( not all( isinstance(entry[2], dict) and isinstance(entry[0], str) for entry in self._callback_data[0] ) or not isinstance(self._callback_data[1], dict) ): raise TypeError("callback_data_json is not in the required format") if conversations_json: try: self._conversations = self._decode_conversations_from_json(conversations_json) self._conversations_json = conversations_json except (ValueError, AttributeError) as exc: raise TypeError( "Unable to deserialize conversations_json. Not valid JSON" ) from exc @property def user_data(self) -> Optional[Dict[int, Dict[Any, Any]]]: """:obj:`dict`: The user_data as a dict.""" return self._user_data @property def user_data_json(self) -> str: """:obj:`str`: The user_data serialized as a JSON-string.""" if self._user_data_json: return self._user_data_json return json.dumps(self.user_data) @property def chat_data(self) -> Optional[Dict[int, Dict[Any, Any]]]: """:obj:`dict`: The chat_data as a dict.""" return self._chat_data @property def chat_data_json(self) -> str: """:obj:`str`: The chat_data serialized as a JSON-string.""" if self._chat_data_json: return self._chat_data_json return json.dumps(self.chat_data) @property def bot_data(self) -> Optional[Dict[Any, Any]]: """:obj:`dict`: The bot_data as a dict.""" return self._bot_data @property def bot_data_json(self) -> str: """:obj:`str`: The bot_data serialized as a JSON-string.""" if self._bot_data_json: return self._bot_data_json return json.dumps(self.bot_data) @property def callback_data(self) -> Optional[CDCData]: """Tuple[List[Tuple[:obj:`str`, :obj:`float`, Dict[:obj:`str`, :class:`object`]]], \ Dict[:obj:`str`, :obj:`str`]]: The metadata on the stored callback data. .. versionadded:: 13.6 """ return self._callback_data @property def callback_data_json(self) -> str: """:obj:`str`: The metadata on the stored callback data as a JSON-string. .. versionadded:: 13.6 """ if self._callback_data_json: return self._callback_data_json return json.dumps(self.callback_data) @property def conversations(self) -> Optional[Dict[str, ConversationDict]]: """:obj:`dict`: The conversations as a dict.""" return self._conversations @property def conversations_json(self) -> str: """:obj:`str`: The conversations serialized as a JSON-string.""" if self._conversations_json: return self._conversations_json if self.conversations: return self._encode_conversations_to_json(self.conversations) return json.dumps(self.conversations) async def get_user_data(self) -> Dict[int, Dict[object, object]]: """Returns the user_data created from the ``user_data_json`` or an empty :obj:`dict`. Returns: :obj:`dict`: The restored user data. """ if self.user_data is None: self._user_data = {} return deepcopy(self.user_data) # type: ignore[arg-type] async def get_chat_data(self) -> Dict[int, Dict[object, object]]: """Returns the chat_data created from the ``chat_data_json`` or an empty :obj:`dict`. Returns: :obj:`dict`: The restored chat data. """ if self.chat_data is None: self._chat_data = {} return deepcopy(self.chat_data) # type: ignore[arg-type] async def get_bot_data(self) -> Dict[object, object]: """Returns the bot_data created from the ``bot_data_json`` or an empty :obj:`dict`. Returns: :obj:`dict`: The restored bot data. """ if self.bot_data is None: self._bot_data = {} return deepcopy(self.bot_data) # type: ignore[arg-type] async def get_callback_data(self) -> Optional[CDCData]: """Returns the callback_data created from the ``callback_data_json`` or :obj:`None`. .. versionadded:: 13.6 Returns: Tuple[List[Tuple[:obj:`str`, :obj:`float`, Dict[:obj:`str`, :class:`object`]]], \ Dict[:obj:`str`, :obj:`str`]]: The restored metadata or :obj:`None`, \ if no data was stored. """ if self.callback_data is None: self._callback_data = None return None return deepcopy(self.callback_data) async def get_conversations(self, name: str) -> ConversationDict: """Returns the conversations created from the ``conversations_json`` or an empty :obj:`dict`. Returns: :obj:`dict`: The restored conversations data. """ if self.conversations is None: self._conversations = {} return self.conversations.get(name, {}).copy() # type: ignore[union-attr] async def update_conversation( self, name: str, key: ConversationKey, new_state: Optional[object] ) -> None: """Will update the conversations for the given handler. Args: name (:obj:`str`): The handler's name. key (:obj:`tuple`): The key the state is changed for. new_state (:obj:`tuple` | :class:`object`): The new state for the given key. """ if not self._conversations: self._conversations = {} if self._conversations.setdefault(name, {}).get(key) == new_state: return self._conversations[name][key] = new_state self._conversations_json = None async def update_user_data(self, user_id: int, data: Dict[Any, Any]) -> None: """Will update the user_data (if changed). Args: user_id (:obj:`int`): The user the data might have been changed for. data (:obj:`dict`): The :attr:`telegram.ext.Application.user_data` ``[user_id]``. """ if self._user_data is None: self._user_data = {} if self._user_data.get(user_id) == data: return self._user_data[user_id] = data self._user_data_json = None async def update_chat_data(self, chat_id: int, data: Dict[Any, Any]) -> None: """Will update the chat_data (if changed). Args: chat_id (:obj:`int`): The chat the data might have been changed for. data (:obj:`dict`): The :attr:`telegram.ext.Application.chat_data` ``[chat_id]``. """ if self._chat_data is None: self._chat_data = {} if self._chat_data.get(chat_id) == data: return self._chat_data[chat_id] = data self._chat_data_json = None async def update_bot_data(self, data: Dict[Any, Any]) -> None: """Will update the bot_data (if changed). Args: data (:obj:`dict`): The :attr:`telegram.ext.Application.bot_data`. """ if self._bot_data == data: return self._bot_data = data self._bot_data_json = None async def update_callback_data(self, data: CDCData) -> None: """Will update the callback_data (if changed). .. versionadded:: 13.6 Args: data (Tuple[List[Tuple[:obj:`str`, :obj:`float`, Dict[:obj:`str`, :class:`object`]]], \ Dict[:obj:`str`, :obj:`str`]]): The relevant data to restore :class:`telegram.ext.CallbackDataCache`. """ if self._callback_data == data: return self._callback_data = data self._callback_data_json = None async def drop_chat_data(self, chat_id: int) -> None: """Will delete the specified key from the :attr:`chat_data`. .. versionadded:: 20.0 Args: chat_id (:obj:`int`): The chat id to delete from the persistence. """ if self._chat_data is None: return self._chat_data.pop(chat_id, None) self._chat_data_json = None async def drop_user_data(self, user_id: int) -> None: """Will delete the specified key from the :attr:`user_data`. .. versionadded:: 20.0 Args: user_id (:obj:`int`): The user id to delete from the persistence. """ if self._user_data is None: return self._user_data.pop(user_id, None) self._user_data_json = None async def refresh_user_data(self, user_id: int, user_data: Dict[Any, Any]) -> None: """Does nothing. .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data` """ async def refresh_chat_data(self, chat_id: int, chat_data: Dict[Any, Any]) -> None: """Does nothing. .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data` """ async def refresh_bot_data(self, bot_data: Dict[Any, Any]) -> None: """Does nothing. .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data` """ async def flush(self) -> None: """Does nothing. .. versionadded:: 20.0 .. seealso:: :meth:`telegram.ext.BasePersistence.flush` """ @staticmethod def _encode_conversations_to_json(conversations: Dict[str, ConversationDict]) -> str: """Helper method to encode a conversations dict (that uses tuples as keys) to a JSON-serializable way. Use :meth:`self._decode_conversations_from_json` to decode. Args: conversations (:obj:`dict`): The conversations dict to transform to JSON. Returns: :obj:`str`: The JSON-serialized conversations dict """ tmp: Dict[str, JSONDict] = {} for handler, states in conversations.items(): tmp[handler] = {} for key, state in states.items(): tmp[handler][json.dumps(key)] = state return json.dumps(tmp) @staticmethod def _decode_conversations_from_json(json_string: str) -> Dict[str, ConversationDict]: """Helper method to decode a conversations dict (that uses tuples as keys) from a JSON-string created with :meth:`self._encode_conversations_to_json`. Args: json_string (:obj:`str`): The conversations dict as JSON string. Returns: :obj:`dict`: The conversations dict after decoding """ tmp = json.loads(json_string) conversations: Dict[str, ConversationDict] = {} for handler, states in tmp.items(): conversations[handler] = {} for key, state in states.items(): conversations[handler][tuple(json.loads(key))] = state return conversations @staticmethod def _decode_user_chat_data_from_json(data: str) -> Dict[int, Dict[object, object]]: """Helper method to decode chat or user data (that uses ints as keys) from a JSON-string. Args: data (:obj:`str`): The user/chat_data dict as JSON string. Returns: :obj:`dict`: The user/chat_data defaultdict after decoding """ tmp: Dict[int, Dict[object, object]] = {} decoded_data = json.loads(data) for user, user_data in decoded_data.items(): int_user_id = int(user) tmp[int_user_id] = {} for key, value in user_data.items(): try: _id = int(key) except ValueError: _id = key tmp[int_user_id][_id] = value return tmp python-telegram-bot-21.1.1/telegram/ext/_extbot.py000066400000000000000000005133211460724040100221160ustar00rootroot00000000000000#!/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 with convenience extensions.""" from copy import copy from datetime import datetime from typing import ( TYPE_CHECKING, Any, Callable, Dict, Generic, List, Optional, Sequence, Tuple, Type, TypeVar, Union, cast, no_type_check, overload, ) from uuid import uuid4 from telegram import ( Animation, Audio, Bot, BotCommand, BotCommandScope, BotDescription, BotName, BotShortDescription, BusinessConnection, CallbackQuery, Chat, ChatAdministratorRights, ChatInviteLink, ChatMember, ChatPermissions, ChatPhoto, Contact, Document, File, ForumTopic, GameHighScore, InlineKeyboardMarkup, InlineQueryResultsButton, InputMedia, LinkPreviewOptions, Location, MaskPosition, MenuButton, Message, MessageId, PhotoSize, Poll, ReactionType, ReplyParameters, SentWebAppMessage, Sticker, StickerSet, TelegramObject, Update, User, UserChatBoosts, UserProfilePhotos, Venue, Video, VideoNote, Voice, WebhookInfo, ) from telegram._utils.datetime import to_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram._utils.logging import get_logger from telegram._utils.repr import build_repr_with_selected_attrs from telegram._utils.types import CorrectOptionID, FileInput, JSONDict, ODVInput, ReplyMarkup from telegram.ext._callbackdatacache import CallbackDataCache from telegram.ext._utils.types import RLARGS from telegram.request import BaseRequest from telegram.warnings import PTBUserWarning if TYPE_CHECKING: from telegram import ( InlineQueryResult, InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo, InputSticker, LabeledPrice, MessageEntity, PassportElementError, ShippingOption, ) from telegram.ext import BaseRateLimiter, Defaults HandledTypes = TypeVar("HandledTypes", bound=Union[Message, CallbackQuery, Chat]) KT = TypeVar("KT", bound=ReplyMarkup) class ExtBot(Bot, Generic[RLARGS]): """This object represents a Telegram Bot with convenience extensions. Warning: Not to be confused with :class:`telegram.Bot`. For the documentation of the arguments, methods and attributes, please see :class:`telegram.Bot`. All API methods of this class have an additional keyword argument ``rate_limit_args``. This can be used to pass additional information to the rate limiter, specifically to :paramref:`telegram.ext.BaseRateLimiter.process_request.rate_limit_args`. This class is a :class:`~typing.Generic` class and accepts one type variable that specifies the generic type of the :attr:`rate_limiter` used by the bot. Use :obj:`None` if no rate limiter is used. Warning: * The keyword argument ``rate_limit_args`` can `not` be used, if :attr:`rate_limiter` is :obj:`None`. * The method :meth:`~telegram.Bot.get_updates` is the only method that does not have the additional argument, as this method will never be rate limited. Examples: :any:`Arbitrary Callback Data Bot ` .. seealso:: :wiki:`Arbitrary callback_data ` .. versionadded:: 13.6 .. versionchanged:: 20.0 Removed the attribute ``arbitrary_callback_data``. You can instead use :attr:`bot.callback_data_cache.maxsize ` to access the size of the cache. .. versionchanged:: 20.5 Removed deprecated methods ``set_sticker_set_thumb`` and ``setStickerSetThumb``. Args: defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to be used if not set explicitly in the bot methods. arbitrary_callback_data (:obj:`bool` | :obj:`int`, optional): Whether to allow arbitrary objects as callback data for :class:`telegram.InlineKeyboardButton`. Pass an integer to specify the maximum number of objects cached in memory. Defaults to :obj:`False`. .. seealso:: :wiki:`Arbitrary callback_data ` rate_limiter (:class:`telegram.ext.BaseRateLimiter`, optional): A rate limiter to use for limiting the number of requests made by the bot per time interval. .. versionadded:: 20.0 """ __slots__ = ("_callback_data_cache", "_defaults", "_rate_limiter") _LOGGER = get_logger(__name__, class_name="ExtBot") # using object() would be a tiny bit safer, but a string plays better with the typing setup __RL_KEY = uuid4().hex @overload def __init__( self: "ExtBot[None]", 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, defaults: Optional["Defaults"] = None, arbitrary_callback_data: Union[bool, int] = False, local_mode: bool = False, ): ... @overload def __init__( self: "ExtBot[RLARGS]", 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, defaults: Optional["Defaults"] = None, arbitrary_callback_data: Union[bool, int] = False, local_mode: bool = False, rate_limiter: Optional["BaseRateLimiter[RLARGS]"] = None, ): ... 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, defaults: Optional["Defaults"] = None, arbitrary_callback_data: Union[bool, int] = False, local_mode: bool = False, rate_limiter: Optional["BaseRateLimiter[RLARGS]"] = None, ): super().__init__( token=token, base_url=base_url, base_file_url=base_file_url, request=request, get_updates_request=get_updates_request, private_key=private_key, private_key_password=private_key_password, local_mode=local_mode, ) with self._unfrozen(): self._defaults: Optional[Defaults] = defaults self._rate_limiter: Optional[BaseRateLimiter] = rate_limiter self._callback_data_cache: Optional[CallbackDataCache] = None # set up callback_data if arbitrary_callback_data is False: return if not isinstance(arbitrary_callback_data, bool): maxsize = cast(int, arbitrary_callback_data) else: maxsize = 1024 self._callback_data_cache = CallbackDataCache(bot=self, maxsize=maxsize) def __repr__(self) -> str: """Give a string representation of the bot in the form ``ExtBot[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) @classmethod def _warn( cls, message: str, category: Type[Warning] = PTBUserWarning, stacklevel: int = 0 ) -> None: """We override this method to add one more level to the stacklevel, so that the warning points to the user's code, not to the PTB code. """ super()._warn(message=message, category=category, stacklevel=stacklevel + 2) @property def callback_data_cache(self) -> Optional[CallbackDataCache]: """:class:`telegram.ext.CallbackDataCache`: Optional. The cache for objects passed as callback data for :class:`telegram.InlineKeyboardButton`. Examples: :any:`Arbitrary Callback Data Bot ` .. versionchanged:: 20.0 * This property is now read-only. * This property is now optional and can be :obj:`None` if :paramref:`~telegram.ext.ExtBot.arbitrary_callback_data` is set to :obj:`False`. """ return self._callback_data_cache async def initialize(self) -> None: """See :meth:`telegram.Bot.initialize`. Also initializes the :paramref:`ExtBot.rate_limiter` (if set) by calling :meth:`telegram.ext.BaseRateLimiter.initialize`. """ # Initialize before calling super, because super calls get_me if self.rate_limiter: await self.rate_limiter.initialize() await super().initialize() async def shutdown(self) -> None: """See :meth:`telegram.Bot.shutdown`. Also shuts down the :paramref:`ExtBot.rate_limiter` (if set) by calling :meth:`telegram.ext.BaseRateLimiter.shutdown`. """ # Shut down the rate limiter before shutting down the request objects! if self.rate_limiter: await self.rate_limiter.shutdown() await super().shutdown() @classmethod def _merge_api_rl_kwargs( cls, api_kwargs: Optional[JSONDict], rate_limit_args: Optional[RLARGS] ) -> Optional[JSONDict]: """Inserts the `rate_limit_args` into `api_kwargs` with the special key `__RL_KEY` so that we can extract them later without having to modify the `telegram.Bot` class. """ if not rate_limit_args: return api_kwargs if api_kwargs is None: api_kwargs = {} api_kwargs[cls.__RL_KEY] = rate_limit_args return api_kwargs @classmethod def _extract_rl_kwargs(cls, data: Optional[JSONDict]) -> Optional[RLARGS]: """Extracts the `rate_limit_args` from `data` if it exists.""" if not data: return None return data.pop(cls.__RL_KEY, None) 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]]: """Order of method calls is: Bot.some_method -> Bot._post -> Bot._do_post. So we can override Bot._do_post to add rate limiting. """ rate_limit_args = self._extract_rl_kwargs(data) if not self.rate_limiter and rate_limit_args is not None: raise ValueError( "`rate_limit_args` can only be used if a `ExtBot.rate_limiter` is set." ) # getting updates should not be rate limited! if endpoint == "getUpdates" or not self.rate_limiter: return await super()._do_post( endpoint=endpoint, data=data, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, read_timeout=read_timeout, ) kwargs = { "read_timeout": read_timeout, "write_timeout": write_timeout, "connect_timeout": connect_timeout, "pool_timeout": pool_timeout, } self._LOGGER.debug( "Passing request through rate limiter of type %s with rate_limit_args %s", type(self.rate_limiter), rate_limit_args, ) return await self.rate_limiter.process_request( callback=super()._do_post, args=(endpoint, data), kwargs=kwargs, endpoint=endpoint, data=data, rate_limit_args=rate_limit_args, ) @property def defaults(self) -> Optional["Defaults"]: """The :class:`telegram.ext.Defaults` used by this bot, if any.""" # This is a property because defaults shouldn't be changed at runtime return self._defaults @property def rate_limiter(self) -> Optional["BaseRateLimiter[RLARGS]"]: """The :class:`telegram.ext.BaseRateLimiter` used by this bot, if any. .. versionadded:: 20.0 """ # This is a property because the rate limiter shouldn't be changed at runtime return self._rate_limiter def _merge_lpo_defaults( self, lpo: ODVInput[LinkPreviewOptions] ) -> Optional[LinkPreviewOptions]: # This is a standalone method because both _insert_defaults and # _insert_defaults_for_ilq_results need this logic # # If Defaults.LPO is set, and LPO is passed in the bot method we should fuse # them, giving precedence to passed values. # Defaults.LPO(True, "google.com", True) & LPO=LPO(True, ..., False) -> # LPO(True, "google.com", False) if self.defaults is None or (defaults_lpo := self.defaults.link_preview_options) is None: return DefaultValue.get_value(lpo) return LinkPreviewOptions( **{ attr: ( getattr(defaults_lpo, attr) # only use the default value # if the value was explicitly passed to the LPO object if isinstance(orig_attr := getattr(lpo, attr), DefaultValue) else orig_attr ) for attr in defaults_lpo.__slots__ } ) def _insert_defaults(self, data: Dict[str, object]) -> None: """Inserts the defaults values for optional kwargs for which tg.ext.Defaults provides convenience functionality, i.e. the kwargs with a tg.utils.helpers.DefaultValue default data is edited in-place. As timeout is not passed via the kwargs, it needs to be passed separately and gets returned. This can only work, if all kwargs that may have defaults are passed in data! """ if self.defaults is None: # If we have no defaults to insert, the behavior is the same as in `tg.Bot` super()._insert_defaults(data) return # if we have Defaults, we # 1) replace all DefaultValue instances with the relevant Defaults value. If there is none, # we fall back to the default value of the bot method # 2) convert all datetime.datetime objects to timestamps wrt the correct default timezone # 3) set the correct parse_mode for all InputMedia objects # 4) handle the LinkPreviewOptions case (see below) # 5) handle the ReplyParameters case (see below) for key, val in data.items(): # 1) if isinstance(val, DefaultValue): data[key] = self.defaults.api_defaults.get(key, val.value) # 2) elif isinstance(val, datetime): data[key] = to_timestamp(val, tzinfo=self.defaults.tzinfo) # 3) elif isinstance(val, InputMedia) and val.parse_mode is DEFAULT_NONE: # Copy object as not to edit it in-place copied_val = copy(val) with copied_val._unfrozen(): copied_val.parse_mode = self.defaults.parse_mode data[key] = copied_val elif key == "media" and isinstance(val, Sequence): # Copy objects as not to edit them in-place copy_list = [copy(media) for media in val] for media in copy_list: if media.parse_mode is DEFAULT_NONE: with media._unfrozen(): media.parse_mode = self.defaults.parse_mode data[key] = copy_list # 4) LinkPreviewOptions: elif isinstance(val, LinkPreviewOptions): data[key] = self._merge_lpo_defaults(val) # 5) # Similar to LinkPreviewOptions, but only two of the arguments of RPs have a default elif isinstance(val, ReplyParameters) and ( (defaults_aswr := self.defaults.allow_sending_without_reply) is not None or self.defaults.quote_parse_mode is not None ): new_value = copy(val) with new_value._unfrozen(): new_value.allow_sending_without_reply = ( defaults_aswr if isinstance(val.allow_sending_without_reply, DefaultValue) else val.allow_sending_without_reply ) new_value.quote_parse_mode = ( self.defaults.quote_parse_mode if isinstance(val.quote_parse_mode, DefaultValue) else val.quote_parse_mode ) data[key] = new_value def _replace_keyboard(self, reply_markup: Optional[KT]) -> Optional[KT]: # If the reply_markup is an inline keyboard and we allow arbitrary callback data, let the # CallbackDataCache build a new keyboard with the data replaced. Otherwise return the input if isinstance(reply_markup, InlineKeyboardMarkup) and self.callback_data_cache is not None: # for some reason mypy doesn't understand that IKB is a subtype of Optional[KT] return self.callback_data_cache.process_keyboard( # type: ignore[return-value] reply_markup ) return reply_markup def insert_callback_data(self, update: Update) -> None: """If this bot allows for arbitrary callback data, this inserts the cached data into all corresponding buttons within this update. Note: Checks :attr:`telegram.Message.via_bot` and :attr:`telegram.Message.from_user` to figure out if a) a reply markup exists and b) it was actually sent by this bot. If not, the message will be returned unchanged. Note that this will fail for channel posts, as :attr:`telegram.Message.from_user` is :obj:`None` for those! In the corresponding reply markups, the callback data will be replaced by :class:`telegram.ext.InvalidCallbackData`. Warning: *In place*, i.e. the passed :class:`telegram.Message` will be changed! Args: update (:class:`telegram.Update`): The update. """ # The only incoming updates that can directly contain a message sent by the bot itself are: # * CallbackQueries # * Messages where the pinned_message is sent by the bot # * Messages where the reply_to_message is sent by the bot # * Messages where via_bot is the bot # Finally there is effective_chat.pinned message, but that's only returned in get_chat if update.callback_query: self._insert_callback_data(update.callback_query) # elif instead of if, as effective_message includes callback_query.message # and that has already been processed elif update.effective_message: self._insert_callback_data(update.effective_message) def _insert_callback_data(self, obj: HandledTypes) -> HandledTypes: if self.callback_data_cache is None: return obj if isinstance(obj, CallbackQuery): self.callback_data_cache.process_callback_query(obj) return obj # type: ignore[return-value] if isinstance(obj, Message): if obj.reply_to_message: # reply_to_message can't contain further reply_to_messages, so no need to check self.callback_data_cache.process_message(obj.reply_to_message) if isinstance(obj.reply_to_message.pinned_message, Message): # pinned messages can't contain reply_to_message, no need to check self.callback_data_cache.process_message(obj.reply_to_message.pinned_message) if isinstance(obj.pinned_message, Message): # pinned messages can't contain reply_to_message, no need to check self.callback_data_cache.process_message(obj.pinned_message) # Finally, handle the message itself self.callback_data_cache.process_message(message=obj) return obj # type: ignore[return-value] if isinstance(obj, Chat) and obj.pinned_message: self.callback_data_cache.process_message(obj.pinned_message) return obj 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, *, 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, ) -> Any: # We override this method to call self._replace_keyboard and self._insert_callback_data. # This covers most methods that have a reply_markup result = await super()._send_message( endpoint=endpoint, data=data, reply_to_message_id=reply_to_message_id, disable_notification=disable_notification, reply_markup=self._replace_keyboard(reply_markup), allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, link_preview_options=link_preview_options, 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, ) if isinstance(result, Message): self._insert_callback_data(result) return result async def get_updates( self, offset: Optional[int] = None, limit: Optional[int] = None, timeout: Optional[int] = None, 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, ...]: updates = await super().get_updates( offset=offset, limit=limit, timeout=timeout, allowed_updates=allowed_updates, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) for update in updates: self.insert_callback_data(update) return updates 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]]: """This method is called by Bot.answer_inline_query to build the actual results list. Overriding this to call self._replace_keyboard suffices """ effective_results, next_offset = super()._effective_inline_results( results=results, next_offset=next_offset, current_offset=current_offset ) # Process arbitrary callback if self.callback_data_cache is None: return effective_results, next_offset results = [] for result in effective_results: # All currently existingInlineQueryResults have a reply_markup, but future ones # might not have. Better be save than sorry if not hasattr(result, "reply_markup"): results.append(result) else: # We build a new result in case the user wants to use the same object in # different places new_result = copy(result) with new_result._unfrozen(): markup = self._replace_keyboard(result.reply_markup) new_result.reply_markup = markup results.append(new_result) return results, next_offset @no_type_check # mypy doesn't play too well with hasattr def _insert_defaults_for_ilq_results(self, res: "InlineQueryResult") -> "InlineQueryResult": """This method is called by Bot.answer_inline_query to replace `DefaultValue(obj)` with `obj`. Overriding this to call insert the actual desired default values. """ if self.defaults is None: # If we have no defaults to insert, the behavior is the same as in `tg.Bot` return super()._insert_defaults_for_ilq_results(res) # Copy the objects that need modification to avoid modifying the original object copied = False if hasattr(res, "parse_mode") and res.parse_mode is DEFAULT_NONE: res = copy(res) with res._unfrozen(): copied = True res.parse_mode = self.defaults.parse_mode if hasattr(res, "input_message_content") and res.input_message_content: if ( hasattr(res.input_message_content, "parse_mode") and res.input_message_content.parse_mode is DEFAULT_NONE ): if not copied: res = copy(res) copied = True with res.input_message_content._unfrozen(): res.input_message_content.parse_mode = self.defaults.parse_mode if hasattr(res.input_message_content, "link_preview_options"): if not copied: res = copy(res) with res.input_message_content._unfrozen(): if res.input_message_content.link_preview_options is DEFAULT_NONE: res.input_message_content.link_preview_options = ( self.defaults.link_preview_options ) else: # merge the existing options with the defaults res.input_message_content.link_preview_options = self._merge_lpo_defaults( res.input_message_content.link_preview_options ) return res 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, rate_limit_args: Optional[RLARGS] = None, ) -> Any: return await super().do_api_request( endpoint=endpoint, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), return_type=return_type, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, ) async def stop_poll( self, chat_id: Union[int, str], message_id: int, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Poll: # We override this method to call self._replace_keyboard return await super().stop_poll( chat_id=chat_id, message_id=message_id, reply_markup=self._replace_keyboard(reply_markup), read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> MessageId: # We override this method to call self._replace_keyboard return await super().copy_message( chat_id=chat_id, from_chat_id=from_chat_id, message_id=message_id, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=self._replace_keyboard(reply_markup), 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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> Tuple["MessageId", ...]: # We override this method to call self._replace_keyboard return await super().copy_messages( 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, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> Chat: # We override this method to call self._insert_callback_data result = await super().get_chat( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) return self._insert_callback_data(result) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().add_sticker_to_set( user_id=user_id, name=name, sticker=sticker, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().answer_callback_query( callback_query_id=callback_query_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().answer_inline_query( inline_query_id=inline_query_id, results=results, cache_time=cache_time, is_personal=is_personal, next_offset=next_offset, current_offset=current_offset, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, button=button, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().answer_pre_checkout_query( pre_checkout_query_id=pre_checkout_query_id, ok=ok, error_message=error_message, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().answer_shipping_query( shipping_query_id=shipping_query_id, ok=ok, shipping_options=shipping_options, error_message=error_message, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> SentWebAppMessage: return await super().answer_web_app_query( web_app_query_id=web_app_query_id, result=result, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().approve_chat_join_request( chat_id=chat_id, user_id=user_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().ban_chat_member( chat_id=chat_id, user_id=user_id, until_date=until_date, revoke_messages=revoke_messages, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().ban_chat_sender_chat( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> ChatInviteLink: return await super().create_chat_invite_link( chat_id=chat_id, expire_date=expire_date, member_limit=member_limit, name=name, creates_join_request=creates_join_request, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) async def create_invoice_link( self, title: str, description: str, payload: str, provider_token: str, 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, rate_limit_args: Optional[RLARGS] = None, ) -> str: return await super().create_invoice_link( 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, send_phone_number_to_provider=send_phone_number_to_provider, send_email_to_provider=send_email_to_provider, is_flexible=is_flexible, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) async def create_new_sticker_set( self, user_id: int, name: str, title: str, stickers: Sequence["InputSticker"], sticker_format: Optional[str] = None, 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().create_new_sticker_set( user_id=user_id, name=name, title=title, stickers=stickers, sticker_format=sticker_format, sticker_type=sticker_type, needs_repainting=needs_repainting, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().decline_chat_join_request( chat_id=chat_id, user_id=user_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().delete_chat_photo( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().delete_chat_sticker_set( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().delete_forum_topic( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().delete_message( chat_id=chat_id, message_id=message_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) async def delete_messages( self, chat_id: Union[str, int], 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().delete_messages( chat_id=chat_id, message_ids=message_ids, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().delete_my_commands( scope=scope, language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().delete_sticker_from_set( sticker=sticker, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().delete_webhook( drop_pending_updates=drop_pending_updates, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> ChatInviteLink: return await super().edit_chat_invite_link( chat_id=chat_id, invite_link=invite_link, expire_date=expire_date, member_limit=member_limit, name=name, creates_join_request=creates_join_request, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().edit_forum_topic( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().edit_general_forum_topic( chat_id=chat_id, name=name, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: Optional[JSONDict] = None, rate_limit_args: Optional[RLARGS] = None, ) -> Union[Message, bool]: return await super().edit_message_caption( chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, caption=caption, reply_markup=reply_markup, parse_mode=parse_mode, caption_entities=caption_entities, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Union[Message, bool]: return await super().edit_message_live_location( chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, latitude=latitude, longitude=longitude, reply_markup=reply_markup, horizontal_accuracy=horizontal_accuracy, heading=heading, proximity_alert_radius=proximity_alert_radius, location=location, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: Optional[JSONDict] = None, rate_limit_args: Optional[RLARGS] = None, ) -> Union[Message, bool]: return await super().edit_message_media( media=media, chat_id=chat_id, message_id=message_id, inline_message_id=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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: Optional[JSONDict] = None, rate_limit_args: Optional[RLARGS] = None, ) -> Union[Message, bool]: return await super().edit_message_reply_markup( chat_id=chat_id, message_id=message_id, inline_message_id=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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Union[Message, bool]: return await super().edit_message_text( text=text, chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, parse_mode=parse_mode, disable_web_page_preview=disable_web_page_preview, reply_markup=reply_markup, entities=entities, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), link_preview_options=link_preview_options, ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> str: return await super().export_chat_invite_link( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().forward_message( chat_id=chat_id, from_chat_id=from_chat_id, message_id=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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> Tuple[MessageId, ...]: return await super().forward_messages( 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, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> Tuple[ChatMember, ...]: return await super().get_chat_administrators( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> ChatMember: return await super().get_chat_member( chat_id=chat_id, user_id=user_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> int: return await super().get_chat_member_count( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> MenuButton: return await super().get_chat_menu_button( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> File: return await super().get_file( file_id=file_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> Tuple[Sticker, ...]: return await super().get_forum_topic_icon_stickers( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> Tuple[GameHighScore, ...]: return await super().get_game_high_scores( user_id=user_id, chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> User: return await super().get_me( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> Tuple[BotCommand, ...]: return await super().get_my_commands( scope=scope, language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> ChatAdministratorRights: return await super().get_my_default_administrator_rights( for_channels=for_channels, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> StickerSet: return await super().get_sticker_set( name=name, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> Tuple[Sticker, ...]: return await super().get_custom_emoji_stickers( custom_emoji_ids=custom_emoji_ids, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> UserProfilePhotos: return await super().get_user_profile_photos( user_id=user_id, offset=offset, limit=limit, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> WebhookInfo: return await super().get_webhook_info( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().leave_chat( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().log_out( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().close( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().close_forum_topic( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().close_general_forum_topic( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> ForumTopic: return await super().create_forum_topic( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().reopen_general_forum_topic( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().hide_general_forum_topic( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().unhide_general_forum_topic( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) async def pin_chat_message( self, chat_id: Union[str, int], message_id: int, 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().pin_chat_message( 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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().promote_chat_member( chat_id=chat_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, 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, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().reopen_forum_topic( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().restrict_chat_member( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> ChatInviteLink: return await super().revoke_chat_invite_link( chat_id=chat_id, invite_link=invite_link, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().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_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, has_spoiler=has_spoiler, thumbnail=thumbnail, reply_parameters=reply_parameters, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, business_connection_id=business_connection_id, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_audio( chat_id=chat_id, audio=audio, duration=duration, performer=performer, business_connection_id=business_connection_id, title=title, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, parse_mode=parse_mode, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, reply_parameters=reply_parameters, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().send_chat_action( chat_id=chat_id, business_connection_id=business_connection_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_contact( chat_id=chat_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_markup=reply_markup, vcard=vcard, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, reply_parameters=reply_parameters, contact=contact, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, business_connection_id=business_connection_id, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_dice( chat_id=chat_id, disable_notification=disable_notification, business_connection_id=business_connection_id, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, emoji=emoji, 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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_document( chat_id=chat_id, document=document, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, parse_mode=parse_mode, disable_content_type_detection=disable_content_type_detection, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, business_connection_id=business_connection_id, message_thread_id=message_thread_id, thumbnail=thumbnail, reply_parameters=reply_parameters, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_game( chat_id=chat_id, game_short_name=game_short_name, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, business_connection_id=business_connection_id, 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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) async def send_invoice( self, chat_id: Union[int, str], title: str, description: str, payload: str, provider_token: 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().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_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, 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, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_location( chat_id=chat_id, latitude=latitude, longitude=longitude, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, live_period=live_period, 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, reply_parameters=reply_parameters, location=location, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, business_connection_id=business_connection_id, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, caption: Optional[str] = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Optional[Sequence["MessageEntity"]] = None, ) -> Tuple[Message, ...]: return await super().send_media_group( chat_id=chat_id, media=media, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, 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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), caption=caption, business_connection_id=business_connection_id, parse_mode=parse_mode, caption_entities=caption_entities, ) 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, *, disable_web_page_preview: 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_message( chat_id=chat_id, text=text, parse_mode=parse_mode, entities=entities, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, business_connection_id=business_connection_id, protect_content=protect_content, message_thread_id=message_thread_id, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, reply_parameters=reply_parameters, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), link_preview_options=link_preview_options, ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_photo( chat_id=chat_id, photo=photo, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, parse_mode=parse_mode, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, has_spoiler=has_spoiler, reply_parameters=reply_parameters, filename=filename, business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) async def send_poll( self, chat_id: Union[int, str], question: str, options: Sequence[str], 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().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_to_message_id=reply_to_message_id, reply_markup=reply_markup, explanation=explanation, explanation_parse_mode=explanation_parse_mode, open_period=open_period, close_date=close_date, allow_sending_without_reply=allow_sending_without_reply, explanation_entities=explanation_entities, business_connection_id=business_connection_id, 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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_sticker( chat_id=chat_id, sticker=sticker, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, business_connection_id=business_connection_id, 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, emoji=emoji, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_venue( chat_id=chat_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_markup=reply_markup, foursquare_type=foursquare_type, google_place_id=google_place_id, google_place_type=google_place_type, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, business_connection_id=business_connection_id, message_thread_id=message_thread_id, reply_parameters=reply_parameters, venue=venue, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_video( chat_id=chat_id, video=video, duration=duration, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, width=width, height=height, parse_mode=parse_mode, supports_streaming=supports_streaming, 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, has_spoiler=has_spoiler, thumbnail=thumbnail, filename=filename, reply_parameters=reply_parameters, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_video_note( chat_id=chat_id, video_note=video_note, duration=duration, length=length, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, message_thread_id=message_thread_id, thumbnail=thumbnail, reply_parameters=reply_parameters, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), business_connection_id=business_connection_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, *, 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, rate_limit_args: Optional[RLARGS] = None, ) -> Message: return await super().send_voice( chat_id=chat_id, voice=voice, duration=duration, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, parse_mode=parse_mode, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, message_thread_id=message_thread_id, reply_parameters=reply_parameters, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), business_connection_id=business_connection_id, ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_chat_administrator_custom_title( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_chat_description( chat_id=chat_id, description=description, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_chat_menu_button( chat_id=chat_id, menu_button=menu_button, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_chat_permissions( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_chat_photo( chat_id=chat_id, photo=photo, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_chat_sticker_set( chat_id=chat_id, sticker_set_name=sticker_set_name, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_chat_title( chat_id=chat_id, title=title, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> Union[Message, bool]: return await super().set_game_score( user_id=user_id, score=score, chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, 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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_my_commands( commands=commands, scope=scope, language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_my_default_administrator_rights( rights=rights, for_channels=for_channels, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_passport_data_errors( user_id=user_id, errors=errors, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_sticker_position_in_set( sticker=sticker, position=position, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_sticker_set_thumbnail( name=name, user_id=user_id, thumbnail=thumbnail, format=format, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_webhook( url=url, certificate=certificate, max_connections=max_connections, allowed_updates=allowed_updates, ip_address=ip_address, drop_pending_updates=drop_pending_updates, secret_token=secret_token, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: Optional[JSONDict] = None, rate_limit_args: Optional[RLARGS] = None, ) -> Union[Message, bool]: return await super().stop_message_live_location( chat_id=chat_id, message_id=message_id, inline_message_id=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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().unban_chat_member( chat_id=chat_id, user_id=user_id, only_if_banned=only_if_banned, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().unban_chat_sender_chat( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().unpin_all_chat_messages( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) async def unpin_chat_message( self, chat_id: Union[str, int], 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().unpin_chat_message( chat_id=chat_id, message_id=message_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().unpin_all_forum_topic_messages( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().unpin_all_general_forum_topic_messages( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> File: return await super().upload_sticker_file( user_id=user_id, sticker=sticker, sticker_format=sticker_format, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_my_description( description=description, language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_my_short_description( short_description=short_description, language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> BotDescription: return await super().get_my_description( language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> BotShortDescription: return await super().get_my_short_description( language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_my_name( name=name, language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> BotName: return await super().get_my_name( language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_custom_emoji_sticker_set_thumbnail( name=name, custom_emoji_id=custom_emoji_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_sticker_set_title( name=name, title=title, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().delete_sticker_set( name=name, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_sticker_emoji_list( sticker=sticker, emoji_list=emoji_list, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_sticker_keywords( sticker=sticker, keywords=keywords, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_sticker_mask_position( sticker=sticker, mask_position=mask_position, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> UserChatBoosts: return await super().get_user_chat_boosts( chat_id=chat_id, user_id=user_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().set_message_reaction( chat_id=chat_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=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> BusinessConnection: return await super().get_business_connection( business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) 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, rate_limit_args: Optional[RLARGS] = None, ) -> bool: return await super().replace_sticker_in_set( user_id=user_id, name=name, old_sticker=old_sticker, sticker=sticker, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) # updated camelCase aliases getMe = get_me sendMessage = send_message deleteMessage = delete_message deleteMessages = delete_messages forwardMessage = forward_message forwardMessages = forward_messages sendPhoto = send_photo sendAudio = send_audio sendDocument = send_document sendSticker = send_sticker sendVideo = send_video sendAnimation = send_animation sendVoice = send_voice sendVideoNote = send_video_note sendMediaGroup = send_media_group sendLocation = send_location editMessageLiveLocation = edit_message_live_location stopMessageLiveLocation = stop_message_live_location sendVenue = send_venue sendContact = send_contact sendGame = send_game sendChatAction = send_chat_action answerInlineQuery = answer_inline_query getUserProfilePhotos = get_user_profile_photos getFile = get_file banChatMember = ban_chat_member banChatSenderChat = ban_chat_sender_chat unbanChatMember = unban_chat_member unbanChatSenderChat = unban_chat_sender_chat answerCallbackQuery = answer_callback_query editMessageText = edit_message_text editMessageCaption = edit_message_caption editMessageMedia = edit_message_media editMessageReplyMarkup = edit_message_reply_markup getUpdates = get_updates setWebhook = set_webhook deleteWebhook = delete_webhook leaveChat = leave_chat getChat = get_chat getChatAdministrators = get_chat_administrators getChatMember = get_chat_member setChatStickerSet = set_chat_sticker_set deleteChatStickerSet = delete_chat_sticker_set getChatMemberCount = get_chat_member_count getWebhookInfo = get_webhook_info setGameScore = set_game_score getGameHighScores = get_game_high_scores sendInvoice = send_invoice answerShippingQuery = answer_shipping_query answerPreCheckoutQuery = answer_pre_checkout_query answerWebAppQuery = answer_web_app_query restrictChatMember = restrict_chat_member promoteChatMember = promote_chat_member setChatPermissions = set_chat_permissions setChatAdministratorCustomTitle = set_chat_administrator_custom_title exportChatInviteLink = export_chat_invite_link createChatInviteLink = create_chat_invite_link editChatInviteLink = edit_chat_invite_link revokeChatInviteLink = revoke_chat_invite_link approveChatJoinRequest = approve_chat_join_request declineChatJoinRequest = decline_chat_join_request setChatPhoto = set_chat_photo deleteChatPhoto = delete_chat_photo setChatTitle = set_chat_title setChatDescription = set_chat_description pinChatMessage = pin_chat_message unpinChatMessage = unpin_chat_message unpinAllChatMessages = unpin_all_chat_messages getStickerSet = get_sticker_set getCustomEmojiStickers = get_custom_emoji_stickers uploadStickerFile = upload_sticker_file createNewStickerSet = create_new_sticker_set addStickerToSet = add_sticker_to_set setStickerPositionInSet = set_sticker_position_in_set deleteStickerFromSet = delete_sticker_from_set setStickerSetThumbnail = set_sticker_set_thumbnail setPassportDataErrors = set_passport_data_errors sendPoll = send_poll stopPoll = stop_poll sendDice = send_dice getMyCommands = get_my_commands setMyCommands = set_my_commands deleteMyCommands = delete_my_commands logOut = log_out copyMessage = copy_message copyMessages = copy_messages getChatMenuButton = get_chat_menu_button setChatMenuButton = set_chat_menu_button getMyDefaultAdministratorRights = get_my_default_administrator_rights setMyDefaultAdministratorRights = set_my_default_administrator_rights createInvoiceLink = create_invoice_link getForumTopicIconStickers = get_forum_topic_icon_stickers createForumTopic = create_forum_topic editForumTopic = edit_forum_topic closeForumTopic = close_forum_topic reopenForumTopic = reopen_forum_topic deleteForumTopic = delete_forum_topic unpinAllForumTopicMessages = unpin_all_forum_topic_messages editGeneralForumTopic = edit_general_forum_topic closeGeneralForumTopic = close_general_forum_topic reopenGeneralForumTopic = reopen_general_forum_topic hideGeneralForumTopic = hide_general_forum_topic unhideGeneralForumTopic = unhide_general_forum_topic setMyDescription = set_my_description getMyDescription = get_my_description setMyShortDescription = set_my_short_description getMyShortDescription = get_my_short_description setCustomEmojiStickerSetThumbnail = set_custom_emoji_sticker_set_thumbnail setStickerSetTitle = set_sticker_set_title deleteStickerSet = delete_sticker_set setStickerEmojiList = set_sticker_emoji_list setStickerKeywords = set_sticker_keywords setStickerMaskPosition = set_sticker_mask_position setMyName = set_my_name getMyName = get_my_name unpinAllGeneralForumTopicMessages = unpin_all_general_forum_topic_messages getUserChatBoosts = get_user_chat_boosts setMessageReaction = set_message_reaction getBusinessConnection = get_business_connection replaceStickerInSet = replace_sticker_in_set python-telegram-bot-21.1.1/telegram/ext/_handlers/000077500000000000000000000000001460724040100220325ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/ext/_handlers/__init__.py000066400000000000000000000000001460724040100241310ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/ext/_handlers/basehandler.py000066400000000000000000000161011460724040100246530ustar00rootroot00000000000000#!/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 base class for handlers as used by the Application.""" from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, Union from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.repr import build_repr_with_selected_attrs from telegram._utils.types import DVType from telegram.ext._utils.types import CCT, HandlerCallback if TYPE_CHECKING: from telegram.ext import Application RT = TypeVar("RT") UT = TypeVar("UT") class BaseHandler(Generic[UT, CCT], ABC): """The base class for all update handlers. Create custom handlers by inheriting from it. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. This class is a :class:`~typing.Generic` class and accepts two type variables: 1. The type of the updates that this handler will handle. Must coincide with the type of the first argument of :paramref:`callback`. :meth:`check_update` must only accept updates of this type. 2. The type of the second argument of :paramref:`callback`. Must coincide with the type of the parameters :paramref:`handle_update.context` and :paramref:`collect_additional_context.context` as well as the second argument of :paramref:`callback`. Must be either :class:`~telegram.ext.CallbackContext` or a subclass of that class. .. tip:: For this type variable, one should usually provide a :class:`~typing.TypeVar` that is also used for the mentioned method arguments. That way, a type checker can check whether this handler fits the definition of the :class:`~Application`. .. seealso:: :wiki:`Types of Handlers ` .. versionchanged:: 20.0 * The attribute ``run_async`` is now :paramref:`block`. * This class was previously named ``Handler``. Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the callback will run in a blocking way. """ __slots__ = ( "block", "callback", ) def __init__( self, callback: HandlerCallback[UT, CCT, RT], block: DVType[bool] = DEFAULT_TRUE, ): self.callback: HandlerCallback[UT, CCT, RT] = callback self.block: DVType[bool] = block def __repr__(self) -> str: """Give a string representation of the handler in the form ``ClassName[callback=...]``. As this class doesn't implement :meth:`object.__str__`, the default implementation will be used, which is equivalent to :meth:`__repr__`. Returns: :obj:`str` """ try: callback_name = self.callback.__qualname__ except AttributeError: callback_name = repr(self.callback) return build_repr_with_selected_attrs(self, callback=callback_name) @abstractmethod def check_update(self, update: object) -> Optional[Union[bool, object]]: """ This method is called to determine if an update should be handled by this handler instance. It should always be overridden. Note: Custom updates types can be handled by the application. Therefore, an implementation of this method should always check the type of :paramref:`update`. Args: update (:obj:`object` | :class:`telegram.Update`): The update to be tested. Returns: Either :obj:`None` or :obj:`False` if the update should not be handled. Otherwise an object that will be passed to :meth:`handle_update` and :meth:`collect_additional_context` when the update gets handled. """ async def handle_update( self, update: UT, application: "Application[Any, CCT, Any, Any, Any, Any]", check_result: object, context: CCT, ) -> RT: """ This method is called if it was determined that an update should indeed be handled by this instance. Calls :attr:`callback` along with its respectful arguments. To work with the :class:`telegram.ext.ConversationHandler`, this method returns the value returned from :attr:`callback`. Note that it can be overridden if needed by the subclassing handler. Args: update (:obj:`str` | :class:`telegram.Update`): The update to be handled. application (:class:`telegram.ext.Application`): The calling application. check_result (:class:`object`): The result from :meth:`check_update`. context (:class:`telegram.ext.CallbackContext`): The context as provided by the application. """ self.collect_additional_context(context, update, application, check_result) return await self.callback(update, context) def collect_additional_context( self, context: CCT, update: UT, application: "Application[Any, CCT, Any, Any, Any, Any]", check_result: Any, ) -> None: """Prepares additional arguments for the context. Override if needed. Args: context (:class:`telegram.ext.CallbackContext`): The context object. update (:class:`telegram.Update`): The update to gather chat/user id from. application (:class:`telegram.ext.Application`): The calling application. check_result: The result (return value) from :meth:`check_update`. """ python-telegram-bot-21.1.1/telegram/ext/_handlers/businessconnectionhandler.py000066400000000000000000000073321460724040100276620ustar00rootroot00000000000000#!/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 BusinessConnectionHandler class.""" from typing import Optional, TypeVar from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import SCT, DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils._update_parsing import parse_chat_id, parse_username from telegram.ext._utils.types import CCT, HandlerCallback RT = TypeVar("RT") class BusinessConnectionHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram :attr:`Business Connections `. .. versionadded:: 21.1 Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only those which are from the specified user ID(s). username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only those which are from the specified username(s). block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. """ __slots__ = ( "_user_ids", "_usernames", ) def __init__( self, callback: HandlerCallback[Update, CCT, RT], user_id: Optional[SCT[int]] = None, username: Optional[SCT[str]] = None, block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback, block=block) self._user_ids = parse_chat_id(user_id) self._usernames = parse_username(username) def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if isinstance(update, Update) and update.business_connection: if not self._user_ids and not self._usernames: return True if update.business_connection.user.id in self._user_ids: return True return update.business_connection.user.username in self._usernames return False python-telegram-bot-21.1.1/telegram/ext/_handlers/businessmessagesdeletedhandler.py000066400000000000000000000074011460724040100306560ustar00rootroot00000000000000#!/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 BusinessMessagesDeletedHandler class.""" from typing import Optional, TypeVar from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import SCT, DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils._update_parsing import parse_chat_id, parse_username from telegram.ext._utils.types import CCT, HandlerCallback RT = TypeVar("RT") class BusinessMessagesDeletedHandler(BaseHandler[Update, CCT]): """Handler class to handle :attr:`deleted Telegram Business messages `. .. versionadded:: 21.1 Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only those which are from the specified chat ID(s). username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only those which are from the specified username(s). block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. """ __slots__ = ( "_chat_ids", "_usernames", ) def __init__( self, callback: HandlerCallback[Update, CCT, RT], chat_id: Optional[SCT[int]] = None, username: Optional[SCT[str]] = None, block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback, block=block) self._chat_ids = parse_chat_id(chat_id) self._usernames = parse_username(username) def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if isinstance(update, Update) and update.deleted_business_messages: if not self._chat_ids and not self._usernames: return True if update.deleted_business_messages.chat.id in self._chat_ids: return True return update.deleted_business_messages.chat.username in self._usernames return False python-telegram-bot-21.1.1/telegram/ext/_handlers/callbackqueryhandler.py000066400000000000000000000164261460724040100265750ustar00rootroot00000000000000#!/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 CallbackQueryHandler class.""" import asyncio import re from typing import TYPE_CHECKING, Any, Callable, Match, Optional, Pattern, TypeVar, Union, cast from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT, HandlerCallback if TYPE_CHECKING: from telegram.ext import Application RT = TypeVar("RT") class CallbackQueryHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram :attr:`callback queries `. Optionally based on a regex. Read the documentation of the :mod:`re` module for more information. Note: * If your bot allows arbitrary objects as :paramref:`~telegram.InlineKeyboardButton.callback_data`, it may happen that the original :attr:`~telegram.InlineKeyboardButton.callback_data` for the incoming :class:`telegram.CallbackQuery` can not be found. This is the case when either a malicious client tempered with the :attr:`telegram.CallbackQuery.data` or the data was simply dropped from cache or not persisted. In these cases, an instance of :class:`telegram.ext.InvalidCallbackData` will be set as :attr:`telegram.CallbackQuery.data`. .. versionadded:: 13.6 Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. pattern (:obj:`str` | :func:`re.Pattern ` | :obj:`callable` | :obj:`type`, \ optional): Pattern to test :attr:`telegram.CallbackQuery.data` against. If a string or a regex pattern is passed, :func:`re.match` is used on :attr:`telegram.CallbackQuery.data` to determine if an update should be handled by this handler. If your bot allows arbitrary objects as :paramref:`~telegram.InlineKeyboardButton.callback_data`, non-strings will be accepted. To filter arbitrary objects you may pass: - a callable, accepting exactly one argument, namely the :attr:`telegram.CallbackQuery.data`. It must return :obj:`True` or :obj:`False`/:obj:`None` to indicate, whether the update should be handled. - a :obj:`type`. If :attr:`telegram.CallbackQuery.data` is an instance of that type (or a subclass), the update will be handled. If :attr:`telegram.CallbackQuery.data` is :obj:`None`, the :class:`telegram.CallbackQuery` update will not be handled. .. seealso:: :wiki:`Arbitrary callback_data ` .. versionchanged:: 13.6 Added support for arbitrary callback data. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: callback (:term:`coroutine function`): The callback function for this handler. pattern (:func:`re.Pattern ` | :obj:`callable` | :obj:`type`): Optional. Regex pattern, callback or type to test :attr:`telegram.CallbackQuery.data` against. .. versionchanged:: 13.6 Added support for arbitrary callback data. block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. """ __slots__ = ("pattern",) def __init__( self, callback: HandlerCallback[Update, CCT, RT], pattern: Optional[ Union[str, Pattern[str], type, Callable[[object], Optional[bool]]] ] = None, block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback, block=block) if callable(pattern) and asyncio.iscoroutinefunction(pattern): raise TypeError( "The `pattern` must not be a coroutine function! Use an ordinary function instead." ) if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern: Optional[ Union[str, Pattern[str], type, Callable[[object], Optional[bool]]] ] = pattern def check_update(self, update: object) -> Optional[Union[bool, object]]: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ # pylint: disable=too-many-return-statements if isinstance(update, Update) and update.callback_query: callback_data = update.callback_query.data if self.pattern: if callback_data is None: return False if isinstance(self.pattern, type): return isinstance(callback_data, self.pattern) if callable(self.pattern): return self.pattern(callback_data) if not isinstance(callback_data, str): return False if match := re.match(self.pattern, callback_data): return match else: return True return None def collect_additional_context( self, context: CCT, update: Update, application: "Application[Any, CCT, Any, Any, Any, Any]", check_result: Union[bool, Match[str]], ) -> None: """Add the result of ``re.match(pattern, update.callback_query.data)`` to :attr:`CallbackContext.matches` as list with one element. """ if self.pattern: check_result = cast(Match, check_result) context.matches = [check_result] python-telegram-bot-21.1.1/telegram/ext/_handlers/chatboosthandler.py000066400000000000000000000126031460724040100257320ustar00rootroot00000000000000#!/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 ChatBoostHandler class.""" from typing import Final, Optional from telegram import Update from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils._update_parsing import parse_chat_id, parse_username from telegram.ext._utils.types import CCT, HandlerCallback class ChatBoostHandler(BaseHandler[Update, CCT]): """ Handler class to handle Telegram updates that contain a chat boost. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. .. versionadded:: 20.8 Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. chat_boost_types (:obj:`int`, optional): Pass one of :attr:`CHAT_BOOST`, :attr:`REMOVED_CHAT_BOOST` or :attr:`ANY_CHAT_BOOST` to specify if this handler should handle only updates with :attr:`telegram.Update.chat_boost`, :attr:`telegram.Update.removed_chat_boost` or both. Defaults to :attr:`CHAT_BOOST`. chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters reactions to allow only those which happen in the specified chat ID(s). chat_username (:obj:`str` | Collection[:obj:`str`], optional): Filters reactions to allow only those which happen in the specified username(s). block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: callback (:term:`coroutine function`): The callback function for this handler. chat_boost_types (:obj:`int`): Optional. Specifies if this handler should handle only updates with :attr:`telegram.Update.chat_boost`, :attr:`telegram.Update.removed_chat_boost` or both. block (:obj:`bool`): Determines whether the callback will run in a blocking way. """ __slots__ = ( "_chat_ids", "_chat_usernames", "chat_boost_types", ) CHAT_BOOST: Final[int] = -1 """ :obj:`int`: Used as a constant to handle only :attr:`telegram.Update.chat_boost`.""" REMOVED_CHAT_BOOST: Final[int] = 0 """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.removed_chat_boost`.""" ANY_CHAT_BOOST: Final[int] = 1 """:obj:`int`: Used as a constant to handle both :attr:`telegram.Update.chat_boost` and :attr:`telegram.Update.removed_chat_boost`.""" def __init__( self, callback: HandlerCallback[Update, CCT, None], chat_boost_types: int = CHAT_BOOST, chat_id: Optional[int] = None, chat_username: Optional[str] = None, block: bool = True, ): super().__init__(callback, block=block) self.chat_boost_types: int = chat_boost_types self._chat_ids = parse_chat_id(chat_id) self._chat_usernames = parse_username(chat_username) def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if not isinstance(update, Update): return False if not (update.chat_boost or update.removed_chat_boost): return False if self.chat_boost_types == self.CHAT_BOOST and not update.chat_boost: return False if self.chat_boost_types == self.REMOVED_CHAT_BOOST and not update.removed_chat_boost: return False if not any((self._chat_ids, self._chat_usernames)): return True # Extract chat and user IDs and usernames from the update for comparison chat_id = chat.id if (chat := update.effective_chat) else None chat_username = chat.username if chat else None return bool(self._chat_ids and (chat_id in self._chat_ids)) or bool( self._chat_usernames and (chat_username in self._chat_usernames) ) python-telegram-bot-21.1.1/telegram/ext/_handlers/chatjoinrequesthandler.py000066400000000000000000000105371460724040100271600ustar00rootroot00000000000000#!/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 ChatJoinRequestHandler class.""" from typing import Optional from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import RT, SCT, DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils._update_parsing import parse_chat_id, parse_username from telegram.ext._utils.types import CCT, HandlerCallback class ChatJoinRequestHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram updates that contain :attr:`telegram.Update.chat_join_request`. Note: If neither of :paramref:`username` and the :paramref:`chat_id` are passed, this handler accepts *any* join request. Otherwise, this handler accepts all requests to join chats for which the chat ID is listed in :paramref:`chat_id` or the username is listed in :paramref:`username`, or both. .. versionadded:: 20.0 Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. .. versionadded:: 13.8 Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only those which are asking to join the specified chat ID(s). .. versionadded:: 20.0 username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only those which are asking to join the specified username(s). .. versionadded:: 20.0 block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the callback will run in a blocking way.. """ __slots__ = ( "_chat_ids", "_usernames", ) def __init__( self, callback: HandlerCallback[Update, CCT, RT], chat_id: Optional[SCT[int]] = None, username: Optional[SCT[str]] = None, block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback, block=block) self._chat_ids = parse_chat_id(chat_id) self._usernames = parse_username(username) def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if isinstance(update, Update) and update.chat_join_request: if not self._chat_ids and not self._usernames: return True if update.chat_join_request.chat.id in self._chat_ids: return True return update.chat_join_request.from_user.username in self._usernames return False python-telegram-bot-21.1.1/telegram/ext/_handlers/chatmemberhandler.py000066400000000000000000000111671460724040100260570ustar00rootroot00000000000000#!/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 ChatMemberHandler class.""" from typing import Final, Optional, TypeVar from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT, HandlerCallback RT = TypeVar("RT") class ChatMemberHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram updates that contain a chat member update. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Examples: :any:`Chat Member Bot ` .. versionadded:: 13.4 Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. chat_member_types (:obj:`int`, optional): Pass one of :attr:`MY_CHAT_MEMBER`, :attr:`CHAT_MEMBER` or :attr:`ANY_CHAT_MEMBER` to specify if this handler should handle only updates with :attr:`telegram.Update.my_chat_member`, :attr:`telegram.Update.chat_member` or both. Defaults to :attr:`MY_CHAT_MEMBER`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: callback (:term:`coroutine function`): The callback function for this handler. chat_member_types (:obj:`int`): Optional. Specifies if this handler should handle only updates with :attr:`telegram.Update.my_chat_member`, :attr:`telegram.Update.chat_member` or both. block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. """ __slots__ = ("chat_member_types",) MY_CHAT_MEMBER: Final[int] = -1 """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.my_chat_member`.""" CHAT_MEMBER: Final[int] = 0 """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.chat_member`.""" ANY_CHAT_MEMBER: Final[int] = 1 """:obj:`int`: Used as a constant to handle both :attr:`telegram.Update.my_chat_member` and :attr:`telegram.Update.chat_member`.""" def __init__( self, callback: HandlerCallback[Update, CCT, RT], chat_member_types: int = MY_CHAT_MEMBER, block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback, block=block) self.chat_member_types: Optional[int] = chat_member_types def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if isinstance(update, Update): if not (update.my_chat_member or update.chat_member): return False if self.chat_member_types == self.ANY_CHAT_MEMBER: return True if self.chat_member_types == self.CHAT_MEMBER: return bool(update.chat_member) return bool(update.my_chat_member) return False python-telegram-bot-21.1.1/telegram/ext/_handlers/choseninlineresulthandler.py000066400000000000000000000113671460724040100276670ustar00rootroot00000000000000#!/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 ChosenInlineResultHandler class.""" import re from typing import TYPE_CHECKING, Any, Match, Optional, Pattern, TypeVar, Union, cast from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT, HandlerCallback RT = TypeVar("RT") if TYPE_CHECKING: from telegram.ext import Application class ChosenInlineResultHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram updates that contain :attr:`telegram.Update.chosen_inline_result`. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` pattern (:obj:`str` | :func:`re.Pattern `, optional): Regex pattern. If not :obj:`None`, :func:`re.match` is used on :attr:`telegram.ChosenInlineResult.result_id` to determine if an update should be handled by this handler. This is accessible in the callback as :attr:`telegram.ext.CallbackContext.matches`. .. versionadded:: 13.6 Attributes: callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. pattern (`Pattern`): Optional. Regex pattern to test :attr:`telegram.ChosenInlineResult.result_id` against. .. versionadded:: 13.6 """ __slots__ = ("pattern",) def __init__( self, callback: HandlerCallback[Update, CCT, RT], block: DVType[bool] = DEFAULT_TRUE, pattern: Optional[Union[str, Pattern[str]]] = None, ): super().__init__(callback, block=block) if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern: Optional[Union[str, Pattern[str]]] = pattern def check_update(self, update: object) -> Optional[Union[bool, object]]: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` | :obj:`re.match` """ if isinstance(update, Update) and update.chosen_inline_result: if self.pattern: if match := re.match(self.pattern, update.chosen_inline_result.result_id): return match else: return True return None def collect_additional_context( self, context: CCT, update: Update, application: "Application[Any, CCT, Any, Any, Any, Any]", check_result: Union[bool, Match[str]], ) -> None: """This function adds the matched regex pattern result to :attr:`telegram.ext.CallbackContext.matches`. """ if self.pattern: check_result = cast(Match, check_result) context.matches = [check_result] python-telegram-bot-21.1.1/telegram/ext/_handlers/commandhandler.py000066400000000000000000000231211460724040100253570ustar00rootroot00000000000000#!/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 CommandHandler class.""" import re from typing import TYPE_CHECKING, Any, FrozenSet, List, Optional, Tuple, TypeVar, Union from telegram import MessageEntity, Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import SCT, DVType from telegram.ext import filters as filters_module from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT, FilterDataDict, HandlerCallback if TYPE_CHECKING: from telegram.ext import Application RT = TypeVar("RT") class CommandHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram commands. Commands are Telegram messages that start with ``/``, optionally followed by an ``@`` and the bot's name and/or some additional text. The handler will add a :obj:`list` to the :class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings, which is the text following the command split on single or consecutive whitespace characters. By default, the handler listens to messages as well as edited messages. To change this behavior use :attr:`~filters.UpdateType.EDITED_MESSAGE ` in the filter argument. Note: :class:`CommandHandler` does *not* handle (edited) channel posts and does *not* handle commands that are part of a caption. Please use :class:`~telegram.ext.MessageHandler` with a suitable combination of filters (e.g. :attr:`telegram.ext.filters.UpdateType.CHANNEL_POSTS`, :attr:`telegram.ext.filters.CAPTION` and :class:`telegram.ext.filters.Regex`) to handle those messages. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Examples: * :any:`Timer Bot ` * :any:`Error Handler Bot ` .. versionchanged:: 20.0 * Renamed the attribute ``command`` to :attr:`commands`, which now is always a :class:`frozenset` * Updating the commands this handler listens to is no longer possible. Args: command (:obj:`str` | Collection[:obj:`str`]): The command or list of commands this handler should listen for. Case-insensitive. Limitations are the same as for :attr:`telegram.BotCommand.command`. callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. filters (:class:`telegram.ext.filters.BaseFilter`, optional): A filter inheriting from :class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in :mod:`telegram.ext.filters`. Filters can be combined using bitwise operators (``&`` for :keyword:`and`, ``|`` for :keyword:`or`, ``~`` for :keyword:`not`) block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` has_args (:obj:`bool` | :obj:`int`, optional): Determines whether the command handler should process the update or not. If :obj:`True`, the handler will process any non-zero number of args. If :obj:`False`, the handler will only process if there are no args. if :obj:`int`, the handler will only process if there are exactly that many args. Defaults to :obj:`None`, which means the handler will process any or no args. .. versionadded:: 20.5 Raises: :exc:`ValueError`: When the command is too long or has illegal chars. Attributes: commands (FrozenSet[:obj:`str`]): The set of commands this handler should listen for. callback (:term:`coroutine function`): The callback function for this handler. filters (:class:`telegram.ext.filters.BaseFilter`): Optional. Only allow updates with these filters. block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. has_args (:obj:`bool` | :obj:`int` | None): Optional argument, otherwise all implementations of :class:`CommandHandler` will break. Defaults to :obj:`None`, which means the handler will process any args or no args. .. versionadded:: 20.5 """ __slots__ = ("commands", "filters", "has_args") def __init__( self, command: SCT[str], callback: HandlerCallback[Update, CCT, RT], filters: Optional[filters_module.BaseFilter] = None, block: DVType[bool] = DEFAULT_TRUE, has_args: Optional[Union[bool, int]] = None, ): super().__init__(callback, block=block) if isinstance(command, str): commands = frozenset({command.lower()}) else: commands = frozenset(x.lower() for x in command) for comm in commands: if not re.match(r"^[\da-z_]{1,32}$", comm): raise ValueError(f"Command `{comm}` is not a valid bot command") self.commands: FrozenSet[str] = commands self.filters: filters_module.BaseFilter = ( filters if filters is not None else filters_module.UpdateType.MESSAGES ) self.has_args: Optional[Union[bool, int]] = has_args if (isinstance(self.has_args, int)) and (self.has_args < 0): raise ValueError("CommandHandler argument has_args cannot be a negative integer") def _check_correct_args(self, args: List[str]) -> Optional[bool]: """Determines whether the args are correct for this handler. Implemented in check_update(). Args: args (:obj:`list`): The args for the handler. Returns: :obj:`bool`: Whether the args are valid for this handler. """ # pylint: disable=too-many-boolean-expressions return bool( (self.has_args is None) or (self.has_args is True and args) or (self.has_args is False and not args) or (isinstance(self.has_args, int) and len(args) == self.has_args) ) def check_update( self, update: object ) -> Optional[Union[bool, Tuple[List[str], Optional[Union[bool, FilterDataDict]]]]]: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`list`: The list of args for the handler. """ if isinstance(update, Update) and update.effective_message: message = update.effective_message if ( message.entities and message.entities[0].type == MessageEntity.BOT_COMMAND and message.entities[0].offset == 0 and message.text and message.get_bot() ): command = message.text[1 : message.entities[0].length] args = message.text.split()[1:] command_parts = command.split("@") command_parts.append(message.get_bot().username) if not ( command_parts[0].lower() in self.commands and command_parts[1].lower() == message.get_bot().username.lower() ): return None if not self._check_correct_args(args): return None filter_result = self.filters.check_update(update) if filter_result: return args, filter_result return False return None def collect_additional_context( self, context: CCT, update: Update, application: "Application[Any, CCT, Any, Any, Any, Any]", check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]], ) -> None: """Add text after the command to :attr:`CallbackContext.args` as list, split on single whitespaces and add output of data filters to :attr:`CallbackContext` as well. """ if isinstance(check_result, tuple): context.args = check_result[0] if isinstance(check_result[1], dict): context.update(check_result[1]) python-telegram-bot-21.1.1/telegram/ext/_handlers/conversationhandler.py000066400000000000000000001231111460724040100264530ustar00rootroot00000000000000#!/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 ConversationHandler.""" import asyncio import datetime from dataclasses import dataclass from typing import ( TYPE_CHECKING, Any, Dict, Final, Generic, List, NoReturn, Optional, Set, Tuple, Union, cast, ) from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE, DefaultValue from telegram._utils.logging import get_logger from telegram._utils.repr import build_repr_with_selected_attrs from telegram._utils.types import DVType from telegram._utils.warnings import warn from telegram.ext._application import ApplicationHandlerStop from telegram.ext._extbot import ExtBot from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._handlers.callbackqueryhandler import CallbackQueryHandler from telegram.ext._handlers.choseninlineresulthandler import ChosenInlineResultHandler from telegram.ext._handlers.inlinequeryhandler import InlineQueryHandler from telegram.ext._handlers.stringcommandhandler import StringCommandHandler from telegram.ext._handlers.stringregexhandler import StringRegexHandler from telegram.ext._handlers.typehandler import TypeHandler from telegram.ext._utils.trackingdict import TrackingDict from telegram.ext._utils.types import CCT, ConversationDict, ConversationKey if TYPE_CHECKING: from telegram.ext import Application, Job, JobQueue _CheckUpdateType = Tuple[object, ConversationKey, BaseHandler[Update, CCT], object] _LOGGER = get_logger(__name__, class_name="ConversationHandler") @dataclass class _ConversationTimeoutContext(Generic[CCT]): """Used as a datastore for conversation timeouts. Passed in the :paramref:`JobQueue.run_once.data` parameter. See :meth:`_trigger_timeout`. """ __slots__ = ("application", "callback_context", "conversation_key", "update") conversation_key: ConversationKey update: Update application: "Application[Any, CCT, Any, Any, Any, JobQueue]" callback_context: CCT @dataclass class PendingState: """Thin wrapper around :class:`asyncio.Task` to handle block=False handlers. Note that this is a public class of this module, since :meth:`Application.update_persistence` needs to access it. It's still hidden from users, since this module itself is private. """ __slots__ = ("old_state", "task") task: asyncio.Task old_state: object def done(self) -> bool: return self.task.done() def resolve(self) -> object: """Returns the new state of the :class:`ConversationHandler` if available. If there was an exception during the task execution, then return the old state. If both the new and old state are :obj:`None`, return `CH.END`. If only the new state is :obj:`None`, return the old state. Raises: :exc:`RuntimeError`: If the current task has not yet finished. """ if not self.task.done(): raise RuntimeError("New state is not yet available") exc = self.task.exception() if exc: _LOGGER.exception( "Task function raised exception. Falling back to old state %s", self.old_state, ) return self.old_state res = self.task.result() if res is None and self.old_state is None: res = ConversationHandler.END elif res is None: # returning None from a callback means that we want to stay in the old state return self.old_state return res class ConversationHandler(BaseHandler[Update, CCT]): """ A handler to hold a conversation with a single or multiple users through Telegram updates by managing three collections of other handlers. Warning: :class:`ConversationHandler` heavily relies on incoming updates being processed one by one. When using this handler, :attr:`telegram.ext.ApplicationBuilder.concurrent_updates` should be set to :obj:`False`. Note: :class:`ConversationHandler` will only accept updates that are (subclass-)instances of :class:`telegram.Update`. This is, because depending on the :attr:`per_user` and :attr:`per_chat`, :class:`ConversationHandler` relies on :attr:`telegram.Update.effective_user` and/or :attr:`telegram.Update.effective_chat` in order to determine which conversation an update should belong to. For :attr:`per_message=True `, :class:`ConversationHandler` uses :attr:`update.callback_query.message.message_id ` when :attr:`per_chat=True ` and :attr:`update.callback_query.inline_message_id <.CallbackQuery.inline_message_id>` when :attr:`per_chat=False `. For a more detailed explanation, please see our `FAQ`_. Finally, :class:`ConversationHandler`, does *not* handle (edited) channel posts. .. _`FAQ`: https://github.com/python-telegram-bot/python-telegram-bot/wiki\ /Frequently-Asked-Questions#what-do-the-per_-settings-in-conversation handler-do The first collection, a :obj:`list` named :attr:`entry_points`, is used to initiate the conversation, for example with a :class:`telegram.ext.CommandHandler` or :class:`telegram.ext.MessageHandler`. The second collection, a :obj:`dict` named :attr:`states`, contains the different conversation steps and one or more associated handlers that should be used if the user sends a message when the conversation with them is currently in that state. Here you can also define a state for :attr:`TIMEOUT` to define the behavior when :attr:`conversation_timeout` is exceeded, and a state for :attr:`WAITING` to define behavior when a new update is received while the previous :attr:`block=False ` handler is not finished. The third collection, a :obj:`list` named :attr:`fallbacks`, is used if the user is currently in a conversation but the state has either no associated handler or the handler that is associated to the state is inappropriate for the update, for example if the update contains a command, but a regular text message is expected. You could use this for a ``/cancel`` command or to let the user know their message was not recognized. To change the state of conversation, the callback function of a handler must return the new state after responding to the user. If it does not return anything (returning :obj:`None` by default), the state will not change. If an entry point callback function returns :obj:`None`, the conversation ends immediately after the execution of this callback function. To end the conversation, the callback function must return :attr:`END` or ``-1``. To handle the conversation timeout, use handler :attr:`TIMEOUT` or ``-2``. Finally, :class:`telegram.ext.ApplicationHandlerStop` can be used in conversations as described in its documentation. Note: In each of the described collections of handlers, a handler may in turn be a :class:`ConversationHandler`. In that case, the child :class:`ConversationHandler` should have the attribute :attr:`map_to_parent` which allows returning to the parent conversation at specified states within the child conversation. Note that the keys in :attr:`map_to_parent` must not appear as keys in :attr:`states` attribute or else the latter will be ignored. You may map :attr:`END` to one of the parents states to continue the parent conversation after the child conversation has ended or even map a state to :attr:`END` to end the *parent* conversation from within the child conversation. For an example on nested :class:`ConversationHandler` s, see :any:`examples.nestedconversationbot`. Examples: * :any:`Conversation Bot ` * :any:`Conversation Bot 2 ` * :any:`Nested Conversation Bot ` * :any:`Persistent Conversation Bot ` Args: entry_points (List[:class:`telegram.ext.BaseHandler`]): A list of :obj:`BaseHandler` objects that can trigger the start of the conversation. The first handler whose :meth:`check_update` method returns :obj:`True` will be used. If all return :obj:`False`, the update is not handled. states (Dict[:obj:`object`, List[:class:`telegram.ext.BaseHandler`]]): A :obj:`dict` that defines the different states of conversation a user can be in and one or more associated :obj:`BaseHandler` objects that should be used in that state. The first handler whose :meth:`check_update` method returns :obj:`True` will be used. fallbacks (List[:class:`telegram.ext.BaseHandler`]): A list of handlers that might be used if the user is in a conversation, but every handler for their current state returned :obj:`False` on :meth:`check_update`. The first handler which :meth:`check_update` method returns :obj:`True` will be used. If all return :obj:`False`, the update is not handled. allow_reentry (:obj:`bool`, optional): If set to :obj:`True`, a user that is currently in a conversation can restart the conversation by triggering one of the entry points. Default is :obj:`False`. per_chat (:obj:`bool`, optional): If the conversation key should contain the Chat's ID. Default is :obj:`True`. per_user (:obj:`bool`, optional): If the conversation key should contain the User's ID. Default is :obj:`True`. per_message (:obj:`bool`, optional): If the conversation key should contain the Message's ID. Default is :obj:`False`. conversation_timeout (:obj:`float` | :obj:`datetime.timedelta`, optional): When this handler is inactive more than this timeout (in seconds), it will be automatically ended. If this value is ``0`` or :obj:`None` (default), there will be no timeout. The last received update and the corresponding :class:`context <.CallbackContext>` will be handled by *ALL* the handler's whose :meth:`check_update` method returns :obj:`True` that are in the state :attr:`ConversationHandler.TIMEOUT`. Caution: * This feature relies on the :attr:`telegram.ext.Application.job_queue` being set and hence requires that the dependencies that :class:`telegram.ext.JobQueue` relies on are installed. * Using :paramref:`conversation_timeout` with nested conversations is currently not supported. You can still try to use it, but it will likely behave differently from what you expect. name (:obj:`str`, optional): The name for this conversation handler. Required for persistence. persistent (:obj:`bool`, optional): If the conversation's dict for this handler should be saved. :paramref:`name` is required and persistence has to be set in :attr:`Application <.Application.persistence>`. .. versionchanged:: 20.0 Was previously named as ``persistence``. map_to_parent (Dict[:obj:`object`, :obj:`object`], optional): A :obj:`dict` that can be used to instruct a child conversation handler to transition into a mapped state on its parent conversation handler in place of a specified nested state. block (:obj:`bool`, optional): Pass :obj:`False` or :obj:`True` to set a default value for the :attr:`BaseHandler.block` setting of all handlers (in :attr:`entry_points`, :attr:`states` and :attr:`fallbacks`). The resolution order for checking if a handler should be run non-blocking is: 1. :attr:`telegram.ext.BaseHandler.block` (if set) 2. the value passed to this parameter (if any) 3. :attr:`telegram.ext.Defaults.block` (if defaults are used) .. seealso:: :wiki:`Concurrency` .. versionchanged:: 20.0 No longer overrides the handlers settings. Resolution order was changed. Raises: :exc:`ValueError`: If :paramref:`persistent` is used but :paramref:`name` was not set, or when :attr:`per_message`, :attr:`per_chat`, :attr:`per_user` are all :obj:`False`. Attributes: block (:obj:`bool`): Determines whether the callback will run in a blocking way. Always :obj:`True` since conversation handlers handle any non-blocking callbacks internally. """ __slots__ = ( "_allow_reentry", "_block", "_child_conversations", "_conversation_timeout", "_conversations", "_entry_points", "_fallbacks", "_map_to_parent", "_name", "_per_chat", "_per_message", "_per_user", "_persistent", "_states", "_timeout_jobs_lock", "timeout_jobs", ) END: Final[int] = -1 """:obj:`int`: Used as a constant to return when a conversation is ended.""" TIMEOUT: Final[int] = -2 """:obj:`int`: Used as a constant to handle state when a conversation is timed out (exceeded :attr:`conversation_timeout`). """ WAITING: Final[int] = -3 """:obj:`int`: Used as a constant to handle state when a conversation is still waiting on the previous :attr:`block=False ` handler to finish.""" # pylint: disable=super-init-not-called def __init__( self, entry_points: List[BaseHandler[Update, CCT]], states: Dict[object, List[BaseHandler[Update, CCT]]], fallbacks: List[BaseHandler[Update, CCT]], allow_reentry: bool = False, per_chat: bool = True, per_user: bool = True, per_message: bool = False, conversation_timeout: Optional[Union[float, datetime.timedelta]] = None, name: Optional[str] = None, persistent: bool = False, map_to_parent: Optional[Dict[object, object]] = None, block: DVType[bool] = DEFAULT_TRUE, ): # these imports need to be here because of circular import error otherwise from telegram.ext import ( # pylint: disable=import-outside-toplevel PollAnswerHandler, PollHandler, PreCheckoutQueryHandler, ShippingQueryHandler, ) # self.block is what the Application checks and we want it to always run CH in a blocking # way so that CH can take care of any non-blocking logic internally self.block: DVType[bool] = True # Store the actual setting in a protected variable instead self._block: DVType[bool] = block self._entry_points: List[BaseHandler[Update, CCT]] = entry_points self._states: Dict[object, List[BaseHandler[Update, CCT]]] = states self._fallbacks: List[BaseHandler[Update, CCT]] = fallbacks self._allow_reentry: bool = allow_reentry self._per_user: bool = per_user self._per_chat: bool = per_chat self._per_message: bool = per_message self._conversation_timeout: Optional[Union[float, datetime.timedelta]] = ( conversation_timeout ) self._name: Optional[str] = name self._map_to_parent: Optional[Dict[object, object]] = map_to_parent # if conversation_timeout is used, this dict is used to schedule a job which runs when the # conv has timed out. self.timeout_jobs: Dict[ConversationKey, Job[Any]] = {} self._timeout_jobs_lock = asyncio.Lock() self._conversations: ConversationDict = {} self._child_conversations: Set[ConversationHandler] = set() if persistent and not self.name: raise ValueError("Conversations can't be persistent when handler is unnamed.") self._persistent: bool = persistent if not any((self.per_user, self.per_chat, self.per_message)): raise ValueError("'per_user', 'per_chat' and 'per_message' can't all be 'False'") if self.per_message and not self.per_chat: warn( "If 'per_message=True' is used, 'per_chat=True' should also be used, " "since message IDs are not globally unique.", stacklevel=2, ) all_handlers: List[BaseHandler[Update, CCT]] = [] all_handlers.extend(entry_points) all_handlers.extend(fallbacks) for state_handlers in states.values(): all_handlers.extend(state_handlers) self._child_conversations.update( handler for handler in all_handlers if isinstance(handler, ConversationHandler) ) # this link will be added to all warnings tied to per_* setting per_faq_link = ( " Read this FAQ entry to learn more about the per_* settings: " "https://github.com/python-telegram-bot/python-telegram-bot/wiki" "/Frequently-Asked-Questions#what-do-the-per_-settings-in-conversationhandler-do." ) # this loop is going to warn the user about handlers which can work unexpectedly # in conversations for handler in all_handlers: if isinstance(handler, (StringCommandHandler, StringRegexHandler)): warn( "The `ConversationHandler` only handles updates of type `telegram.Update`. " f"{handler.__class__.__name__} handles updates of type `str`.", stacklevel=2, ) elif isinstance(handler, TypeHandler) and not issubclass(handler.type, Update): warn( "The `ConversationHandler` only handles updates of type `telegram.Update`." f" The TypeHandler is set to handle {handler.type.__name__}.", stacklevel=2, ) elif isinstance(handler, PollHandler): warn( "PollHandler will never trigger in a conversation since it has no information " "about the chat or the user who voted in it. Do you mean the " "`PollAnswerHandler`?", stacklevel=2, ) elif self.per_chat and ( isinstance( handler, ( ShippingQueryHandler, InlineQueryHandler, ChosenInlineResultHandler, PreCheckoutQueryHandler, PollAnswerHandler, ), ) ): warn( f"Updates handled by {handler.__class__.__name__} only have information about " "the user, so this handler won't ever be triggered if `per_chat=True`." f"{per_faq_link}", stacklevel=2, ) elif self.per_message and not isinstance(handler, CallbackQueryHandler): warn( "If 'per_message=True', all entry points, state handlers, and fallbacks" " must be 'CallbackQueryHandler', since no other handlers " f"have a message context.{per_faq_link}", stacklevel=2, ) elif not self.per_message and isinstance(handler, CallbackQueryHandler): warn( "If 'per_message=False', 'CallbackQueryHandler' will not be " f"tracked for every message.{per_faq_link}", stacklevel=2, ) if self.conversation_timeout and isinstance(handler, self.__class__): warn( "Using `conversation_timeout` with nested conversations is currently not " "supported. You can still try to use it, but it will likely behave " "differently from what you expect.", stacklevel=2, ) def __repr__(self) -> str: """Give a string representation of the ConversationHandler in the form ``ConversationHandler[name=..., states={...}]``. If there are more than 3 states, only the first 3 states are listed. As this class doesn't implement :meth:`object.__str__`, the default implementation will be used, which is equivalent to :meth:`__repr__`. Returns: :obj:`str` """ truncation_threshold = 3 states = dict(list(self.states.items())[:truncation_threshold]) states_string = str(states) if len(self.states) > truncation_threshold: states_string = states_string[:-1] + ", ...}" return build_repr_with_selected_attrs( self, name=self.name, states=states_string, ) @property def entry_points(self) -> List[BaseHandler[Update, CCT]]: """List[:class:`telegram.ext.BaseHandler`]: A list of :obj:`BaseHandler` objects that can trigger the start of the conversation. """ return self._entry_points @entry_points.setter def entry_points(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to entry_points after initialization." ) @property def states(self) -> Dict[object, List[BaseHandler[Update, CCT]]]: """Dict[:obj:`object`, List[:class:`telegram.ext.BaseHandler`]]: A :obj:`dict` that defines the different states of conversation a user can be in and one or more associated :obj:`BaseHandler` objects that should be used in that state. """ return self._states @states.setter def states(self, value: object) -> NoReturn: raise AttributeError("You can not assign a new value to states after initialization.") @property def fallbacks(self) -> List[BaseHandler[Update, CCT]]: """List[:class:`telegram.ext.BaseHandler`]: A list of handlers that might be used if the user is in a conversation, but every handler for their current state returned :obj:`False` on :meth:`check_update`. """ return self._fallbacks @fallbacks.setter def fallbacks(self, value: object) -> NoReturn: raise AttributeError("You can not assign a new value to fallbacks after initialization.") @property def allow_reentry(self) -> bool: """:obj:`bool`: Determines if a user can restart a conversation with an entry point.""" return self._allow_reentry @allow_reentry.setter def allow_reentry(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to allow_reentry after initialization." ) @property def per_user(self) -> bool: """:obj:`bool`: If the conversation key should contain the User's ID.""" return self._per_user @per_user.setter def per_user(self, value: object) -> NoReturn: raise AttributeError("You can not assign a new value to per_user after initialization.") @property def per_chat(self) -> bool: """:obj:`bool`: If the conversation key should contain the Chat's ID.""" return self._per_chat @per_chat.setter def per_chat(self, value: object) -> NoReturn: raise AttributeError("You can not assign a new value to per_chat after initialization.") @property def per_message(self) -> bool: """:obj:`bool`: If the conversation key should contain the message's ID.""" return self._per_message @per_message.setter def per_message(self, value: object) -> NoReturn: raise AttributeError("You can not assign a new value to per_message after initialization.") @property def conversation_timeout( self, ) -> Optional[Union[float, datetime.timedelta]]: """:obj:`float` | :obj:`datetime.timedelta`: Optional. When this handler is inactive more than this timeout (in seconds), it will be automatically ended. """ return self._conversation_timeout @conversation_timeout.setter def conversation_timeout(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to conversation_timeout after initialization." ) @property def name(self) -> Optional[str]: """:obj:`str`: Optional. The name for this :class:`ConversationHandler`.""" return self._name @name.setter def name(self, value: object) -> NoReturn: raise AttributeError("You can not assign a new value to name after initialization.") @property def persistent(self) -> bool: """:obj:`bool`: Optional. If the conversations dict for this handler should be saved. :attr:`name` is required and persistence has to be set in :attr:`Application <.Application.persistence>`. """ return self._persistent @persistent.setter def persistent(self, value: object) -> NoReturn: raise AttributeError("You can not assign a new value to persistent after initialization.") @property def map_to_parent(self) -> Optional[Dict[object, object]]: """Dict[:obj:`object`, :obj:`object`]: Optional. A :obj:`dict` that can be used to instruct a nested :class:`ConversationHandler` to transition into a mapped state on its parent :class:`ConversationHandler` in place of a specified nested state. """ return self._map_to_parent @map_to_parent.setter def map_to_parent(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to map_to_parent after initialization." ) async def _initialize_persistence( self, application: "Application" ) -> Dict[str, TrackingDict[ConversationKey, object]]: """Initializes the persistence for this handler and its child conversations. While this method is marked as protected, we expect it to be called by the Application/parent conversations. It's just protected to hide it from users. Args: application (:class:`telegram.ext.Application`): The application. Returns: A dict {conversation.name -> TrackingDict}, which contains all dict of this conversation and possible child conversations. """ if not (self.persistent and self.name and application.persistence): raise RuntimeError( "This handler is not persistent, has no name or the application has no " "persistence!" ) current_conversations = self._conversations self._conversations = cast( TrackingDict[ConversationKey, object], TrackingDict(), ) # In the conversation already processed updates self._conversations.update(current_conversations) # above might be partly overridden but that's okay since we warn about that in # add_handler stored_data = await application.persistence.get_conversations(self.name) self._conversations.update_no_track(stored_data) # Since CH.END is stored as normal state, we need to properly parse it here in order to # actually end the conversation, i.e. delete the key from the _conversations dict # This also makes sure that these entries are deleted from the persisted data on the next # run of Application.update_persistence for key, state in stored_data.items(): if state == self.END: self._update_state(new_state=self.END, key=key) out = {self.name: self._conversations} for handler in self._child_conversations: out.update( await handler._initialize_persistence( # pylint: disable=protected-access application=application ) ) return out def _get_key(self, update: Update) -> ConversationKey: """Builds the conversation key associated with the update.""" chat = update.effective_chat user = update.effective_user key: List[Union[int, str]] = [] if self.per_chat: if chat is None: raise RuntimeError("Can't build key for update without effective chat!") key.append(chat.id) if self.per_user: if user is None: raise RuntimeError("Can't build key for update without effective user!") key.append(user.id) if self.per_message: if update.callback_query is None: raise RuntimeError("Can't build key for update without CallbackQuery!") if update.callback_query.inline_message_id: key.append(update.callback_query.inline_message_id) else: key.append(update.callback_query.message.message_id) # type: ignore[union-attr] return tuple(key) async def _schedule_job_delayed( self, new_state: asyncio.Task, application: "Application[Any, CCT, Any, Any, Any, JobQueue]", update: Update, context: CCT, conversation_key: ConversationKey, ) -> None: try: effective_new_state = await new_state except Exception as exc: _LOGGER.debug( "Non-blocking handler callback raised exception. Not scheduling conversation " "timeout.", exc_info=exc, ) return None return self._schedule_job( new_state=effective_new_state, application=application, update=update, context=context, conversation_key=conversation_key, ) def _schedule_job( self, new_state: object, application: "Application[Any, CCT, Any, Any, Any, JobQueue]", update: Update, context: CCT, conversation_key: ConversationKey, ) -> None: """Schedules a job which executes :meth:`_trigger_timeout` upon conversation timeout.""" if new_state == self.END: return try: # both job_queue & conversation_timeout are checked before calling _schedule_job j_queue = application.job_queue self.timeout_jobs[conversation_key] = j_queue.run_once( # type: ignore[union-attr] self._trigger_timeout, self.conversation_timeout, # type: ignore[arg-type] data=_ConversationTimeoutContext(conversation_key, update, application, context), ) except Exception as exc: _LOGGER.exception("Failed to schedule timeout.", exc_info=exc) # pylint: disable=too-many-return-statements def check_update(self, update: object) -> Optional[_CheckUpdateType[CCT]]: """ Determines whether an update should be handled by this conversation handler, and if so in which state the conversation currently is. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if not isinstance(update, Update): return None # Ignore messages in channels if update.channel_post or update.edited_channel_post: return None if self.per_chat and not update.effective_chat: return None if self.per_user and not update.effective_user: return None if self.per_message and not update.callback_query: return None if update.callback_query and self.per_chat and not update.callback_query.message: return None key = self._get_key(update) state = self._conversations.get(key) check: Optional[object] = None # Resolve futures if isinstance(state, PendingState): _LOGGER.debug("Waiting for asyncio Task to finish ...") # check if future is finished or not if state.done(): res = state.resolve() # Special case if an error was raised in a non-blocking entry-point if state.old_state is None and state.task.exception(): self._conversations.pop(key, None) state = None else: self._update_state(res, key) state = self._conversations.get(key) # if not then handle WAITING state instead else: handlers = self.states.get(self.WAITING, []) for handler_ in handlers: check = handler_.check_update(update) if check is not None and check is not False: return self.WAITING, key, handler_, check return None _LOGGER.debug("Selecting conversation %s with state %s", str(key), str(state)) handler: Optional[BaseHandler] = None # Search entry points for a match if state is None or self.allow_reentry: for entry_point in self.entry_points: check = entry_point.check_update(update) if check is not None and check is not False: handler = entry_point break else: if state is None: return None # Get the handler list for current state, if we didn't find one yet and we're still here if state is not None and handler is None: for candidate in self.states.get(state, []): check = candidate.check_update(update) if check is not None and check is not False: handler = candidate break # Find a fallback handler if all other handlers fail else: for fallback in self.fallbacks: check = fallback.check_update(update) if check is not None and check is not False: handler = fallback break else: return None return state, key, handler, check # type: ignore[return-value] async def handle_update( # type: ignore[override] self, update: Update, application: "Application[Any, CCT, Any, Any, Any, Any]", check_result: _CheckUpdateType[CCT], context: CCT, ) -> Optional[object]: """Send the update to the callback for the current state and BaseHandler Args: check_result: The result from :meth:`check_update`. For this handler it's a tuple of the conversation state, key, handler, and the handler's check result. update (:class:`telegram.Update`): Incoming telegram update. application (:class:`telegram.ext.Application`): Application that originated the update. context (:class:`telegram.ext.CallbackContext`): The context as provided by the application. """ current_state, conversation_key, handler, handler_check_result = check_result raise_dp_handler_stop = False async with self._timeout_jobs_lock: # Remove the old timeout job (if present) timeout_job = self.timeout_jobs.pop(conversation_key, None) if timeout_job is not None: timeout_job.schedule_removal() # Resolution order of "block": # 1. Setting of the selected handler # 2. Setting of the ConversationHandler # 3. Default values of the bot if handler.block is not DEFAULT_TRUE: block = handler.block elif self._block is not DEFAULT_TRUE: block = self._block elif isinstance(application.bot, ExtBot) and application.bot.defaults is not None: block = application.bot.defaults.block else: block = DefaultValue.get_value(handler.block) try: # Now create task or await the callback if block: new_state: object = await handler.handle_update( update, application, handler_check_result, context ) else: new_state = application.create_task( coroutine=handler.handle_update( update, application, handler_check_result, context ), update=update, name=f"ConversationHandler:{update.update_id}:handle_update:non_blocking_cb", ) except ApplicationHandlerStop as exception: new_state = exception.state raise_dp_handler_stop = True async with self._timeout_jobs_lock: if self.conversation_timeout: if application.job_queue is None: warn( "Ignoring `conversation_timeout` because the Application has no JobQueue.", stacklevel=1, ) elif not application.job_queue.scheduler.running: warn( "Ignoring `conversation_timeout` because the Applications JobQueue is " "not running.", stacklevel=1, ) elif isinstance(new_state, asyncio.Task): # Add the new timeout job # checking if the new state is self.END is done in _schedule_job application.create_task( self._schedule_job_delayed( new_state, application, update, context, conversation_key ), update=update, name=f"ConversationHandler:{update.update_id}:handle_update:timeout_job", ) else: self._schedule_job(new_state, application, update, context, conversation_key) if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent: self._update_state(self.END, conversation_key, handler) if raise_dp_handler_stop: raise ApplicationHandlerStop(self.map_to_parent.get(new_state)) return self.map_to_parent.get(new_state) if current_state != self.WAITING: self._update_state(new_state, conversation_key, handler) if raise_dp_handler_stop: # Don't pass the new state here. If we're in a nested conversation, the parent is # expecting None as return value. raise ApplicationHandlerStop # Signals a possible parent conversation to stay in the current state return None def _update_state( self, new_state: object, key: ConversationKey, handler: Optional[BaseHandler] = None ) -> None: if new_state == self.END: if key in self._conversations: # If there is no key in conversations, nothing is done. del self._conversations[key] elif isinstance(new_state, asyncio.Task): self._conversations[key] = PendingState( old_state=self._conversations.get(key), task=new_state ) elif new_state is not None: if new_state not in self.states: warn( f"{repr(handler.callback.__name__) if handler is not None else 'BaseHandler'} " f"returned state {new_state} which is unknown to the " f"ConversationHandler{' ' + self.name if self.name is not None else ''}.", stacklevel=2, ) self._conversations[key] = new_state async def _trigger_timeout(self, context: CCT) -> None: """This is run whenever a conversation has timed out. Also makes sure that all handlers which are in the :attr:`TIMEOUT` state and whose :meth:`BaseHandler.check_update` returns :obj:`True` is handled. """ job = cast("Job", context.job) ctxt = cast(_ConversationTimeoutContext, job.data) _LOGGER.debug( "Conversation timeout was triggered for conversation %s!", ctxt.conversation_key ) callback_context = ctxt.callback_context async with self._timeout_jobs_lock: found_job = self.timeout_jobs.get(ctxt.conversation_key) if found_job is not job: # The timeout has been cancelled in handle_update return del self.timeout_jobs[ctxt.conversation_key] # Now run all handlers which are in TIMEOUT state handlers = self.states.get(self.TIMEOUT, []) for handler in handlers: check = handler.check_update(ctxt.update) if check is not None and check is not False: try: await handler.handle_update( ctxt.update, ctxt.application, check, callback_context ) except ApplicationHandlerStop: warn( "ApplicationHandlerStop in TIMEOUT state of " "ConversationHandler has no effect. Ignoring.", stacklevel=2, ) self._update_state(self.END, ctxt.conversation_key) python-telegram-bot-21.1.1/telegram/ext/_handlers/inlinequeryhandler.py000066400000000000000000000133441460724040100263130ustar00rootroot00000000000000#!/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 InlineQueryHandler class.""" import re from typing import TYPE_CHECKING, Any, List, Match, Optional, Pattern, TypeVar, Union, cast from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT, HandlerCallback if TYPE_CHECKING: from telegram.ext import Application RT = TypeVar("RT") class InlineQueryHandler(BaseHandler[Update, CCT]): """ BaseHandler class to handle Telegram updates that contain a :attr:`telegram.Update.inline_query`. Optionally based on a regex. Read the documentation of the :mod:`re` module for more information. Warning: * When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. * :attr:`telegram.InlineQuery.chat_type` will not be set for inline queries from secret chats and may not be set for inline queries coming from third-party clients. These updates won't be handled, if :attr:`chat_types` is passed. Examples: :any:`Inline Bot ` Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. pattern (:obj:`str` | :func:`re.Pattern `, optional): Regex pattern. If not :obj:`None`, :func:`re.match` is used on :attr:`telegram.InlineQuery.query` to determine if an update should be handled by this handler. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` chat_types (List[:obj:`str`], optional): List of allowed chat types. If passed, will only handle inline queries with the appropriate :attr:`telegram.InlineQuery.chat_type`. .. versionadded:: 13.5 Attributes: callback (:term:`coroutine function`): The callback function for this handler. pattern (:obj:`str` | :func:`re.Pattern `): Optional. Regex pattern to test :attr:`telegram.InlineQuery.query` against. chat_types (List[:obj:`str`]): Optional. List of allowed chat types. .. versionadded:: 13.5 block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. """ __slots__ = ("chat_types", "pattern") def __init__( self, callback: HandlerCallback[Update, CCT, RT], pattern: Optional[Union[str, Pattern[str]]] = None, block: DVType[bool] = DEFAULT_TRUE, chat_types: Optional[List[str]] = None, ): super().__init__(callback, block=block) if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern: Optional[Union[str, Pattern[str]]] = pattern self.chat_types: Optional[List[str]] = chat_types def check_update(self, update: object) -> Optional[Union[bool, Match[str]]]: """ Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` | :obj:`re.match` """ if isinstance(update, Update) and update.inline_query: if (self.chat_types is not None) and ( update.inline_query.chat_type not in self.chat_types ): return False if ( self.pattern and update.inline_query.query and (match := re.match(self.pattern, update.inline_query.query)) ): return match if not self.pattern: return True return None def collect_additional_context( self, context: CCT, update: Update, application: "Application[Any, CCT, Any, Any, Any, Any]", check_result: Optional[Union[bool, Match[str]]], ) -> None: """Add the result of ``re.match(pattern, update.inline_query.query)`` to :attr:`CallbackContext.matches` as list with one element. """ if self.pattern: check_result = cast(Match, check_result) context.matches = [check_result] python-telegram-bot-21.1.1/telegram/ext/_handlers/messagehandler.py000066400000000000000000000111111460724040100253610ustar00rootroot00000000000000#!/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 MessageHandler class.""" from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypeVar, Union from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import DVType from telegram.ext import filters as filters_module from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT, HandlerCallback if TYPE_CHECKING: from telegram.ext import Application RT = TypeVar("RT") class MessageHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram messages. They might contain text, media or status updates. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: filters (:class:`telegram.ext.filters.BaseFilter`): A filter inheriting from :class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in :mod:`telegram.ext.filters`. Filters can be combined using bitwise operators (& for and, | for or, ~ for not). Passing :obj:`None` is a shortcut to passing :class:`telegram.ext.filters.ALL`. .. seealso:: :wiki:`Advanced Filters ` callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: filters (:class:`telegram.ext.filters.BaseFilter`): Only allow updates with these Filters. See :mod:`telegram.ext.filters` for a full list of all available filters. callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. """ __slots__ = ("filters",) def __init__( self, filters: Optional[filters_module.BaseFilter], callback: HandlerCallback[Update, CCT, RT], block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback, block=block) self.filters: filters_module.BaseFilter = ( filters if filters is not None else filters_module.ALL ) def check_update(self, update: object) -> Optional[Union[bool, Dict[str, List[Any]]]]: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if isinstance(update, Update): return self.filters.check_update(update) or False return None def collect_additional_context( self, context: CCT, update: Update, application: "Application[Any, CCT, Any, Any, Any, Any]", check_result: Optional[Union[bool, Dict[str, object]]], ) -> None: """Adds possible output of data filters to the :class:`CallbackContext`.""" if isinstance(check_result, dict): context.update(check_result) python-telegram-bot-21.1.1/telegram/ext/_handlers/messagereactionhandler.py000066400000000000000000000177101460724040100271210ustar00rootroot00000000000000#!/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 MessageReactionHandler class.""" from typing import Final, Optional from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import RT, SCT, DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils._update_parsing import parse_chat_id, parse_username from telegram.ext._utils.types import CCT, HandlerCallback class MessageReactionHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram updates that contain a message reaction. Note: The following rules apply to both ``username`` and the ``chat_id`` param groups, respectively: * If none of them are passed, the handler does not filter the update for that specific attribute. * If a chat ID **or** a username is passed, the updates will be filtered with that specific attribute. * If a chat ID **and** a username are passed, an update containing **any** of them will be filtered. * :attr:`telegram.MessageReactionUpdated.actor_chat` is *not* considered for :paramref:`user_id` and :paramref:`user_username` filtering. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. .. versionadded:: 20.8 Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. message_reaction_types (:obj:`int`, optional): Pass one of :attr:`MESSAGE_REACTION_UPDATED`, :attr:`MESSAGE_REACTION_COUNT_UPDATED` or :attr:`MESSAGE_REACTION` to specify if this handler should handle only updates with :attr:`telegram.Update.message_reaction`, :attr:`telegram.Update.message_reaction_count` or both. Defaults to :attr:`MESSAGE_REACTION`. chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters reactions to allow only those which happen in the specified chat ID(s). chat_username (:obj:`str` | Collection[:obj:`str`], optional): Filters reactions to allow only those which happen in the specified username(s). user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters reactions to allow only those which are set by the specified chat ID(s) (this can be the chat itself in the case of anonymous users, see the :paramref:`telegram.MessageReactionUpdated.actor_chat`). user_username (:obj:`str` | Collection[:obj:`str`], optional): Filters reactions to allow only those which are set by the specified username(s) (this can be the chat itself in the case of anonymous users, see the :paramref:`telegram.MessageReactionUpdated.actor_chat`). block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: callback (:term:`coroutine function`): The callback function for this handler. message_reaction_types (:obj:`int`): Optional. Specifies if this handler should handle only updates with :attr:`telegram.Update.message_reaction`, :attr:`telegram.Update.message_reaction_count` or both. block (:obj:`bool`): Determines whether the callback will run in a blocking way. """ __slots__ = ( "_chat_ids", "_chat_usernames", "_user_ids", "_user_usernames", "message_reaction_types", ) MESSAGE_REACTION_UPDATED: Final[int] = -1 """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.message_reaction`.""" MESSAGE_REACTION_COUNT_UPDATED: Final[int] = 0 """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.message_reaction_count`.""" MESSAGE_REACTION: Final[int] = 1 """:obj:`int`: Used as a constant to handle both :attr:`telegram.Update.message_reaction` and :attr:`telegram.Update.message_reaction_count`.""" def __init__( self, callback: HandlerCallback[Update, CCT, RT], chat_id: Optional[SCT[int]] = None, chat_username: Optional[SCT[str]] = None, user_id: Optional[SCT[int]] = None, user_username: Optional[SCT[str]] = None, message_reaction_types: int = MESSAGE_REACTION, block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback, block=block) self.message_reaction_types: int = message_reaction_types self._chat_ids = parse_chat_id(chat_id) self._chat_usernames = parse_username(chat_username) if (user_id or user_username) and message_reaction_types in ( self.MESSAGE_REACTION, self.MESSAGE_REACTION_COUNT_UPDATED, ): raise ValueError( "You can not filter for users and include anonymous reactions. Set " "`message_reaction_types` to MESSAGE_REACTION_UPDATED." ) self._user_ids = parse_chat_id(user_id) self._user_usernames = parse_username(user_username) def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if not isinstance(update, Update): return False if not (update.message_reaction or update.message_reaction_count): return False if ( self.message_reaction_types == self.MESSAGE_REACTION_UPDATED and update.message_reaction_count ): return False if ( self.message_reaction_types == self.MESSAGE_REACTION_COUNT_UPDATED and update.message_reaction ): return False if not any((self._chat_ids, self._chat_usernames, self._user_ids, self._user_usernames)): return True # Extract chat and user IDs and usernames from the update for comparison chat_id = chat.id if (chat := update.effective_chat) else None chat_username = chat.username if chat else None user_id = user.id if (user := update.effective_user) else None user_username = user.username if user else None return ( bool(self._chat_ids and (chat_id in self._chat_ids)) or bool(self._chat_usernames and (chat_username in self._chat_usernames)) or bool(self._user_ids and (user_id in self._user_ids)) or bool(self._user_usernames and (user_username in self._user_usernames)) ) python-telegram-bot-21.1.1/telegram/ext/_handlers/pollanswerhandler.py000066400000000000000000000053341460724040100261350ustar00rootroot00000000000000#!/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 PollAnswerHandler class.""" from telegram import Update from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT class PollAnswerHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram updates that contain a :attr:`poll answer `. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Examples: :any:`Poll Bot ` Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the callback will run in a blocking way.. """ __slots__ = () def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ return isinstance(update, Update) and bool(update.poll_answer) python-telegram-bot-21.1.1/telegram/ext/_handlers/pollhandler.py000066400000000000000000000052711460724040100247150ustar00rootroot00000000000000#!/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 PollHandler class.""" from telegram import Update from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT class PollHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram updates that contain a :attr:`poll `. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Examples: :any:`Poll Bot ` Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the callback will run in a blocking way.. """ __slots__ = () def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ return isinstance(update, Update) and bool(update.poll) python-telegram-bot-21.1.1/telegram/ext/_handlers/precheckoutqueryhandler.py000066400000000000000000000076311460724040100273530ustar00rootroot00000000000000#!/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 PreCheckoutQueryHandler class.""" import re from typing import Optional, Pattern, TypeVar, Union from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT, HandlerCallback RT = TypeVar("RT") class PreCheckoutQueryHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram :attr:`telegram.Update.pre_checkout_query`. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Examples: :any:`Payment Bot ` Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` pattern (:obj:`str` | :func:`re.Pattern `, optional): Optional. Regex pattern to test :attr:`telegram.PreCheckoutQuery.invoice_payload` against. .. versionadded:: 20.8 Attributes: callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the callback will run in a blocking way.. pattern (:obj:`str` | :func:`re.Pattern `, optional): Optional. Regex pattern to test :attr:`telegram.PreCheckoutQuery.invoice_payload` against. .. versionadded:: 20.8 """ __slots__ = ("pattern",) def __init__( self, callback: HandlerCallback[Update, CCT, RT], block: DVType[bool] = DEFAULT_TRUE, pattern: Optional[Union[str, Pattern[str]]] = None, ): super().__init__(callback, block=block) self.pattern: Optional[Pattern[str]] = re.compile(pattern) if pattern is not None else None def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if isinstance(update, Update) and update.pre_checkout_query: invoice_payload = update.pre_checkout_query.invoice_payload if self.pattern: if self.pattern.match(invoice_payload): return True else: return True return False python-telegram-bot-21.1.1/telegram/ext/_handlers/prefixhandler.py000066400000000000000000000173261460724040100252500ustar00rootroot00000000000000#!/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 PrefixHandler class.""" import itertools from typing import TYPE_CHECKING, Any, Dict, FrozenSet, List, Optional, Tuple, TypeVar, Union from telegram import Update from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import SCT, DVType from telegram.ext import filters as filters_module from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT, HandlerCallback if TYPE_CHECKING: from telegram.ext import Application RT = TypeVar("RT") class PrefixHandler(BaseHandler[Update, CCT]): """Handler class to handle custom prefix commands. This is an intermediate handler between :class:`MessageHandler` and :class:`CommandHandler`. It supports configurable commands with the same options as :class:`CommandHandler`. It will respond to every combination of :paramref:`prefix` and :paramref:`command`. It will add a :obj:`list` to the :class:`CallbackContext` named :attr:`CallbackContext.args`, containing a list of strings, which is the text following the command split on single or consecutive whitespace characters. Examples: Single prefix and command: .. code:: python PrefixHandler("!", "test", callback) # will respond to '!test'. Multiple prefixes, single command: .. code:: python PrefixHandler(["!", "#"], "test", callback) # will respond to '!test' and '#test'. Multiple prefixes and commands: .. code:: python PrefixHandler( ["!", "#"], ["test", "help"], callback ) # will respond to '!test', '#test', '!help' and '#help'. By default, the handler listens to messages as well as edited messages. To change this behavior use :attr:`~filters.UpdateType.EDITED_MESSAGE ` Note: * :class:`PrefixHandler` does *not* handle (edited) channel posts. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. .. versionchanged:: 20.0 * :class:`PrefixHandler` is no longer a subclass of :class:`CommandHandler`. * Removed the attributes ``command`` and ``prefix``. Instead, the new :attr:`commands` contains all commands that this handler listens to as a :class:`frozenset`, which includes the prefixes. * Updating the prefixes and commands this handler listens to is no longer possible. Args: prefix (:obj:`str` | Collection[:obj:`str`]): The prefix(es) that will precede :paramref:`command`. command (:obj:`str` | Collection[:obj:`str`]): The command or list of commands this handler should listen for. Case-insensitive. callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. filters (:class:`telegram.ext.filters.BaseFilter`, optional): A filter inheriting from :class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in :mod:`telegram.ext.filters`. Filters can be combined using bitwise operators (``&`` for :keyword:`and`, ``|`` for :keyword:`or`, ``~`` for :keyword:`not`) block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: commands (FrozenSet[:obj:`str`]): The commands that this handler will listen for, i.e. the combinations of :paramref:`prefix` and :paramref:`command`. callback (:term:`coroutine function`): The callback function for this handler. filters (:class:`telegram.ext.filters.BaseFilter`): Optional. Only allow updates with these Filters. block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. """ # 'prefix' is a class property, & 'command' is included in the superclass, so they're left out. __slots__ = ("commands", "filters") def __init__( self, prefix: SCT[str], command: SCT[str], callback: HandlerCallback[Update, CCT, RT], filters: Optional[filters_module.BaseFilter] = None, block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback=callback, block=block) prefixes = {prefix.lower()} if isinstance(prefix, str) else {x.lower() for x in prefix} commands = {command.lower()} if isinstance(command, str) else {x.lower() for x in command} self.commands: FrozenSet[str] = frozenset( p + c for p, c in itertools.product(prefixes, commands) ) self.filters: filters_module.BaseFilter = ( filters if filters is not None else filters_module.UpdateType.MESSAGES ) def check_update( self, update: object ) -> Optional[Union[bool, Tuple[List[str], Optional[Union[bool, Dict[Any, Any]]]]]]: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`list`: The list of args for the handler. """ if isinstance(update, Update) and update.effective_message: message = update.effective_message if message.text: text_list = message.text.split() if text_list[0].lower() not in self.commands: return None filter_result = self.filters.check_update(update) if filter_result: return text_list[1:], filter_result return False return None def collect_additional_context( self, context: CCT, update: Update, application: "Application[Any, CCT, Any, Any, Any, Any]", check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]], ) -> None: """Add text after the command to :attr:`CallbackContext.args` as list, split on single whitespaces and add output of data filters to :attr:`CallbackContext` as well. """ if isinstance(check_result, tuple): context.args = check_result[0] if isinstance(check_result[1], dict): context.update(check_result[1]) python-telegram-bot-21.1.1/telegram/ext/_handlers/shippingqueryhandler.py000066400000000000000000000053031460724040100266520ustar00rootroot00000000000000#!/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 ShippingQueryHandler class.""" from telegram import Update from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT class ShippingQueryHandler(BaseHandler[Update, CCT]): """Handler class to handle Telegram :attr:`telegram.Update.shipping_query`. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Examples: :any:`Payment Bot ` Args: callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the callback will run in a blocking way.. """ __slots__ = () def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ return isinstance(update, Update) and bool(update.shipping_query) python-telegram-bot-21.1.1/telegram/ext/_handlers/stringcommandhandler.py000066400000000000000000000105001460724040100266030ustar00rootroot00000000000000#!/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 StringCommandHandler class.""" from typing import TYPE_CHECKING, Any, List, Optional from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT, RT, HandlerCallback if TYPE_CHECKING: from telegram.ext import Application class StringCommandHandler(BaseHandler[str, CCT]): """Handler class to handle string commands. Commands are string updates that start with ``/``. The handler will add a :obj:`list` to the :class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings, which is the text following the command split on single whitespace characters. Note: This handler is not used to handle Telegram :class:`telegram.Update`, but strings manually put in the queue. For example to send messages with the bot using command line or API. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: command (:obj:`str`): The command this handler should listen for. callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: command (:obj:`str`): The command this handler should listen for. callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. """ __slots__ = ("command",) def __init__( self, command: str, callback: HandlerCallback[str, CCT, RT], block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback, block=block) self.command: str = command def check_update(self, update: object) -> Optional[List[str]]: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:obj:`object`): The incoming update. Returns: List[:obj:`str`]: List containing the text command split on whitespace. """ if isinstance(update, str) and update.startswith("/"): args = update[1:].split(" ") if args[0] == self.command: return args[1:] return None def collect_additional_context( self, context: CCT, update: str, application: "Application[Any, CCT, Any, Any, Any, Any]", check_result: Optional[List[str]], ) -> None: """Add text after the command to :attr:`CallbackContext.args` as list, split on single whitespaces. """ context.args = check_result python-telegram-bot-21.1.1/telegram/ext/_handlers/stringregexhandler.py000066400000000000000000000106301460724040100263030ustar00rootroot00000000000000#!/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 StringRegexHandler class.""" import re from typing import TYPE_CHECKING, Any, Match, Optional, Pattern, TypeVar, Union from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT, HandlerCallback if TYPE_CHECKING: from telegram.ext import Application RT = TypeVar("RT") class StringRegexHandler(BaseHandler[str, CCT]): """Handler class to handle string updates based on a regex which checks the update content. Read the documentation of the :mod:`re` module for more information. The :func:`re.match` function is used to determine if an update should be handled by this handler. Note: This handler is not used to handle Telegram :class:`telegram.Update`, but strings manually put in the queue. For example to send messages with the bot using command line or API. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: pattern (:obj:`str` | :func:`re.Pattern `): The regex pattern. callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: pattern (:obj:`str` | :func:`re.Pattern `): The regex pattern. callback (:term:`coroutine function`): The callback function for this handler. block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. """ __slots__ = ("pattern",) def __init__( self, pattern: Union[str, Pattern[str]], callback: HandlerCallback[str, CCT, RT], block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback, block=block) if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern: Union[str, Pattern[str]] = pattern def check_update(self, update: object) -> Optional[Match[str]]: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:obj:`object`): The incoming update. Returns: :obj:`None` | :obj:`re.match` """ if isinstance(update, str) and (match := re.match(self.pattern, update)): return match return None def collect_additional_context( self, context: CCT, update: str, application: "Application[Any, CCT, Any, Any, Any, Any]", check_result: Optional[Match[str]], ) -> None: """Add the result of ``re.match(pattern, update)`` to :attr:`CallbackContext.matches` as list with one element. """ if self.pattern and check_result: context.matches = [check_result] python-telegram-bot-21.1.1/telegram/ext/_handlers/typehandler.py000066400000000000000000000074371460724040100247360ustar00rootroot00000000000000#!/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 TypeHandler class.""" from typing import Optional, Type, TypeVar from telegram._utils.defaultvalue import DEFAULT_TRUE from telegram._utils.types import DVType from telegram.ext._handlers.basehandler import BaseHandler from telegram.ext._utils.types import CCT, HandlerCallback RT = TypeVar("RT") UT = TypeVar("UT") class TypeHandler(BaseHandler[UT, CCT]): """Handler class to handle updates of custom types. Warning: When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: type (:external:class:`type`): The :external:class:`type` of updates this handler should process, as determined by :obj:`isinstance` callback (:term:`coroutine function`): The callback function for this handler. Will be called when :meth:`check_update` has determined that an update should be processed by this handler. Callback signature:: async def callback(update: Update, context: CallbackContext) The return value of the callback is usually ignored except for the special case of :class:`telegram.ext.ConversationHandler`. strict (:obj:`bool`, optional): Use ``type`` instead of :obj:`isinstance`. Default is :obj:`False`. block (:obj:`bool`, optional): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`. .. seealso:: :wiki:`Concurrency` Attributes: type (:external:class:`type`): The :external:class:`type` of updates this handler should process. callback (:term:`coroutine function`): The callback function for this handler. strict (:obj:`bool`): Use :external:class:`type` instead of :obj:`isinstance`. Default is :obj:`False`. block (:obj:`bool`): Determines whether the return value of the callback should be awaited before processing the next handler in :meth:`telegram.ext.Application.process_update`. """ __slots__ = ("strict", "type") def __init__( self, type: Type[UT], # pylint: disable=redefined-builtin callback: HandlerCallback[UT, CCT, RT], strict: bool = False, block: DVType[bool] = DEFAULT_TRUE, ): super().__init__(callback, block=block) self.type: Type[UT] = type self.strict: Optional[bool] = strict def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handler's :attr:`callback`. Args: update (:obj:`object`): Incoming update. Returns: :obj:`bool` """ if not self.strict: return isinstance(update, self.type) return type(update) is self.type # pylint: disable=unidiomatic-typecheck python-telegram-bot-21.1.1/telegram/ext/_jobqueue.py000066400000000000000000001165541460724040100224370ustar00rootroot00000000000000#!/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 JobQueue and Job.""" import asyncio import datetime import weakref from typing import TYPE_CHECKING, Any, Generic, Optional, Tuple, Union, cast, overload try: import pytz from apscheduler.executors.asyncio import AsyncIOExecutor from apscheduler.schedulers.asyncio import AsyncIOScheduler APS_AVAILABLE = True except ImportError: APS_AVAILABLE = False from telegram._utils.repr import build_repr_with_selected_attrs from telegram._utils.types import JSONDict from telegram.ext._extbot import ExtBot from telegram.ext._utils.types import CCT, JobCallback if TYPE_CHECKING: if APS_AVAILABLE: from apscheduler.job import Job as APSJob from telegram.ext import Application _ALL_DAYS = tuple(range(7)) class JobQueue(Generic[CCT]): """This class allows you to periodically perform tasks with the bot. It is a convenience wrapper for the APScheduler library. This class is a :class:`~typing.Generic` class and accepts one type variable that specifies the type of the argument ``context`` of the job callbacks (:paramref:`~run_once.callback`) of :meth:`run_once` and the other scheduling methods. Important: If you want to use this class, you must install PTB with the optional requirement ``job-queue``, i.e. .. code-block:: bash pip install "python-telegram-bot[job-queue]" Examples: :any:`Timer Bot ` .. seealso:: :wiki:`Architecture Overview `, :wiki:`Job Queue ` .. versionchanged:: 20.0 To use this class, PTB must be installed via ``pip install "python-telegram-bot[job-queue]"``. Attributes: scheduler (:class:`apscheduler.schedulers.asyncio.AsyncIOScheduler`): The scheduler. Warning: This scheduler is configured by :meth:`set_application`. Additional configuration settings can be made by users. However, calling :meth:`~apscheduler.schedulers.base.BaseScheduler.configure` will delete any previous configuration settings. Therefore, please make sure to pass the values returned by :attr:`scheduler_configuration` to the method call in addition to your custom values. Alternatively, you can also use methods like :meth:`~apscheduler.schedulers.base.BaseScheduler.add_jobstore` to avoid using :meth:`~apscheduler.schedulers.base.BaseScheduler.configure` altogether. .. versionchanged:: 20.0 Uses :class:`~apscheduler.schedulers.asyncio.AsyncIOScheduler` instead of :class:`~apscheduler.schedulers.background.BackgroundScheduler` """ __slots__ = ("_application", "_executor", "scheduler") _CRON_MAPPING = ("sun", "mon", "tue", "wed", "thu", "fri", "sat") def __init__(self) -> None: if not APS_AVAILABLE: raise RuntimeError( "To use `JobQueue`, PTB must be installed via `pip install " '"python-telegram-bot[job-queue]"`.' ) self._application: Optional[weakref.ReferenceType[Application]] = None self._executor = AsyncIOExecutor() self.scheduler: "AsyncIOScheduler" = AsyncIOScheduler(**self.scheduler_configuration) def __repr__(self) -> str: """Give a string representation of the JobQueue in the form ``JobQueue[application=...]``. 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, application=self.application) @property def application(self) -> "Application[Any, CCT, Any, Any, Any, JobQueue[CCT]]": """The application this JobQueue is associated with.""" if self._application is None: raise RuntimeError("No application was set for this JobQueue.") application = self._application() if application is not None: return application raise RuntimeError("The application instance is no longer alive.") @property def scheduler_configuration(self) -> JSONDict: """Provides configuration values that are used by :class:`JobQueue` for :attr:`scheduler`. Tip: Since calling :meth:`scheduler.configure() ` deletes any previous setting, please make sure to pass these values to the method call in addition to your custom values: .. code-block:: python scheduler.configure(..., **job_queue.scheduler_configuration) Alternatively, you can also use methods like :meth:`~apscheduler.schedulers.base.BaseScheduler.add_jobstore` to avoid using :meth:`~apscheduler.schedulers.base.BaseScheduler.configure` altogether. .. versionadded:: 20.7 Returns: Dict[:obj:`str`, :obj:`object`]: The configuration values as dictionary. """ timezone: object = pytz.utc if ( self._application and isinstance(self.application.bot, ExtBot) and self.application.bot.defaults ): timezone = self.application.bot.defaults.tzinfo or pytz.utc return { "timezone": timezone, "executors": {"default": self._executor}, } def _tz_now(self) -> datetime.datetime: return datetime.datetime.now(self.scheduler.timezone) @overload def _parse_time_input(self, time: None, shift_day: bool = False) -> None: ... @overload def _parse_time_input( self, time: Union[float, datetime.timedelta, datetime.datetime, datetime.time], shift_day: bool = False, ) -> datetime.datetime: ... def _parse_time_input( self, time: Union[float, datetime.timedelta, datetime.datetime, datetime.time, None], shift_day: bool = False, ) -> Optional[datetime.datetime]: if time is None: return None if isinstance(time, (int, float)): return self._tz_now() + datetime.timedelta(seconds=time) if isinstance(time, datetime.timedelta): return self._tz_now() + time if isinstance(time, datetime.time): date_time = datetime.datetime.combine( datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time ) if date_time.tzinfo is None: date_time = self.scheduler.timezone.localize(date_time) if shift_day and date_time <= datetime.datetime.now(pytz.utc): date_time += datetime.timedelta(days=1) return date_time return time def set_application( self, application: "Application[Any, CCT, Any, Any, Any, JobQueue[CCT]]" ) -> None: """Set the application to be used by this JobQueue. Args: application (:class:`telegram.ext.Application`): The application. """ self._application = weakref.ref(application) self.scheduler.configure(**self.scheduler_configuration) @staticmethod async def job_callback(job_queue: "JobQueue[CCT]", job: "Job[CCT]") -> None: """This method is used as a callback for the APScheduler jobs. More precisely, the ``func`` argument of :class:`apscheduler.job.Job` is set to this method and the ``arg`` argument (representing positional arguments to ``func``) is set to a tuple containing the :class:`JobQueue` itself and the :class:`~telegram.ext.Job` instance. Tip: This method is a static method rather than a bound method. This makes the arguments more transparent and allows for easier handling of PTBs integration of APScheduler when utilizing advanced features of APScheduler. Hint: This method is effectively a wrapper for :meth:`telegram.ext.Job.run`. .. versionadded:: 20.4 Args: job_queue (:class:`JobQueue`): The job queue that created the job. job (:class:`~telegram.ext.Job`): The job to run. """ await job.run(job_queue.application) def run_once( self, callback: JobCallback[CCT], when: Union[float, datetime.timedelta, datetime.datetime, datetime.time], data: Optional[object] = None, name: Optional[str] = None, chat_id: Optional[int] = None, user_id: Optional[int] = None, job_kwargs: Optional[JSONDict] = None, ) -> "Job[CCT]": """Creates a new :class:`Job` instance that runs once and adds it to the queue. Args: callback (:term:`coroutine function`): The callback function that should be executed by the new job. Callback signature:: async def callback(context: CallbackContext) when (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \ :obj:`datetime.datetime` | :obj:`datetime.time`): Time in or at which the job should run. This parameter will be interpreted depending on its type. * :obj:`int` or :obj:`float` will be interpreted as "seconds from now" in which the job should run. * :obj:`datetime.timedelta` will be interpreted as "time from now" in which the job should run. * :obj:`datetime.datetime` will be interpreted as a specific date and time at which the job should run. If the timezone (:attr:`datetime.datetime.tzinfo`) is :obj:`None`, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used. * :obj:`datetime.time` will be interpreted as a specific time of day at which the job should run. This could be either today or, if the time has already passed, tomorrow. If the timezone (:attr:`datetime.time.tzinfo`) is :obj:`None`, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used. chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will be available in the callback. .. versionadded:: 20.0 user_id (:obj:`int`, optional): User id of the user associated with this job. If passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will be available in the callback. .. versionadded:: 20.0 data (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through :attr:`Job.data` in the callback. Defaults to :obj:`None`. .. versionchanged:: 20.0 Renamed the parameter ``context`` to :paramref:`data`. name (:obj:`str`, optional): The name of the new job. Defaults to :external:attr:`callback.__name__ `. job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the :meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`. Returns: :class:`telegram.ext.Job`: The new :class:`Job` instance that has been added to the job queue. """ if not job_kwargs: job_kwargs = {} name = name or callback.__name__ job = Job(callback=callback, data=data, name=name, chat_id=chat_id, user_id=user_id) date_time = self._parse_time_input(when, shift_day=True) j = self.scheduler.add_job( self.job_callback, name=name, trigger="date", run_date=date_time, args=(self, job), timezone=date_time.tzinfo or self.scheduler.timezone, **job_kwargs, ) job._job = j # pylint: disable=protected-access return job def run_repeating( self, callback: JobCallback[CCT], interval: Union[float, datetime.timedelta], first: Optional[Union[float, datetime.timedelta, datetime.datetime, datetime.time]] = None, last: Optional[Union[float, datetime.timedelta, datetime.datetime, datetime.time]] = None, data: Optional[object] = None, name: Optional[str] = None, chat_id: Optional[int] = None, user_id: Optional[int] = None, job_kwargs: Optional[JSONDict] = None, ) -> "Job[CCT]": """Creates a new :class:`Job` instance that runs at specified intervals and adds it to the queue. Note: For a note about DST, please see the documentation of `APScheduler`_. .. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html #daylight-saving-time-behavior Args: callback (:term:`coroutine function`): The callback function that should be executed by the new job. Callback signature:: async def callback(context: CallbackContext) interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`): The interval in which the job will run. If it is an :obj:`int` or a :obj:`float`, it will be interpreted as seconds. first (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \ :obj:`datetime.datetime` | :obj:`datetime.time`, optional): Time in or at which the job should run. This parameter will be interpreted depending on its type. * :obj:`int` or :obj:`float` will be interpreted as "seconds from now" in which the job should run. * :obj:`datetime.timedelta` will be interpreted as "time from now" in which the job should run. * :obj:`datetime.datetime` will be interpreted as a specific date and time at which the job should run. If the timezone (:attr:`datetime.datetime.tzinfo`) is :obj:`None`, the default timezone of the bot will be used. * :obj:`datetime.time` will be interpreted as a specific time of day at which the job should run. This could be either today or, if the time has already passed, tomorrow. If the timezone (:attr:`datetime.time.tzinfo`) is :obj:`None`, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used. Defaults to :paramref:`interval` Note: Setting :paramref:`first` to ``0``, ``datetime.datetime.now()`` or another value that indicates that the job should run immediately will not work due to how the APScheduler library works. If you want to run a job immediately, we recommend to use an approach along the lines of:: job = context.job_queue.run_repeating(callback, interval=5) await job.run(context.application) .. seealso:: :meth:`telegram.ext.Job.run` last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \ :obj:`datetime.datetime` | :obj:`datetime.time`, optional): Latest possible time for the job to run. This parameter will be interpreted depending on its type. See :paramref:`first` for details. If :paramref:`last` is :obj:`datetime.datetime` or :obj:`datetime.time` type and ``last.tzinfo`` is :obj:`None`, the default timezone of the bot will be assumed, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used. Defaults to :obj:`None`. data (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through :attr:`Job.data` in the callback. Defaults to :obj:`None`. .. versionchanged:: 20.0 Renamed the parameter ``context`` to :paramref:`data`. name (:obj:`str`, optional): The name of the new job. Defaults to :external:attr:`callback.__name__ `. chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will be available in the callback. .. versionadded:: 20.0 user_id (:obj:`int`, optional): User id of the user associated with this job. If passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will be available in the callback. .. versionadded:: 20.0 job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the :meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`. Returns: :class:`telegram.ext.Job`: The new :class:`Job` instance that has been added to the job queue. """ if not job_kwargs: job_kwargs = {} name = name or callback.__name__ job = Job(callback=callback, data=data, name=name, chat_id=chat_id, user_id=user_id) dt_first = self._parse_time_input(first) dt_last = self._parse_time_input(last) if dt_last and dt_first and dt_last < dt_first: raise ValueError("'last' must not be before 'first'!") if isinstance(interval, datetime.timedelta): interval = interval.total_seconds() j = self.scheduler.add_job( self.job_callback, trigger="interval", args=(self, job), start_date=dt_first, end_date=dt_last, seconds=interval, name=name, **job_kwargs, ) job._job = j # pylint: disable=protected-access return job def run_monthly( self, callback: JobCallback[CCT], when: datetime.time, day: int, data: Optional[object] = None, name: Optional[str] = None, chat_id: Optional[int] = None, user_id: Optional[int] = None, job_kwargs: Optional[JSONDict] = None, ) -> "Job[CCT]": """Creates a new :class:`Job` that runs on a monthly basis and adds it to the queue. .. versionchanged:: 20.0 The ``day_is_strict`` argument was removed. Instead one can now pass ``-1`` to the :paramref:`day` parameter to have the job run on the last day of the month. Args: callback (:term:`coroutine function`): The callback function that should be executed by the new job. Callback signature:: async def callback(context: CallbackContext) when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone (``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used. day (:obj:`int`): Defines the day of the month whereby the job would run. It should be within the range of ``1`` and ``31``, inclusive. If a month has fewer days than this number, the job will not run in this month. Passing ``-1`` leads to the job running on the last day of the month. data (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through :attr:`Job.data` in the callback. Defaults to :obj:`None`. .. versionchanged:: 20.0 Renamed the parameter ``context`` to :paramref:`data`. name (:obj:`str`, optional): The name of the new job. Defaults to :external:attr:`callback.__name__ `. chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will be available in the callback. .. versionadded:: 20.0 user_id (:obj:`int`, optional): User id of the user associated with this job. If passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will be available in the callback. .. versionadded:: 20.0 job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the :meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`. Returns: :class:`telegram.ext.Job`: The new :class:`Job` instance that has been added to the job queue. """ if not job_kwargs: job_kwargs = {} name = name or callback.__name__ job = Job(callback=callback, data=data, name=name, chat_id=chat_id, user_id=user_id) j = self.scheduler.add_job( self.job_callback, trigger="cron", args=(self, job), name=name, day="last" if day == -1 else day, hour=when.hour, minute=when.minute, second=when.second, timezone=when.tzinfo or self.scheduler.timezone, **job_kwargs, ) job._job = j # pylint: disable=protected-access return job def run_daily( self, callback: JobCallback[CCT], time: datetime.time, days: Tuple[int, ...] = _ALL_DAYS, data: Optional[object] = None, name: Optional[str] = None, chat_id: Optional[int] = None, user_id: Optional[int] = None, job_kwargs: Optional[JSONDict] = None, ) -> "Job[CCT]": """Creates a new :class:`Job` that runs on a daily basis and adds it to the queue. Note: For a note about DST, please see the documentation of `APScheduler`_. .. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html #daylight-saving-time-behavior Args: callback (:term:`coroutine function`): The callback function that should be executed by the new job. Callback signature:: async def callback(context: CallbackContext) time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone (:obj:`datetime.time.tzinfo`) is :obj:`None`, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used. days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should run (where ``0-6`` correspond to sunday - saturday). By default, the job will run every day. .. versionchanged:: 20.0 Changed day of the week mapping of 0-6 from monday-sunday to sunday-saturday. data (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through :attr:`Job.data` in the callback. Defaults to :obj:`None`. .. versionchanged:: 20.0 Renamed the parameter ``context`` to :paramref:`data`. name (:obj:`str`, optional): The name of the new job. Defaults to :external:attr:`callback.__name__ `. chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will be available in the callback. .. versionadded:: 20.0 user_id (:obj:`int`, optional): User id of the user associated with this job. If passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will be available in the callback. .. versionadded:: 20.0 job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the :meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`. Returns: :class:`telegram.ext.Job`: The new :class:`Job` instance that has been added to the job queue. """ if not job_kwargs: job_kwargs = {} name = name or callback.__name__ job = Job(callback=callback, data=data, name=name, chat_id=chat_id, user_id=user_id) j = self.scheduler.add_job( self.job_callback, name=name, args=(self, job), trigger="cron", day_of_week=",".join([self._CRON_MAPPING[d] for d in days]), hour=time.hour, minute=time.minute, second=time.second, timezone=time.tzinfo or self.scheduler.timezone, **job_kwargs, ) job._job = j # pylint: disable=protected-access return job def run_custom( self, callback: JobCallback[CCT], job_kwargs: JSONDict, data: Optional[object] = None, name: Optional[str] = None, chat_id: Optional[int] = None, user_id: Optional[int] = None, ) -> "Job[CCT]": """Creates a new custom defined :class:`Job`. Args: callback (:term:`coroutine function`): The callback function that should be executed by the new job. Callback signature:: async def callback(context: CallbackContext) job_kwargs (:obj:`dict`): Arbitrary keyword arguments. Used as arguments for :meth:`apscheduler.schedulers.base.BaseScheduler.add_job`. data (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through :attr:`Job.data` in the callback. Defaults to :obj:`None`. .. versionchanged:: 20.0 Renamed the parameter ``context`` to :paramref:`data`. name (:obj:`str`, optional): The name of the new job. Defaults to :external:attr:`callback.__name__ `. chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will be available in the callback. .. versionadded:: 20.0 user_id (:obj:`int`, optional): User id of the user associated with this job. If passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will be available in the callback. .. versionadded:: 20.0 Returns: :class:`telegram.ext.Job`: The new :class:`Job` instance that has been added to the job queue. """ name = name or callback.__name__ job = Job(callback=callback, data=data, name=name, chat_id=chat_id, user_id=user_id) j = self.scheduler.add_job(self.job_callback, args=(self, job), name=name, **job_kwargs) job._job = j # pylint: disable=protected-access return job async def start(self) -> None: # this method async just in case future versions need that """Starts the :class:`~telegram.ext.JobQueue`.""" if not self.scheduler.running: self.scheduler.start() async def stop(self, wait: bool = True) -> None: """Shuts down the :class:`~telegram.ext.JobQueue`. Args: wait (:obj:`bool`, optional): Whether to wait until all currently running jobs have finished. Defaults to :obj:`True`. """ # the interface methods of AsyncIOExecutor are currently not really asyncio-compatible # so we apply some small tweaks here to try and smoothen the integration into PTB # TODO: When APS 4.0 hits, we should be able to remove the tweaks if wait: # Unfortunately AsyncIOExecutor just cancels them all ... await asyncio.gather( *self._executor._pending_futures, # pylint: disable=protected-access return_exceptions=True, ) if self.scheduler.running: self.scheduler.shutdown(wait=wait) # scheduler.shutdown schedules a task in the event loop but immediately returns # so give it a tiny bit of time to actually shut down. await asyncio.sleep(0.01) def jobs(self) -> Tuple["Job[CCT]", ...]: """Returns a tuple of all *scheduled* jobs that are currently in the :class:`JobQueue`. Returns: Tuple[:class:`Job`]: Tuple of all *scheduled* jobs. """ return tuple(Job.from_aps_job(job) for job in self.scheduler.get_jobs()) def get_jobs_by_name(self, name: str) -> Tuple["Job[CCT]", ...]: """Returns a tuple of all *pending/scheduled* jobs with the given name that are currently in the :class:`JobQueue`. Returns: Tuple[:class:`Job`]: Tuple of all *pending* or *scheduled* jobs matching the name. """ return tuple(job for job in self.jobs() if job.name == name) class Job(Generic[CCT]): """This class is a convenience wrapper for the jobs held in a :class:`telegram.ext.JobQueue`. With the current backend APScheduler, :attr:`job` holds a :class:`apscheduler.job.Job` instance. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :class:`id ` is equal. This class is a :class:`~typing.Generic` class and accepts one type variable that specifies the type of the argument ``context`` of :paramref:`callback`. Important: If you want to use this class, you must install PTB with the optional requirement ``job-queue``, i.e. .. code-block:: bash pip install "python-telegram-bot[job-queue]" Note: All attributes and instance methods of :attr:`job` are also directly available as attributes/methods of the corresponding :class:`telegram.ext.Job` object. Warning: This class should not be instantiated manually. Use the methods of :class:`telegram.ext.JobQueue` to schedule jobs. .. seealso:: :wiki:`Job Queue ` .. versionchanged:: 20.0 * Removed argument and attribute ``job_queue``. * Renamed ``Job.context`` to :attr:`Job.data`. * Removed argument ``job`` * To use this class, PTB must be installed via ``pip install "python-telegram-bot[job-queue]"``. Args: callback (:term:`coroutine function`): The callback function that should be executed by the new job. Callback signature:: async def callback(context: CallbackContext) data (:obj:`object`, optional): Additional data needed for the :paramref:`callback` function. Can be accessed through :attr:`Job.data` in the callback. Defaults to :obj:`None`. name (:obj:`str`, optional): The name of the new job. Defaults to :external:obj:`callback.__name__ `. chat_id (:obj:`int`, optional): Chat id of the chat that this job is associated with. .. versionadded:: 20.0 user_id (:obj:`int`, optional): User id of the user that this job is associated with. .. versionadded:: 20.0 Attributes: callback (:term:`coroutine function`): The callback function that should be executed by the new job. data (:obj:`object`): Optional. Additional data needed for the :attr:`callback` function. name (:obj:`str`): Optional. The name of the new job. chat_id (:obj:`int`): Optional. Chat id of the chat that this job is associated with. .. versionadded:: 20.0 user_id (:obj:`int`): Optional. User id of the user that this job is associated with. .. versionadded:: 20.0 """ __slots__ = ( "_enabled", "_job", "_removed", "callback", "chat_id", "data", "name", "user_id", ) def __init__( self, callback: JobCallback[CCT], data: Optional[object] = None, name: Optional[str] = None, chat_id: Optional[int] = None, user_id: Optional[int] = None, ): if not APS_AVAILABLE: raise RuntimeError( "To use `Job`, PTB must be installed via `pip install " '"python-telegram-bot[job-queue]"`.' ) self.callback: JobCallback[CCT] = callback self.data: Optional[object] = data self.name: Optional[str] = name or callback.__name__ self.chat_id: Optional[int] = chat_id self.user_id: Optional[int] = user_id self._removed = False self._enabled = False self._job = cast("APSJob", None) def __getattr__(self, item: str) -> object: """Overrides :py:meth:`object.__getattr__` to get specific attribute of the :class:`telegram.ext.Job` object or of its attribute :class:`apscheduler.job.Job`, if exists. Args: item (:obj:`str`): The name of the attribute. Returns: :object: The value of the attribute. Raises: :exc:`AttributeError`: If the attribute does not exist in both :class:`telegram.ext.Job` and :class:`apscheduler.job.Job` objects. """ try: return getattr(self.job, item) except AttributeError as exc: raise AttributeError( f"Neither 'telegram.ext.Job' nor 'apscheduler.job.Job' has attribute '{item}'" ) from exc def __eq__(self, other: object) -> bool: """Defines equality condition for the :class:`telegram.ext.Job` object. Two objects of this class are considered to be equal if their :class:`id ` are equal. Returns: :obj:`True` if both objects have :paramref:`id` parameters identical. :obj:`False` otherwise. """ if isinstance(other, self.__class__): return self.id == other.id return False 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`: The hash value of the object. """ return hash(self.id) def __repr__(self) -> str: """Give a string representation of the job in the form ``Job[id=..., name=..., callback=..., trigger=...]``. 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, id=self.job.id, name=self.name, callback=self.callback.__name__, trigger=self.job.trigger, ) @property def job(self) -> "APSJob": """:class:`apscheduler.job.Job`: The APS Job this job is a wrapper for. .. versionchanged:: 20.0 This property is now read-only. """ return self._job @property def removed(self) -> bool: """:obj:`bool`: Whether this job is due to be removed.""" return self._removed @property def enabled(self) -> bool: """:obj:`bool`: Whether this job is enabled.""" return self._enabled @enabled.setter def enabled(self, status: bool) -> None: if status: self.job.resume() else: self.job.pause() self._enabled = status @property def next_t(self) -> Optional[datetime.datetime]: """ :class:`datetime.datetime`: Datetime for the next job execution. Datetime is localized according to :attr:`datetime.datetime.tzinfo`. If job is removed or already ran it equals to :obj:`None`. Warning: This attribute is only available, if the :class:`telegram.ext.JobQueue` this job belongs to is already started. Otherwise APScheduler raises an :exc:`AttributeError`. """ return self.job.next_run_time @classmethod def from_aps_job(cls, aps_job: "APSJob") -> "Job[CCT]": """Provides the :class:`telegram.ext.Job` that is associated with the given APScheduler job. Tip: This method can be useful when using advanced APScheduler features along with :class:`telegram.ext.JobQueue`. .. versionadded:: 20.4 Args: aps_job (:class:`apscheduler.job.Job`): The APScheduler job Returns: :class:`telegram.ext.Job` """ ext_job = aps_job.args[1] ext_job._job = aps_job # pylint: disable=protected-access return ext_job async def run( self, application: "Application[Any, CCT, Any, Any, Any, JobQueue[CCT]]" ) -> None: """Executes the callback function independently of the jobs schedule. Also calls :meth:`telegram.ext.Application.update_persistence`. .. versionchanged:: 20.0 Calls :meth:`telegram.ext.Application.update_persistence`. Args: application (:class:`telegram.ext.Application`): The application this job is associated with. """ # We shield the task such that the job isn't cancelled mid-run await asyncio.shield(self._run(application)) async def _run( self, application: "Application[Any, CCT, Any, Any, Any, JobQueue[CCT]]" ) -> None: try: context = application.context_types.context.from_job(self, application) await context.refresh_data() await self.callback(context) except Exception as exc: await application.create_task( application.process_error(None, exc, job=self), name=f"Job:{self.id}:run:process_error", ) finally: # This is internal logic of application - let's keep it private for now application._mark_for_persistence_update(job=self) # pylint: disable=protected-access def schedule_removal(self) -> None: """ Schedules this job for removal from the :class:`JobQueue`. It will be removed without executing its callback function again. """ self.job.remove() self._removed = True python-telegram-bot-21.1.1/telegram/ext/_picklepersistence.py000066400000000000000000000546361460724040100243360ustar00rootroot00000000000000#!/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 PicklePersistence class.""" import pickle from copy import deepcopy from pathlib import Path from typing import Any, Callable, Dict, Optional, Set, Tuple, Type, TypeVar, Union, cast, overload from telegram import Bot, TelegramObject from telegram._utils.types import FilePathInput from telegram._utils.warnings import warn from telegram.ext import BasePersistence, PersistenceInput from telegram.ext._contexttypes import ContextTypes from telegram.ext._utils.types import BD, CD, UD, CDCData, ConversationDict, ConversationKey _REPLACED_KNOWN_BOT = "a known bot replaced by PTB's PicklePersistence" _REPLACED_UNKNOWN_BOT = "an unknown bot replaced by PTB's PicklePersistence" TelegramObj = TypeVar("TelegramObj", bound=TelegramObject) def _all_subclasses(cls: Type[TelegramObj]) -> Set[Type[TelegramObj]]: """Gets all subclasses of the specified object, recursively. from https://stackoverflow.com/a/3862957/9706202 """ subclasses = cls.__subclasses__() return set(subclasses).union([s for c in subclasses for s in _all_subclasses(c)]) def _reconstruct_to(cls: Type[TelegramObj], kwargs: dict) -> TelegramObj: """ This method is used for unpickling. The data, which is in the form a dictionary, is converted back into a class. Works mostly the same as :meth:`TelegramObject.__setstate__`. This function should be kept in place for backwards compatibility even if the pickling logic is changed, since `_custom_reduction` places references to this function into the pickled data. """ obj = cls.__new__(cls) obj.__setstate__(kwargs) return obj def _custom_reduction(cls: TelegramObj) -> Tuple[Callable, Tuple[Type[TelegramObj], dict]]: """ This method is used for pickling. The bot attribute is preserved so _BotPickler().persistent_id works as intended. """ data = cls._get_attrs(include_private=True) # pylint: disable=protected-access # MappingProxyType is not pickable, so we convert it to a dict # no need to convert back to MPT in _reconstruct_to, since it's done in __setstate__ data["api_kwargs"] = dict(data["api_kwargs"]) # type: ignore[arg-type] return _reconstruct_to, (cls.__class__, data) class _BotPickler(pickle.Pickler): __slots__ = ("_bot",) def __init__(self, bot: Bot, *args: Any, **kwargs: Any): self._bot = bot super().__init__(*args, **kwargs) def reducer_override( self, obj: TelegramObj ) -> Tuple[Callable, Tuple[Type[TelegramObj], dict]]: """ This method is used for pickling. The bot attribute is preserved so _BotPickler().persistent_id works as intended. """ if not isinstance(obj, TelegramObject): return NotImplemented return _custom_reduction(obj) def persistent_id(self, obj: object) -> Optional[str]: """Used to 'mark' the Bot, so it can be replaced later. See https://docs.python.org/3/library/pickle.html#pickle.Pickler.persistent_id for more info """ if obj is self._bot: return _REPLACED_KNOWN_BOT if isinstance(obj, Bot): warn( "Unknown bot instance found. Will be replaced by `None` during unpickling", stacklevel=2, ) return _REPLACED_UNKNOWN_BOT return None # pickles as usual class _BotUnpickler(pickle.Unpickler): __slots__ = ("_bot",) def __init__(self, bot: Bot, *args: Any, **kwargs: Any): self._bot = bot super().__init__(*args, **kwargs) def persistent_load(self, pid: str) -> Optional[Bot]: """Replaces the bot with the current bot if known, else it is replaced by :obj:`None`.""" if pid == _REPLACED_KNOWN_BOT: return self._bot if pid == _REPLACED_UNKNOWN_BOT: return None raise pickle.UnpicklingError("Found unknown persistent id when unpickling!") class PicklePersistence(BasePersistence[UD, CD, BD]): """Using python's builtin :mod:`pickle` for making your bot persistent. Attention: The interface provided by this class is intended to be accessed exclusively by :class:`~telegram.ext.Application`. Calling any of the methods below manually might interfere with the integration of persistence into :class:`~telegram.ext.Application`. Note: This implementation of :class:`BasePersistence` uses the functionality of the pickle module to support serialization of bot instances. Specifically any reference to :attr:`~BasePersistence.bot` will be replaced by a placeholder before pickling and :attr:`~BasePersistence.bot` will be inserted back when loading the data. Examples: :any:`Persistent Conversation Bot ` .. seealso:: :wiki:`Making Your Bot Persistent ` .. versionchanged:: 20.0 * The parameters and attributes ``store_*_data`` were replaced by :attr:`store_data`. * The parameter and attribute ``filename`` were replaced by :attr:`filepath`. * :attr:`filepath` now also accepts :obj:`pathlib.Path` as argument. Args: filepath (:obj:`str` | :obj:`pathlib.Path`): The filepath for storing the pickle files. When :attr:`single_file` is :obj:`False` this will be used as a prefix. store_data (:class:`~telegram.ext.PersistenceInput`, optional): Specifies which kinds of data will be saved by this persistence instance. By default, all available kinds of data will be saved. single_file (:obj:`bool`, optional): When :obj:`False` will store 5 separate files of `filename_user_data`, `filename_bot_data`, `filename_chat_data`, `filename_callback_data` and `filename_conversations`. Default is :obj:`True`. on_flush (:obj:`bool`, optional): When :obj:`True` will only save to file when :meth:`flush` is called and keep data in memory until that happens. When :obj:`False` will store data on any transaction *and* on call to :meth:`flush`. Default is :obj:`False`. context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance of :class:`telegram.ext.ContextTypes` to customize the types used in the ``context`` interface. If not passed, the defaults documented in :class:`telegram.ext.ContextTypes` will be used. .. versionadded:: 13.6 update_interval (:obj:`int` | :obj:`float`, optional): The :class:`~telegram.ext.Application` will update the persistence in regular intervals. This parameter specifies the time (in seconds) to wait between two consecutive runs of updating the persistence. Defaults to 60 seconds. .. versionadded:: 20.0 Attributes: filepath (:obj:`str` | :obj:`pathlib.Path`): The filepath for storing the pickle files. When :attr:`single_file` is :obj:`False` this will be used as a prefix. store_data (:class:`~telegram.ext.PersistenceInput`): Specifies which kinds of data will be saved by this persistence instance. single_file (:obj:`bool`): Optional. When :obj:`False` will store 5 separate files of `filename_user_data`, `filename_bot_data`, `filename_chat_data`, `filename_callback_data` and `filename_conversations`. Default is :obj:`True`. on_flush (:obj:`bool`): Optional. When :obj:`True` will only save to file when :meth:`flush` is called and keep data in memory until that happens. When :obj:`False` will store data on any transaction *and* on call to :meth:`flush`. Default is :obj:`False`. context_types (:class:`telegram.ext.ContextTypes`): Container for the types used in the ``context`` interface. .. versionadded:: 13.6 """ __slots__ = ( "bot_data", "callback_data", "chat_data", "context_types", "conversations", "filepath", "on_flush", "single_file", "user_data", ) @overload def __init__( self: "PicklePersistence[Dict[Any, Any], Dict[Any, Any], Dict[Any, Any]]", filepath: FilePathInput, store_data: Optional[PersistenceInput] = None, single_file: bool = True, on_flush: bool = False, update_interval: float = 60, ): ... @overload def __init__( self: "PicklePersistence[UD, CD, BD]", filepath: FilePathInput, store_data: Optional[PersistenceInput] = None, single_file: bool = True, on_flush: bool = False, update_interval: float = 60, context_types: Optional[ContextTypes[Any, UD, CD, BD]] = None, ): ... def __init__( self, filepath: FilePathInput, store_data: Optional[PersistenceInput] = None, single_file: bool = True, on_flush: bool = False, update_interval: float = 60, context_types: Optional[ContextTypes[Any, UD, CD, BD]] = None, ): super().__init__(store_data=store_data, update_interval=update_interval) self.filepath: Path = Path(filepath) self.single_file: Optional[bool] = single_file self.on_flush: Optional[bool] = on_flush self.user_data: Optional[Dict[int, UD]] = None self.chat_data: Optional[Dict[int, CD]] = None self.bot_data: Optional[BD] = None self.callback_data: Optional[CDCData] = None self.conversations: Optional[Dict[str, Dict[Tuple[Union[int, str], ...], object]]] = None self.context_types: ContextTypes[Any, UD, CD, BD] = cast( ContextTypes[Any, UD, CD, BD], context_types or ContextTypes() ) def _load_singlefile(self) -> None: try: with self.filepath.open("rb") as file: data = _BotUnpickler(self.bot, file).load() self.user_data = data["user_data"] self.chat_data = data["chat_data"] # For backwards compatibility with files not containing bot data self.bot_data = data.get("bot_data", self.context_types.bot_data()) self.callback_data = data.get("callback_data", {}) self.conversations = data["conversations"] except OSError: self.conversations = {} self.user_data = {} self.chat_data = {} self.bot_data = self.context_types.bot_data() self.callback_data = None except pickle.UnpicklingError as exc: filename = self.filepath.name raise TypeError(f"File {filename} does not contain valid pickle data") from exc except Exception as exc: raise TypeError(f"Something went wrong unpickling {self.filepath.name}") from exc def _load_file(self, filepath: Path) -> Any: try: with filepath.open("rb") as file: return _BotUnpickler(self.bot, file).load() except OSError: return None except pickle.UnpicklingError as exc: raise TypeError(f"File {filepath.name} does not contain valid pickle data") from exc except Exception as exc: raise TypeError(f"Something went wrong unpickling {filepath.name}") from exc def _dump_singlefile(self) -> None: data = { "conversations": self.conversations, "user_data": self.user_data, "chat_data": self.chat_data, "bot_data": self.bot_data, "callback_data": self.callback_data, } with self.filepath.open("wb") as file: _BotPickler(self.bot, file, protocol=pickle.HIGHEST_PROTOCOL).dump(data) def _dump_file(self, filepath: Path, data: object) -> None: with filepath.open("wb") as file: _BotPickler(self.bot, file, protocol=pickle.HIGHEST_PROTOCOL).dump(data) async def get_user_data(self) -> Dict[int, UD]: """Returns the user_data from the pickle file if it exists or an empty :obj:`dict`. Returns: Dict[:obj:`int`, :obj:`dict`]: The restored user data. """ if self.user_data: pass elif not self.single_file: data = self._load_file(Path(f"{self.filepath}_user_data")) if not data: data = {} self.user_data = data else: self._load_singlefile() return deepcopy(self.user_data) # type: ignore[arg-type] async def get_chat_data(self) -> Dict[int, CD]: """Returns the chat_data from the pickle file if it exists or an empty :obj:`dict`. Returns: Dict[:obj:`int`, :obj:`dict`]: The restored chat data. """ if self.chat_data: pass elif not self.single_file: data = self._load_file(Path(f"{self.filepath}_chat_data")) if not data: data = {} self.chat_data = data else: self._load_singlefile() return deepcopy(self.chat_data) # type: ignore[arg-type] async def get_bot_data(self) -> BD: """Returns the bot_data from the pickle file if it exists or an empty object of type :obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`. Returns: :obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`: The restored bot data. """ if self.bot_data: pass elif not self.single_file: data = self._load_file(Path(f"{self.filepath}_bot_data")) if not data: data = self.context_types.bot_data() self.bot_data = data else: self._load_singlefile() return deepcopy(self.bot_data) # type: ignore[return-value] async def get_callback_data(self) -> Optional[CDCData]: """Returns the callback data from the pickle file if it exists or :obj:`None`. .. versionadded:: 13.6 Returns: Tuple[List[Tuple[:obj:`str`, :obj:`float`, Dict[:obj:`str`, :class:`object`]]], Dict[:obj:`str`, :obj:`str`]] | :obj:`None`: The restored metadata or :obj:`None`, if no data was stored. """ if self.callback_data: pass elif not self.single_file: data = self._load_file(Path(f"{self.filepath}_callback_data")) if not data: data = None self.callback_data = data else: self._load_singlefile() if self.callback_data is None: return None return deepcopy(self.callback_data) async def get_conversations(self, name: str) -> ConversationDict: """Returns the conversations from the pickle file if it exists or an empty dict. Args: name (:obj:`str`): The handlers name. Returns: :obj:`dict`: The restored conversations for the handler. """ if self.conversations: pass elif not self.single_file: data = self._load_file(Path(f"{self.filepath}_conversations")) if not data: data = {name: {}} self.conversations = data else: self._load_singlefile() return self.conversations.get(name, {}).copy() # type: ignore[union-attr] async def update_conversation( self, name: str, key: ConversationKey, new_state: Optional[object] ) -> None: """Will update the conversations for the given handler and depending on :attr:`on_flush` save the pickle file. Args: name (:obj:`str`): The handler's name. key (:obj:`tuple`): The key the state is changed for. new_state (:class:`object`): The new state for the given key. """ if not self.conversations: self.conversations = {} if self.conversations.setdefault(name, {}).get(key) == new_state: return self.conversations[name][key] = new_state if not self.on_flush: if not self.single_file: self._dump_file(Path(f"{self.filepath}_conversations"), self.conversations) else: self._dump_singlefile() async def update_user_data(self, user_id: int, data: UD) -> None: """Will update the user_data and depending on :attr:`on_flush` save the pickle file. Args: user_id (:obj:`int`): The user the data might have been changed for. data (:obj:`dict`): The :attr:`telegram.ext.Application.user_data` ``[user_id]``. """ if self.user_data is None: self.user_data = {} if self.user_data.get(user_id) == data: return self.user_data[user_id] = data if not self.on_flush: if not self.single_file: self._dump_file(Path(f"{self.filepath}_user_data"), self.user_data) else: self._dump_singlefile() async def update_chat_data(self, chat_id: int, data: CD) -> None: """Will update the chat_data and depending on :attr:`on_flush` save the pickle file. Args: chat_id (:obj:`int`): The chat the data might have been changed for. data (:obj:`dict`): The :attr:`telegram.ext.Application.chat_data` ``[chat_id]``. """ if self.chat_data is None: self.chat_data = {} if self.chat_data.get(chat_id) == data: return self.chat_data[chat_id] = data if not self.on_flush: if not self.single_file: self._dump_file(Path(f"{self.filepath}_chat_data"), self.chat_data) else: self._dump_singlefile() async def update_bot_data(self, data: BD) -> None: """Will update the bot_data and depending on :attr:`on_flush` save the pickle file. Args: data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`): The :attr:`telegram.ext.Application.bot_data`. """ if self.bot_data == data: return self.bot_data = data if not self.on_flush: if not self.single_file: self._dump_file(Path(f"{self.filepath}_bot_data"), self.bot_data) else: self._dump_singlefile() async def update_callback_data(self, data: CDCData) -> None: """Will update the callback_data (if changed) and depending on :attr:`on_flush` save the pickle file. .. versionadded:: 13.6 Args: data (Tuple[List[Tuple[:obj:`str`, :obj:`float`, \ Dict[:obj:`str`, :class:`object`]]], Dict[:obj:`str`, :obj:`str`]]): The relevant data to restore :class:`telegram.ext.CallbackDataCache`. """ if self.callback_data == data: return self.callback_data = data if not self.on_flush: if not self.single_file: self._dump_file(Path(f"{self.filepath}_callback_data"), self.callback_data) else: self._dump_singlefile() async def drop_chat_data(self, chat_id: int) -> None: """Will delete the specified key from the ``chat_data`` and depending on :attr:`on_flush` save the pickle file. .. versionadded:: 20.0 Args: chat_id (:obj:`int`): The chat id to delete from the persistence. """ if self.chat_data is None: return self.chat_data.pop(chat_id, None) if not self.on_flush: if not self.single_file: self._dump_file(Path(f"{self.filepath}_chat_data"), self.chat_data) else: self._dump_singlefile() async def drop_user_data(self, user_id: int) -> None: """Will delete the specified key from the ``user_data`` and depending on :attr:`on_flush` save the pickle file. .. versionadded:: 20.0 Args: user_id (:obj:`int`): The user id to delete from the persistence. """ if self.user_data is None: return self.user_data.pop(user_id, None) if not self.on_flush: if not self.single_file: self._dump_file(Path(f"{self.filepath}_user_data"), self.user_data) else: self._dump_singlefile() async def refresh_user_data(self, user_id: int, user_data: UD) -> None: """Does nothing. .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data` """ async def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: """Does nothing. .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data` """ async def refresh_bot_data(self, bot_data: BD) -> None: """Does nothing. .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data` """ async def flush(self) -> None: """Will save all data in memory to pickle file(s).""" if self.single_file: if ( self.user_data or self.chat_data or self.bot_data or self.callback_data or self.conversations ): self._dump_singlefile() else: if self.user_data: self._dump_file(Path(f"{self.filepath}_user_data"), self.user_data) if self.chat_data: self._dump_file(Path(f"{self.filepath}_chat_data"), self.chat_data) if self.bot_data: self._dump_file(Path(f"{self.filepath}_bot_data"), self.bot_data) if self.callback_data: self._dump_file(Path(f"{self.filepath}_callback_data"), self.callback_data) if self.conversations: self._dump_file(Path(f"{self.filepath}_conversations"), self.conversations) python-telegram-bot-21.1.1/telegram/ext/_updater.py000066400000000000000000001117171460724040100222600ustar00rootroot00000000000000#!/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 Updater, which tries to make creating Telegram bots intuitive.""" import asyncio import contextlib import ssl from pathlib import Path from types import TracebackType from typing import ( TYPE_CHECKING, Any, AsyncContextManager, Callable, Coroutine, List, Optional, Type, TypeVar, Union, ) from telegram._utils.defaultvalue import DEFAULT_80, DEFAULT_IP, DEFAULT_NONE, DefaultValue from telegram._utils.logging import get_logger from telegram._utils.repr import build_repr_with_selected_attrs from telegram._utils.types import DVType, ODVInput from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut try: from telegram.ext._utils.webhookhandler import WebhookAppClass, WebhookServer WEBHOOKS_AVAILABLE = True except ImportError: WEBHOOKS_AVAILABLE = False if TYPE_CHECKING: from socket import socket from telegram import Bot _UpdaterType = TypeVar("_UpdaterType", bound="Updater") # pylint: disable=invalid-name _LOGGER = get_logger(__name__) class Updater(AsyncContextManager["Updater"]): """This class fetches updates for the bot either via long polling or by starting a webhook server. Received updates are enqueued into the :attr:`update_queue` and may be fetched from there to handle them appropriately. Instances of this class can be used as asyncio context managers, where .. code:: python async with updater: # code is roughly equivalent to .. code:: python try: await updater.initialize() # code finally: await updater.shutdown() .. seealso:: :meth:`__aenter__` and :meth:`__aexit__`. .. seealso:: :wiki:`Architecture Overview `, :wiki:`Builder Pattern ` .. versionchanged:: 20.0 * Removed argument and attribute ``user_sig_handler`` * The only arguments and attributes are now :attr:`bot` and :attr:`update_queue` as now the sole purpose of this class is to fetch updates. The entry point to a PTB application is now :class:`telegram.ext.Application`. Args: bot (:class:`telegram.Bot`): The bot used with this Updater. update_queue (:class:`asyncio.Queue`): Queue for the updates. Attributes: bot (:class:`telegram.Bot`): The bot used with this Updater. update_queue (:class:`asyncio.Queue`): Queue for the updates. """ __slots__ = ( "__lock", "__polling_cleanup_cb", "__polling_task", "__polling_task_stop_event", "_httpd", "_initialized", "_last_update_id", "_running", "bot", "update_queue", ) def __init__( self, bot: "Bot", update_queue: "asyncio.Queue[object]", ): self.bot: Bot = bot self.update_queue: asyncio.Queue[object] = update_queue self._last_update_id = 0 self._running = False self._initialized = False self._httpd: Optional[WebhookServer] = None self.__lock = asyncio.Lock() self.__polling_task: Optional[asyncio.Task] = None self.__polling_task_stop_event: asyncio.Event = asyncio.Event() self.__polling_cleanup_cb: Optional[Callable[[], Coroutine[Any, Any, None]]] = None async def __aenter__(self: _UpdaterType) -> _UpdaterType: # noqa: PYI019 """ |async_context_manager| :meth:`initializes ` the Updater. Returns: The initialized Updater instance. Raises: :exc:`Exception`: If an exception is raised during initialization, :meth:`shutdown` is called in this case. """ try: await self.initialize() return self except Exception as exc: await self.shutdown() raise exc 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 Updater.""" # 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 __repr__(self) -> str: """Give a string representation of the updater in the form ``Updater[bot=...]``. 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, bot=self.bot) @property def running(self) -> bool: return self._running async def initialize(self) -> None: """Initializes the Updater & the associated :attr:`bot` by calling :meth:`telegram.Bot.initialize`. .. seealso:: :meth:`shutdown` """ if self._initialized: _LOGGER.debug("This Updater is already initialized.") return await self.bot.initialize() self._initialized = True async def shutdown(self) -> None: """ Shutdown the Updater & the associated :attr:`bot` by calling :meth:`telegram.Bot.shutdown`. .. seealso:: :meth:`initialize` Raises: :exc:`RuntimeError`: If the updater is still running. """ if self.running: raise RuntimeError("This Updater is still running!") if not self._initialized: _LOGGER.debug("This Updater is already shut down. Returning.") return await self.bot.shutdown() self._initialized = False _LOGGER.debug("Shut down of Updater complete") async def start_polling( self, poll_interval: float = 0.0, timeout: int = 10, bootstrap_retries: int = -1, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, allowed_updates: Optional[List[str]] = None, drop_pending_updates: Optional[bool] = None, error_callback: Optional[Callable[[TelegramError], None]] = None, ) -> "asyncio.Queue[object]": """Starts polling updates from Telegram. .. versionchanged:: 20.0 Removed the ``clean`` argument in favor of :paramref:`drop_pending_updates`. Args: poll_interval (:obj:`float`, optional): Time to wait between polling updates from Telegram in seconds. Default is ``0.0``. timeout (:obj:`int`, optional): Passed to :paramref:`telegram.Bot.get_updates.timeout`. Defaults to ``10`` seconds. bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the :class:`telegram.ext.Updater` will retry on failures on the Telegram server. * < 0 - retry indefinitely (default) * 0 - no retries * > 0 - retry up to X times read_timeout (:obj:`float`, optional): Value to pass to :paramref:`telegram.Bot.get_updates.read_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. versionchanged:: 20.7 Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE` instead of ``2``. .. deprecated:: 20.7 Deprecated in favor of setting the timeout via :meth:`telegram.ext.ApplicationBuilder.get_updates_read_timeout` or :paramref:`telegram.Bot.get_updates_request`. write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.Bot.get_updates.write_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. deprecated:: 20.7 Deprecated in favor of setting the timeout via :meth:`telegram.ext.ApplicationBuilder.get_updates_write_timeout` or :paramref:`telegram.Bot.get_updates_request`. connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.Bot.get_updates.connect_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. deprecated:: 20.7 Deprecated in favor of setting the timeout via :meth:`telegram.ext.ApplicationBuilder.get_updates_connect_timeout` or :paramref:`telegram.Bot.get_updates_request`. pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to :paramref:`telegram.Bot.get_updates.pool_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`. .. deprecated:: 20.7 Deprecated in favor of setting the timeout via :meth:`telegram.ext.ApplicationBuilder.get_updates_pool_timeout` or :paramref:`telegram.Bot.get_updates_request`. allowed_updates (List[:obj:`str`], optional): Passed to :meth:`telegram.Bot.get_updates`. drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers before actually starting to poll. Default is :obj:`False`. .. versionadded :: 13.4 error_callback (Callable[[:exc:`telegram.error.TelegramError`], :obj:`None`], \ optional): Callback to handle :exc:`telegram.error.TelegramError` s that occur while calling :meth:`telegram.Bot.get_updates` during polling. Defaults to :obj:`None`, in which case errors will be logged. Callback signature:: def callback(error: telegram.error.TelegramError) Note: The :paramref:`error_callback` must *not* be a :term:`coroutine function`! If asynchronous behavior of the callback is wanted, please schedule a task from within the callback. Returns: :class:`asyncio.Queue`: The update queue that can be filled from the main thread. Raises: :exc:`RuntimeError`: If the updater is already running or was not initialized. """ # We refrain from issuing deprecation warnings for the timeout parameters here, as we # already issue them in `Application`. This means that there are no warnings when using # `Updater` without `Application`, but this is a rather special use case. if error_callback and asyncio.iscoroutinefunction(error_callback): raise TypeError( "The `error_callback` must not be a coroutine function! Use an ordinary function " "instead. " ) async with self.__lock: if self.running: raise RuntimeError("This Updater is already running!") if not self._initialized: raise RuntimeError("This Updater was not initialized via `Updater.initialize`!") self._running = True try: # Create & start tasks polling_ready = asyncio.Event() await self._start_polling( poll_interval=poll_interval, timeout=timeout, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, bootstrap_retries=bootstrap_retries, drop_pending_updates=drop_pending_updates, allowed_updates=allowed_updates, ready=polling_ready, error_callback=error_callback, ) _LOGGER.debug("Waiting for polling to start") await polling_ready.wait() _LOGGER.debug("Polling updates from Telegram started") return self.update_queue except Exception as exc: self._running = False raise exc async def _start_polling( self, poll_interval: float, timeout: int, read_timeout: ODVInput[float], write_timeout: ODVInput[float], connect_timeout: ODVInput[float], pool_timeout: ODVInput[float], bootstrap_retries: int, drop_pending_updates: Optional[bool], allowed_updates: Optional[List[str]], ready: asyncio.Event, error_callback: Optional[Callable[[TelegramError], None]], ) -> None: _LOGGER.debug("Updater started (polling)") # the bootstrapping phase does two things: # 1) make sure there is no webhook set # 2) apply drop_pending_updates await self._bootstrap( bootstrap_retries, drop_pending_updates=drop_pending_updates, webhook_url="", allowed_updates=None, ) _LOGGER.debug("Bootstrap done") async def polling_action_cb() -> bool: try: updates = await self.bot.get_updates( offset=self._last_update_id, timeout=timeout, read_timeout=read_timeout, connect_timeout=connect_timeout, write_timeout=write_timeout, pool_timeout=pool_timeout, allowed_updates=allowed_updates, ) except TelegramError as exc: # TelegramErrors should be processed by the network retry loop raise exc except Exception as exc: # Other exceptions should not. Let's log them for now. _LOGGER.critical( "Something went wrong processing the data received from Telegram. " "Received data was *not* processed!", exc_info=exc, ) return True if updates: if not self.running: _LOGGER.critical( "Updater stopped unexpectedly. Pulled updates will be ignored and pulled " "again on restart." ) else: for update in updates: await self.update_queue.put(update) self._last_update_id = updates[-1].update_id + 1 # Add one to 'confirm' it return True # Keep fetching updates & don't quit. Polls with poll_interval. def default_error_callback(exc: TelegramError) -> None: _LOGGER.exception("Exception happened while polling for updates.", exc_info=exc) # Start task that runs in background, pulls # updates from Telegram and inserts them in the update queue of the # Application. self.__polling_task = asyncio.create_task( self._network_loop_retry( action_cb=polling_action_cb, on_err_cb=error_callback or default_error_callback, description="getting Updates", interval=poll_interval, stop_event=self.__polling_task_stop_event, ), name="Updater:start_polling:polling_task", ) # Prepare a cleanup callback to await on _stop_polling # Calling get_updates one more time with the latest `offset` parameter ensures that # all updates that where put into the update queue are also marked as "read" to TG, # so we do not receive them again on the next startup # We define this here so that we can use the same parameters as in the polling task async def _get_updates_cleanup() -> None: _LOGGER.debug( "Calling `get_updates` one more time to mark all fetched updates as read." ) try: await self.bot.get_updates( offset=self._last_update_id, # We don't want to do long polling here! timeout=0, read_timeout=read_timeout, connect_timeout=connect_timeout, write_timeout=write_timeout, pool_timeout=pool_timeout, allowed_updates=allowed_updates, ) except TelegramError as exc: _LOGGER.error( "Error while calling `get_updates` one more time to mark all fetched updates " "as read: %s. Suppressing error to ensure graceful shutdown. When polling for " "updates is restarted, updates may be fetched again. Please adjust timeouts " "via `ApplicationBuilder` or the parameter `get_updates_request` of `Bot`.", exc_info=exc, ) self.__polling_cleanup_cb = _get_updates_cleanup if ready is not None: ready.set() async def start_webhook( self, listen: DVType[str] = DEFAULT_IP, port: DVType[int] = DEFAULT_80, url_path: str = "", cert: Optional[Union[str, Path]] = None, key: Optional[Union[str, Path]] = None, bootstrap_retries: int = 0, webhook_url: Optional[str] = None, allowed_updates: Optional[List[str]] = None, drop_pending_updates: Optional[bool] = None, ip_address: Optional[str] = None, max_connections: int = 40, secret_token: Optional[str] = None, unix: Optional[Union[str, Path, "socket"]] = None, ) -> "asyncio.Queue[object]": """ Starts a small http server to listen for updates via webhook. If :paramref:`cert` and :paramref:`key` are not provided, the webhook will be started directly on ``http://listen:port/url_path``, so SSL can be handled by another application. Else, the webhook will be started on ``https://listen:port/url_path``. Also calls :meth:`telegram.Bot.set_webhook` as required. Important: If you want to use this method, you must install PTB with the optional requirement ``webhooks``, i.e. .. code-block:: bash pip install "python-telegram-bot[webhooks]" .. seealso:: :wiki:`Webhooks` .. versionchanged:: 13.4 :meth:`start_webhook` now *always* calls :meth:`telegram.Bot.set_webhook`, so pass ``webhook_url`` instead of calling ``updater.bot.set_webhook(webhook_url)`` manually. .. versionchanged:: 20.0 * Removed the ``clean`` argument in favor of :paramref:`drop_pending_updates` and removed the deprecated argument ``force_event_loop``. Args: listen (:obj:`str`, optional): IP-Address to listen on. Defaults to `127.0.0.1 `_. port (:obj:`int`, optional): Port the bot should be listening on. Must be one of :attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS` unless the bot is running behind a proxy. Defaults to ``80``. url_path (:obj:`str`, optional): Path inside url (http(s)://listen:port/). Defaults to ``''``. cert (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL certificate file. key (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL key file. drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers before actually starting to poll. Default is :obj:`False`. .. versionadded :: 13.4 bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the :class:`telegram.ext.Updater` will retry on failures on the Telegram server. * < 0 - retry indefinitely * 0 - no retries (default) * > 0 - retry up to X times webhook_url (:obj:`str`, optional): Explicitly specify the webhook url. Useful behind NAT, reverse proxy, etc. Default is derived from :paramref:`listen`, :paramref:`port`, :paramref:`url_path`, :paramref:`cert`, and :paramref:`key`. ip_address (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`. Defaults to :obj:`None`. .. versionadded :: 13.4 allowed_updates (List[:obj:`str`], optional): Passed to :meth:`telegram.Bot.set_webhook`. Defaults to :obj:`None`. max_connections (:obj:`int`, optional): Passed to :meth:`telegram.Bot.set_webhook`. Defaults to ``40``. .. versionadded:: 13.6 secret_token (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`. Defaults to :obj:`None`. When added, the web server started by this call will expect the token to be set in the ``X-Telegram-Bot-Api-Secret-Token`` header of an incoming request and will raise a :class:`http.HTTPStatus.FORBIDDEN ` error if either the header isn't set or it is set to a wrong token. .. versionadded:: 20.0 unix (:class:`pathlib.Path` | :obj:`str` | :class:`socket.socket`, optional): Can be either: * the path to the unix socket file as :class:`pathlib.Path` or :obj:`str`. This will be passed to `tornado.netutil.bind_unix_socket `_ to create the socket. If the Path does not exist, the file will be created. * or the socket itself. This option allows you to e.g. restrict the permissions of the socket for improved security. Note that you need to pass the correct family, type and socket options yourself. Caution: This parameter is a replacement for the default TCP bind. Therefore, it is mutually exclusive with :paramref:`listen` and :paramref:`port`. When using this param, you must also run a reverse proxy to the unix socket and set the appropriate :paramref:`webhook_url`. .. versionadded:: 20.8 .. versionchanged:: 21.1 Added support to pass a socket instance itself. Returns: :class:`queue.Queue`: The update queue that can be filled from the main thread. Raises: :exc:`RuntimeError`: If the updater is already running or was not initialized. """ if not WEBHOOKS_AVAILABLE: raise RuntimeError( "To use `start_webhook`, PTB must be installed via `pip install " '"python-telegram-bot[webhooks]"`.' ) # unix has special requirements what must and mustn't be set when using it if unix: error_msg = ( "You can not pass unix and {0}, only use one. Unix if you want to " "initialize a unix socket, or {0} for a standard TCP server." ) if not isinstance(listen, DefaultValue): raise RuntimeError(error_msg.format("listen")) if not isinstance(port, DefaultValue): raise RuntimeError(error_msg.format("port")) if not webhook_url: raise RuntimeError( "Since you set unix, you also need to set the URL to the webhook " "of the proxy you run in front of the unix socket." ) async with self.__lock: if self.running: raise RuntimeError("This Updater is already running!") if not self._initialized: raise RuntimeError("This Updater was not initialized via `Updater.initialize`!") self._running = True try: # Create & start tasks webhook_ready = asyncio.Event() await self._start_webhook( listen=DefaultValue.get_value(listen), port=DefaultValue.get_value(port), url_path=url_path, cert=cert, key=key, bootstrap_retries=bootstrap_retries, drop_pending_updates=drop_pending_updates, webhook_url=webhook_url, allowed_updates=allowed_updates, ready=webhook_ready, ip_address=ip_address, max_connections=max_connections, secret_token=secret_token, unix=unix, ) _LOGGER.debug("Waiting for webhook server to start") await webhook_ready.wait() _LOGGER.debug("Webhook server started") except Exception as exc: self._running = False raise exc # Return the update queue so the main thread can insert updates return self.update_queue async def _start_webhook( self, listen: str, port: int, url_path: str, bootstrap_retries: int, allowed_updates: Optional[List[str]], cert: Optional[Union[str, Path]] = None, key: Optional[Union[str, Path]] = None, drop_pending_updates: Optional[bool] = None, webhook_url: Optional[str] = None, ready: Optional[asyncio.Event] = None, ip_address: Optional[str] = None, max_connections: int = 40, secret_token: Optional[str] = None, unix: Optional[Union[str, Path, "socket"]] = None, ) -> None: _LOGGER.debug("Updater thread started (webhook)") if not url_path.startswith("/"): url_path = f"/{url_path}" # Create Tornado app instance app = WebhookAppClass(url_path, self.bot, self.update_queue, secret_token) # Form SSL Context # An SSLError is raised if the private key does not match with the certificate # Note that we only use the SSL certificate for the WebhookServer, if the key is also # present. This is because the WebhookServer may not actually be in charge of performing # the SSL handshake, e.g. in case a reverse proxy is used if cert is not None and key is not None: try: ssl_ctx: Optional[ssl.SSLContext] = ssl.create_default_context( ssl.Purpose.CLIENT_AUTH ) ssl_ctx.load_cert_chain(cert, key) # type: ignore[union-attr] except ssl.SSLError as exc: raise TelegramError("Invalid SSL Certificate") from exc else: ssl_ctx = None # Create and start server self._httpd = WebhookServer(listen, port, app, ssl_ctx, unix) if not webhook_url: webhook_url = self._gen_webhook_url( protocol="https" if ssl_ctx else "http", listen=DefaultValue.get_value(listen), port=port, url_path=url_path, ) # We pass along the cert to the webhook if present. await self._bootstrap( # Passing a Path or string only works if the bot is running against a local bot API # server, so let's read the contents cert=Path(cert).read_bytes() if cert else None, max_retries=bootstrap_retries, drop_pending_updates=drop_pending_updates, webhook_url=webhook_url, allowed_updates=allowed_updates, ip_address=ip_address, max_connections=max_connections, secret_token=secret_token, ) await self._httpd.serve_forever(ready=ready) @staticmethod def _gen_webhook_url(protocol: str, listen: str, port: int, url_path: str) -> str: # TODO: double check if this should be https in any case - the docs of start_webhook # say differently! return f"{protocol}://{listen}:{port}{url_path}" async def _network_loop_retry( self, action_cb: Callable[..., Coroutine], on_err_cb: Callable[[TelegramError], None], description: str, interval: float, stop_event: Optional[asyncio.Event], ) -> None: """Perform a loop calling `action_cb`, retrying after network errors. Stop condition for loop: `self.running` evaluates :obj:`False` or return value of `action_cb` evaluates :obj:`False`. Args: action_cb (:term:`coroutine function`): Network oriented callback function to call. on_err_cb (:obj:`callable`): Callback to call when TelegramError is caught. Receives the exception object as a parameter. description (:obj:`str`): Description text to use for logs and exception raised. interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to `action_cb`. stop_event (:class:`asyncio.Event` | :obj:`None`): Event to wait on for stopping the loop. Setting the event will make the loop exit even if `action_cb` is currently running. """ async def do_action() -> bool: if not stop_event: return await action_cb() action_cb_task = asyncio.create_task(action_cb()) stop_task = asyncio.create_task(stop_event.wait()) done, pending = await asyncio.wait( (action_cb_task, stop_task), return_when=asyncio.FIRST_COMPLETED ) with contextlib.suppress(asyncio.CancelledError): for task in pending: task.cancel() if stop_task in done: _LOGGER.debug("Network loop retry %s was cancelled", description) return False return action_cb_task.result() _LOGGER.debug("Start network loop retry %s", description) cur_interval = interval while self.running: try: if not await do_action(): break except RetryAfter as exc: _LOGGER.info("%s", exc) cur_interval = 0.5 + exc.retry_after except TimedOut as toe: _LOGGER.debug("Timed out %s: %s", description, toe) # If failure is due to timeout, we should retry asap. cur_interval = 0 except InvalidToken as pex: _LOGGER.error("Invalid token; aborting") raise pex except TelegramError as telegram_exc: _LOGGER.error("Error while %s: %s", description, telegram_exc) on_err_cb(telegram_exc) # increase waiting times on subsequent errors up to 30secs cur_interval = 1 if cur_interval == 0 else min(30, 1.5 * cur_interval) else: cur_interval = interval if cur_interval: await asyncio.sleep(cur_interval) async def _bootstrap( self, max_retries: int, webhook_url: Optional[str], allowed_updates: Optional[List[str]], drop_pending_updates: Optional[bool] = None, cert: Optional[bytes] = None, bootstrap_interval: float = 1, ip_address: Optional[str] = None, max_connections: int = 40, secret_token: Optional[str] = None, ) -> None: """Prepares the setup for fetching updates: delete or set the webhook and drop pending updates if appropriate. If there are unsuccessful attempts, this will retry as specified by :paramref:`max_retries`. """ retries = 0 async def bootstrap_del_webhook() -> bool: _LOGGER.debug("Deleting webhook") if drop_pending_updates: _LOGGER.debug("Dropping pending updates from Telegram server") await self.bot.delete_webhook(drop_pending_updates=drop_pending_updates) return False async def bootstrap_set_webhook() -> bool: _LOGGER.debug("Setting webhook") if drop_pending_updates: _LOGGER.debug("Dropping pending updates from Telegram server") await self.bot.set_webhook( url=webhook_url, # type: ignore[arg-type] certificate=cert, allowed_updates=allowed_updates, ip_address=ip_address, drop_pending_updates=drop_pending_updates, max_connections=max_connections, secret_token=secret_token, ) return False def bootstrap_on_err_cb(exc: Exception) -> None: # We need this since retries is an immutable object otherwise and the changes # wouldn't propagate outside of thi function nonlocal retries if not isinstance(exc, InvalidToken) and (max_retries < 0 or retries < max_retries): retries += 1 _LOGGER.warning( "Failed bootstrap phase; try=%s max_retries=%s", retries, max_retries ) else: _LOGGER.error("Failed bootstrap phase after %s retries (%s)", retries, exc) raise exc # Dropping pending updates from TG can be efficiently done with the drop_pending_updates # parameter of delete/start_webhook, even in the case of polling. Also, we want to make # sure that no webhook is configured in case of polling, so we just always call # delete_webhook for polling if drop_pending_updates or not webhook_url: await self._network_loop_retry( bootstrap_del_webhook, bootstrap_on_err_cb, "bootstrap del webhook", bootstrap_interval, stop_event=None, ) # Reset the retries counter for the next _network_loop_retry call retries = 0 # Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set, # so we set it anyhow. if webhook_url: await self._network_loop_retry( bootstrap_set_webhook, bootstrap_on_err_cb, "bootstrap set webhook", bootstrap_interval, stop_event=None, ) async def stop(self) -> None: """Stops the polling/webhook. .. seealso:: :meth:`start_polling`, :meth:`start_webhook` Raises: :exc:`RuntimeError`: If the updater is not running. """ async with self.__lock: if not self.running: raise RuntimeError("This Updater is not running!") _LOGGER.debug("Stopping Updater") self._running = False await self._stop_httpd() await self._stop_polling() _LOGGER.debug("Updater.stop() is complete") async def _stop_httpd(self) -> None: """Stops the Webhook server by calling ``WebhookServer.shutdown()``""" if self._httpd: _LOGGER.debug("Waiting for current webhook connection to be closed.") await self._httpd.shutdown() self._httpd = None async def _stop_polling(self) -> None: """Stops the polling task by awaiting it.""" if self.__polling_task: _LOGGER.debug("Waiting background polling task to finish up.") self.__polling_task_stop_event.set() with contextlib.suppress(asyncio.CancelledError): await self.__polling_task # It only fails in rare edge-cases, e.g. when `stop()` is called directly # after start_polling(), but lets better be safe than sorry ... self.__polling_task = None self.__polling_task_stop_event.clear() if self.__polling_cleanup_cb: await self.__polling_cleanup_cb() self.__polling_cleanup_cb = None else: _LOGGER.warning( "No polling cleanup callback defined. The last fetched updates may be " "fetched again on the next polling start." ) python-telegram-bot-21.1.1/telegram/ext/_utils/000077500000000000000000000000001460724040100213725ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/ext/_utils/__init__.py000066400000000000000000000014401460724040100235020ustar00rootroot00000000000000# # 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/]. python-telegram-bot-21.1.1/telegram/ext/_utils/_update_parsing.py000066400000000000000000000037151460724040100251160ustar00rootroot00000000000000#!/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 helper functions related to parsing updates and their contents. .. versionadded:: 20.8 Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ from typing import FrozenSet, Optional from telegram._utils.types import SCT def parse_chat_id(chat_id: Optional[SCT[int]]) -> FrozenSet[int]: """Accepts a chat id or collection of chat ids and returns a frozenset of chat ids.""" if chat_id is None: return frozenset() if isinstance(chat_id, int): return frozenset({chat_id}) return frozenset(chat_id) def parse_username(username: Optional[SCT[str]]) -> FrozenSet[str]: """Accepts a username or collection of usernames and returns a frozenset of usernames. Strips the leading ``@`` if present. """ if username is None: return frozenset() if isinstance(username, str): return frozenset({username[1:] if username.startswith("@") else username}) return frozenset({usr[1:] if usr.startswith("@") else usr for usr in username}) python-telegram-bot-21.1.1/telegram/ext/_utils/stack.py000066400000000000000000000050321460724040100230510ustar00rootroot00000000000000#!/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 helper functions related to inspecting the program stack. .. versionadded:: 20.0 Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ from pathlib import Path from types import FrameType from typing import Optional from telegram._utils.logging import get_logger _LOGGER = get_logger(__name__) def was_called_by(frame: Optional[FrameType], caller: Path) -> bool: """Checks if the passed frame was called by the specified file. Example: .. code:: pycon >>> was_called_by(inspect.currentframe(), Path(__file__)) True Arguments: frame (:obj:`FrameType`): The frame - usually the return value of ``inspect.currentframe()``. If :obj:`None` is passed, the return value will be :obj:`False`. caller (:obj:`pathlib.Path`): File that should be the caller. Returns: :obj:`bool`: Whether the frame was called by the specified file. """ if frame is None: return False try: return _was_called_by(frame, caller) except Exception as exc: _LOGGER.debug( "Failed to check if frame was called by `caller`. Assuming that it was not.", exc_info=exc, ) return False def _was_called_by(frame: FrameType, caller: Path) -> bool: # https://stackoverflow.com/a/57712700/10606962 if Path(frame.f_code.co_filename).resolve() == caller: return True while frame.f_back: frame = frame.f_back if Path(frame.f_code.co_filename).resolve() == caller: return True return False python-telegram-bot-21.1.1/telegram/ext/_utils/trackingdict.py000066400000000000000000000107541460724040100244210ustar00rootroot00000000000000#!/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 mutable mapping that keeps track of the keys that where accessed. .. versionadded:: 20.0 Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ from collections import UserDict from typing import Final, Generic, List, Mapping, Optional, Set, Tuple, TypeVar, Union from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue _VT = TypeVar("_VT") _KT = TypeVar("_KT") _T = TypeVar("_T") class TrackingDict(UserDict, Generic[_KT, _VT]): """Mutable mapping that keeps track of which keys where accessed with write access. Read-access is not tracked. Note: * ``setdefault()`` and ``pop`` are considered writing only depending on whether the key is present * deleting values is considered writing """ DELETED: Final = object() """Special marker indicating that an entry was deleted.""" __slots__ = ("_write_access_keys",) def __init__(self) -> None: super().__init__() self._write_access_keys: Set[_KT] = set() def __setitem__(self, key: _KT, value: _VT) -> None: self.__track_write(key) super().__setitem__(key, value) def __delitem__(self, key: _KT) -> None: self.__track_write(key) super().__delitem__(key) def __track_write(self, key: Union[_KT, Set[_KT]]) -> None: if isinstance(key, set): self._write_access_keys |= key else: self._write_access_keys.add(key) def pop_accessed_keys(self) -> Set[_KT]: """Returns all keys that were write-accessed since the last time this method was called.""" out = self._write_access_keys self._write_access_keys = set() return out def pop_accessed_write_items(self) -> List[Tuple[_KT, _VT]]: """ Returns all keys & corresponding values as set of tuples that were write-accessed since the last time this method was called. If a key was deleted, the value will be :attr:`DELETED`. """ keys = self.pop_accessed_keys() return [(key, self.get(key, self.DELETED)) for key in keys] def mark_as_accessed(self, key: _KT) -> None: """Use this method have the key returned again in the next call to :meth:`pop_accessed_write_items` or :meth:`pop_accessed_keys` """ self._write_access_keys.add(key) # Override methods to track access def update_no_track(self, mapping: Mapping[_KT, _VT]) -> None: """Like ``update``, but doesn't count towards write access.""" for key, value in mapping.items(): self.data[key] = value # Mypy seems a bit inconsistent about what it wants as types for `default` and return value # so we just ignore a bit def pop( # type: ignore[override] self, key: _KT, default: _VT = DEFAULT_NONE, # type: ignore[assignment] ) -> _VT: if key in self: self.__track_write(key) if isinstance(default, DefaultValue): return super().pop(key) return super().pop(key, default=default) def clear(self) -> None: self.__track_write(set(super().keys())) super().clear() # Mypy seems a bit inconsistent about what it wants as types for `default` and return value # so we just ignore a bit def setdefault(self: "TrackingDict[_KT, _T]", key: _KT, default: Optional[_T] = None) -> _T: if key in self: return self[key] self.__track_write(key) self[key] = default # type: ignore[assignment] return default # type: ignore[return-value] python-telegram-bot-21.1.1/telegram/ext/_utils/types.py000066400000000000000000000060751460724040100231200ustar00rootroot00000000000000#!/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 custom typing aliases. .. versionadded:: 13.6 Warning: Contents of this module are intended to be used internally by the library and *not* by the user. Changes to this module are not considered breaking changes and may not be documented in the changelog. """ from typing import ( TYPE_CHECKING, Any, Callable, Coroutine, Dict, List, MutableMapping, Tuple, TypeVar, Union, ) if TYPE_CHECKING: from typing import Optional from telegram import Bot from telegram.ext import BaseRateLimiter, CallbackContext, JobQueue CCT = TypeVar("CCT", bound="CallbackContext[Any, Any, Any, Any]") """An instance of :class:`telegram.ext.CallbackContext` or a custom subclass. .. versionadded:: 13.6 """ RT = TypeVar("RT") UT = TypeVar("UT") HandlerCallback = Callable[[UT, CCT], Coroutine[Any, Any, RT]] """Type of a handler callback .. versionadded:: 20.0 """ JobCallback = Callable[[CCT], Coroutine[Any, Any, Any]] """Type of a job callback .. versionadded:: 20.0 """ ConversationKey = Tuple[Union[int, str], ...] ConversationDict = MutableMapping[ConversationKey, object] """Dict[Tuple[:obj:`int` | :obj:`str`, ...], Optional[:obj:`object`]]: Dicts as maintained by the :class:`telegram.ext.ConversationHandler`. .. versionadded:: 13.6 """ CDCData = Tuple[List[Tuple[str, float, Dict[str, Any]]], Dict[str, str]] """Tuple[List[Tuple[:obj:`str`, :obj:`float`, Dict[:obj:`str`, :class:`object`]]], \ Dict[:obj:`str`, :obj:`str`]]: Data returned by :attr:`telegram.ext.CallbackDataCache.persistence_data`. .. versionadded:: 13.6 """ BT = TypeVar("BT", bound="Bot") """Type of the bot. .. versionadded:: 20.0 """ UD = TypeVar("UD") """Type of the user data for a single user. .. versionadded:: 13.6 """ CD = TypeVar("CD") """Type of the chat data for a single user. .. versionadded:: 13.6 """ BD = TypeVar("BD") """Type of the bot data. .. versionadded:: 13.6 """ JQ = TypeVar("JQ", bound=Union[None, "JobQueue"]) """Type of the job queue. .. versionadded:: 20.0""" RL = TypeVar("RL", bound="Optional[BaseRateLimiter]") """Type of the rate limiter. .. versionadded:: 20.0""" RLARGS = TypeVar("RLARGS") """Type of the rate limiter arguments. .. versionadded:: 20.0""" FilterDataDict = Dict[str, List[Any]] python-telegram-bot-21.1.1/telegram/ext/_utils/webhookhandler.py000066400000000000000000000174671460724040100247570ustar00rootroot00000000000000#!/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 import asyncio import json from http import HTTPStatus from pathlib import Path from socket import socket from ssl import SSLContext from types import TracebackType from typing import TYPE_CHECKING, Optional, Type, Union # Instead of checking for ImportError here, we do that in `updater.py`, where we import from # this module. Doing it here would be tricky, as the classes below subclass tornado classes import tornado.web from tornado.httpserver import HTTPServer try: from tornado.netutil import bind_unix_socket UNIX_AVAILABLE = True except ImportError: UNIX_AVAILABLE = False from telegram import Update from telegram._utils.logging import get_logger from telegram.ext._extbot import ExtBot if TYPE_CHECKING: from telegram import Bot # This module is not visible to users, so we log as Updater _LOGGER = get_logger(__name__, class_name="Updater") class WebhookServer: """Thin wrapper around ``tornado.httpserver.HTTPServer``.""" __slots__ = ( "_http_server", "_server_lock", "_shutdown_lock", "is_running", "listen", "port", "unix", ) def __init__( self, listen: str, port: int, webhook_app: "WebhookAppClass", ssl_ctx: Optional[SSLContext], unix: Optional[Union[str, Path, socket]] = None, ): if unix and not UNIX_AVAILABLE: raise RuntimeError("This OS does not support binding unix sockets.") self._http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx) self.listen = listen self.port = port self.is_running = False self.unix = None if unix and isinstance(unix, socket): self.unix = unix elif unix: self.unix = bind_unix_socket(str(unix)) self._server_lock = asyncio.Lock() self._shutdown_lock = asyncio.Lock() async def serve_forever(self, ready: Optional[asyncio.Event] = None) -> None: async with self._server_lock: if self.unix: self._http_server.add_socket(self.unix) else: self._http_server.listen(self.port, address=self.listen) self.is_running = True if ready is not None: ready.set() _LOGGER.debug("Webhook Server started.") async def shutdown(self) -> None: async with self._shutdown_lock: if not self.is_running: _LOGGER.debug("Webhook Server is already shut down. Returning") return self.is_running = False self._http_server.stop() await self._http_server.close_all_connections() _LOGGER.debug("Webhook Server stopped") class WebhookAppClass(tornado.web.Application): """Application used in the Webserver""" def __init__( self, webhook_path: str, bot: "Bot", update_queue: asyncio.Queue, secret_token: Optional[str] = None, ): self.shared_objects = { "bot": bot, "update_queue": update_queue, "secret_token": secret_token, } handlers = [(rf"{webhook_path}/?", TelegramHandler, self.shared_objects)] tornado.web.Application.__init__(self, handlers) # type: ignore def log_request(self, handler: tornado.web.RequestHandler) -> None: """Overrides the default implementation since we have our own logging setup.""" # pylint: disable=abstract-method class TelegramHandler(tornado.web.RequestHandler): """BaseHandler that processes incoming requests from Telegram""" __slots__ = ("bot", "secret_token", "update_queue") SUPPORTED_METHODS = ("POST",) # type: ignore[assignment] def initialize(self, bot: "Bot", update_queue: asyncio.Queue, secret_token: str) -> None: """Initialize for each request - that's the interface provided by tornado""" # pylint: disable=attribute-defined-outside-init self.bot = bot self.update_queue = update_queue self.secret_token = secret_token if secret_token: _LOGGER.debug( "The webhook server has a secret token, expecting it in incoming requests now" ) def set_default_headers(self) -> None: """Sets default headers""" self.set_header("Content-Type", 'application/json; charset="utf-8"') async def post(self) -> None: """Handle incoming POST request""" _LOGGER.debug("Webhook triggered") self._validate_post() json_string = self.request.body.decode() data = json.loads(json_string) self.set_status(HTTPStatus.OK) _LOGGER.debug("Webhook received data: %s", json_string) try: update = Update.de_json(data, self.bot) except Exception as exc: _LOGGER.critical( "Something went wrong processing the data received from Telegram. " "Received data was *not* processed!", exc_info=exc, ) raise tornado.web.HTTPError( HTTPStatus.BAD_REQUEST, reason="Update could not be processed" ) from exc if update: _LOGGER.debug( "Received Update with ID %d on Webhook", # For some reason pylint thinks update is a general TelegramObject update.update_id, # pylint: disable=no-member ) # handle arbitrary callback data, if necessary if isinstance(self.bot, ExtBot): self.bot.insert_callback_data(update) await self.update_queue.put(update) def _validate_post(self) -> None: """Only accept requests with content type JSON""" ct_header = self.request.headers.get("Content-Type", None) if ct_header != "application/json": raise tornado.web.HTTPError(HTTPStatus.FORBIDDEN) # verifying that the secret token is the one the user set when the user set one if self.secret_token is not None: token = self.request.headers.get("X-Telegram-Bot-Api-Secret-Token") if not token: _LOGGER.debug("Request did not include the secret token") raise tornado.web.HTTPError( HTTPStatus.FORBIDDEN, reason="Request did not include the secret token" ) if token != self.secret_token: _LOGGER.debug("Request had the wrong secret token: %s", token) raise tornado.web.HTTPError( HTTPStatus.FORBIDDEN, reason="Request had the wrong secret token" ) def log_exception( self, typ: Optional[Type[BaseException]], value: Optional[BaseException], tb: Optional[TracebackType], ) -> None: """Override the default logging and instead use our custom logging.""" _LOGGER.debug( "%s - %s", self.request.remote_ip, "Exception in TelegramHandler", exc_info=(typ, value, tb) if typ and value and tb else value, ) python-telegram-bot-21.1.1/telegram/ext/filters.py000066400000000000000000002770651460724040100221360ustar00rootroot00000000000000#!/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 filters for use with :class:`telegram.ext.MessageHandler`, :class:`telegram.ext.CommandHandler`, or :class:`telegram.ext.PrefixHandler`. .. versionchanged:: 20.0 #. Filters are no longer callable, if you're using a custom filter and are calling an existing filter, then switch to the new syntax: ``filters.{filter}.check_update(update)``. #. Removed the ``Filters`` class. The filters are now directly attributes/classes of the :mod:`~telegram.ext.filters` module. #. The names of all filters has been updated: * Filter classes which are ready for use, e.g ``Filters.all`` are now capitalized, e.g ``filters.ALL``. * Filters which need to be initialized are now in CamelCase. E.g. ``filters.User(...)``. * Filters which do both (like ``Filters.text``) are now split as ready-to-use version ``filters.TEXT`` and class version ``filters.Text(...)``. """ __all__ = ( "ALL", "ANIMATION", "ATTACHMENT", "AUDIO", "BOOST_ADDED", "CAPTION", "CHAT", "COMMAND", "CONTACT", "FORWARDED", "GAME", "GIVEAWAY", "GIVEAWAY_WINNERS", "HAS_MEDIA_SPOILER", "HAS_PROTECTED_CONTENT", "INVOICE", "IS_AUTOMATIC_FORWARD", "IS_FROM_OFFLINE", "IS_TOPIC_MESSAGE", "LOCATION", "PASSPORT_DATA", "PHOTO", "POLL", "PREMIUM_USER", "REPLY", "REPLY_TO_STORY", "SENDER_BOOST_COUNT", "STORY", "SUCCESSFUL_PAYMENT", "TEXT", "USER", "USER_ATTACHMENT", "VENUE", "VIA_BOT", "VIDEO", "VIDEO_NOTE", "VOICE", "BaseFilter", "Caption", "CaptionEntity", "CaptionRegex", "Chat", "ChatType", "Command", "Dice", "Document", "Entity", "ForwardedFrom", "Language", "Mention", "MessageFilter", "Regex", "SenderChat", "StatusUpdate", "Sticker", "SuccessfulPayment", "Text", "UpdateFilter", "UpdateType", "User", "ViaBot", ) import mimetypes import re from abc import ABC, abstractmethod from typing import ( Collection, Dict, FrozenSet, Iterable, List, Match, NoReturn, Optional, Pattern, Sequence, Set, Tuple, Union, cast, ) from telegram import Chat as TGChat from telegram import ( Message, MessageEntity, MessageOriginChannel, MessageOriginChat, MessageOriginUser, Update, ) from telegram import User as TGUser from telegram._utils.types import SCT from telegram.constants import DiceEmoji as DiceEmojiEnum from telegram.ext._utils._update_parsing import parse_chat_id, parse_username from telegram.ext._utils.types import FilterDataDict class BaseFilter: """Base class for all Filters. Filters subclassing from this class can combined using bitwise operators: And:: filters.TEXT & filters.Entity(MENTION) Or:: filters.AUDIO | filters.VIDEO Exclusive Or:: filters.Regex('To Be') ^ filters.Regex('Not 2B') Not:: ~ filters.COMMAND Also works with more than two filters:: filters.TEXT & (filters.Entity("url") | filters.Entity("text_link")) filters.TEXT & (~ filters.FORWARDED) Note: Filters use the same short circuiting logic as python's :keyword:`and`, :keyword:`or` and :keyword:`not`. This means that for example:: filters.Regex(r'(a?x)') | filters.Regex(r'(b?x)') With ``message.text == 'x'``, will only ever return the matches for the first filter, since the second one is never evaluated. If you want to create your own filters create a class inheriting from either :class:`MessageFilter` or :class:`UpdateFilter` and implement a ``filter()`` method that returns a boolean: :obj:`True` if the message should be handled, :obj:`False` otherwise. Note that the filters work only as class instances, not actual class objects (so remember to initialize your filter classes). By default, the filters name (what will get printed when converted to a string for display) will be the class name. If you want to overwrite this assign a better name to the :attr:`name` class variable. .. versionadded:: 20.0 Added the arguments :attr:`name` and :attr:`data_filter`. Args: name (:obj:`str`): Name for this filter. Defaults to the type of filter. data_filter (:obj:`bool`): Whether this filter is a data filter. A data filter should return a dict with lists. The dict will be merged with :class:`telegram.ext.CallbackContext`'s internal dict in most cases (depends on the handler). """ __slots__ = ("_data_filter", "_name") def __init__(self, name: Optional[str] = None, data_filter: bool = False): self._name = self.__class__.__name__ if name is None else name self._data_filter = data_filter def __and__(self, other: "BaseFilter") -> "BaseFilter": """Defines `AND` bitwise operator for :class:`BaseFilter` object. The combined filter accepts an update only if it is accepted by both filters. For example, ``filters.PHOTO & filters.CAPTION`` will only accept messages that contain both a photo and a caption. Returns: :obj:`BaseFilter` """ return _MergedFilter(self, and_filter=other) def __or__(self, other: "BaseFilter") -> "BaseFilter": """Defines `OR` bitwise operator for :class:`BaseFilter` object. The combined filter accepts an update only if it is accepted by any of the filters. For example, ``filters.PHOTO | filters.CAPTION`` will only accept messages that contain photo or caption or both. Returns: :obj:`BaseFilter` """ return _MergedFilter(self, or_filter=other) def __xor__(self, other: "BaseFilter") -> "BaseFilter": """Defines `XOR` bitwise operator for :class:`BaseFilter` object. The combined filter accepts an update only if it is accepted by any of the filters and not both of them. For example, ``filters.PHOTO ^ filters.CAPTION`` will only accept messages that contain photo or caption, not both of them. Returns: :obj:`BaseFilter` """ return _XORFilter(self, other) def __invert__(self) -> "BaseFilter": """Defines `NOT` bitwise operator for :class:`BaseFilter` object. The combined filter accepts an update only if it is accepted by any of the filters. For example, ``~ filters.PHOTO`` will only accept messages that do not contain photo. Returns: :obj:`BaseFilter` """ return _InvertedFilter(self) def __repr__(self) -> str: """Gives name for this filter. .. seealso:: :meth:`name` Returns: :obj:`str`: """ return self.name @property def data_filter(self) -> bool: """:obj:`bool`: Whether this filter is a data filter.""" return self._data_filter @data_filter.setter def data_filter(self, value: bool) -> None: self._data_filter = value @property def name(self) -> str: """:obj:`str`: Name for this filter.""" return self._name @name.setter def name(self, name: str) -> None: self._name = name def check_update(self, update: Update) -> Optional[Union[bool, FilterDataDict]]: """Checks if the specified update should be handled by this filter. .. versionchanged:: 21.1 This filter now also returns :obj:`True` if the update contains :attr:`~telegram.Update.business_message` or :attr:`~telegram.Update.edited_business_message`. Args: update (:class:`telegram.Update`): The update to check. Returns: :obj:`bool`: :obj:`True` if the update contains one of :attr:`~telegram.Update.channel_post`, :attr:`~telegram.Update.message`, :attr:`~telegram.Update.edited_channel_post`, :attr:`~telegram.Update.edited_message`, :attr:`telegram.Update.business_message`, :attr:`telegram.Update.edited_business_message`, or :obj:`False` otherwise. """ return bool( # Only message updates should be handled. update.channel_post # pylint: disable=too-many-boolean-expressions or update.message or update.edited_channel_post or update.edited_message or update.business_message or update.edited_business_message ) class MessageFilter(BaseFilter): """Base class for all Message Filters. In contrast to :class:`UpdateFilter`, the object passed to :meth:`filter` is :obj:`telegram.Update.effective_message`. Please see :class:`BaseFilter` for details on how to create custom filters. .. seealso:: :wiki:`Advanced Filters ` """ __slots__ = () def check_update(self, update: Update) -> Optional[Union[bool, FilterDataDict]]: """Checks if the specified update should be handled by this filter by passing :attr:`~telegram.Update.effective_message` to :meth:`filter`. Args: update (:class:`telegram.Update`): The update to check. Returns: :obj:`bool` | Dict[:obj:`str`, :obj:`list`] | :obj:`None`: If the update should be handled by this filter, returns :obj:`True` or a dict with lists, in case the filter is a data filter. If the update should not be handled by this filter, :obj:`False` or :obj:`None`. """ if super().check_update(update): return self.filter(update.effective_message) # type: ignore[arg-type] return False @abstractmethod def filter(self, message: Message) -> Optional[Union[bool, FilterDataDict]]: """This method must be overwritten. Args: message (:class:`telegram.Message`): The message that is tested. Returns: :obj:`dict` or :obj:`bool` """ class UpdateFilter(BaseFilter): """Base class for all Update Filters. In contrast to :class:`MessageFilter`, the object passed to :meth:`filter` is an instance of :class:`telegram.Update`, which allows to create filters like :attr:`telegram.ext.filters.UpdateType.EDITED_MESSAGE`. Please see :class:`telegram.ext.filters.BaseFilter` for details on how to create custom filters. """ __slots__ = () def check_update(self, update: Update) -> Optional[Union[bool, FilterDataDict]]: """Checks if the specified update should be handled by this filter. Args: update (:class:`telegram.Update`): The update to check. Returns: :obj:`bool` | Dict[:obj:`str`, :obj:`list`] | :obj:`None`: If the update should be handled by this filter, returns :obj:`True` or a dict with lists, in case the filter is a data filter. If the update should not be handled by this filter, :obj:`False` or :obj:`None`. """ return self.filter(update) if super().check_update(update) else False @abstractmethod def filter(self, update: Update) -> Optional[Union[bool, FilterDataDict]]: """This method must be overwritten. Args: update (:class:`telegram.Update`): The update that is tested. Returns: :obj:`dict` or :obj:`bool`. """ class _InvertedFilter(UpdateFilter): """Represents a filter that has been inverted. Args: f: The filter to invert. """ __slots__ = ("inv_filter",) def __init__(self, f: BaseFilter): super().__init__() self.inv_filter = f def filter(self, update: Update) -> bool: return not bool(self.inv_filter.check_update(update)) @property def name(self) -> str: return f"" @name.setter def name(self, name: str) -> NoReturn: raise RuntimeError("Cannot set name for combined filters.") class _MergedFilter(UpdateFilter): """Represents a filter consisting of two other filters. Args: base_filter: Filter 1 of the merged filter. and_filter: Optional filter to "and" with base_filter. Mutually exclusive with or_filter. or_filter: Optional filter to "or" with base_filter. Mutually exclusive with and_filter. """ __slots__ = ("and_filter", "base_filter", "or_filter") def __init__( self, base_filter: BaseFilter, and_filter: Optional[BaseFilter] = None, or_filter: Optional[BaseFilter] = None, ): super().__init__() self.base_filter = base_filter if self.base_filter.data_filter: self.data_filter = True self.and_filter = and_filter if ( self.and_filter and not isinstance(self.and_filter, bool) and self.and_filter.data_filter ): self.data_filter = True self.or_filter = or_filter if self.or_filter and not isinstance(self.and_filter, bool) and self.or_filter.data_filter: self.data_filter = True @staticmethod def _merge(base_output: Union[bool, Dict], comp_output: Union[bool, Dict]) -> FilterDataDict: base = base_output if isinstance(base_output, dict) else {} comp = comp_output if isinstance(comp_output, dict) else {} for k in comp: # Make sure comp values are lists comp_value = comp[k] if isinstance(comp[k], list) else [] try: # If base is a list then merge if isinstance(base[k], list): base[k] += comp_value else: base[k] = [base[k], *comp_value] except KeyError: base[k] = comp_value return base # pylint: disable=too-many-return-statements def filter(self, update: Update) -> Union[bool, FilterDataDict]: base_output = self.base_filter.check_update(update) # We need to check if the filters are data filters and if so return the merged data. # If it's not a data filter or an or_filter but no matches return bool if self.and_filter: # And filter needs to short circuit if base is falsy if base_output: comp_output = self.and_filter.check_update(update) if comp_output: if self.data_filter: merged = self._merge(base_output, comp_output) if merged: return merged return True elif self.or_filter: # Or filter needs to short circuit if base is truthy if base_output: if self.data_filter: return base_output return True comp_output = self.or_filter.check_update(update) if comp_output: if self.data_filter: return comp_output return True return False @property def name(self) -> str: return ( f"<{self.base_filter} {'and' if self.and_filter else 'or'} " f"{self.and_filter or self.or_filter}>" ) @name.setter def name(self, name: str) -> NoReturn: raise RuntimeError("Cannot set name for combined filters.") class _XORFilter(UpdateFilter): """Convenience filter acting as wrapper for :class:`MergedFilter` representing the an XOR gate for two filters. Args: base_filter: Filter 1 of the merged filter. xor_filter: Filter 2 of the merged filter. """ __slots__ = ("base_filter", "merged_filter", "xor_filter") def __init__(self, base_filter: BaseFilter, xor_filter: BaseFilter): super().__init__() self.base_filter = base_filter self.xor_filter = xor_filter self.merged_filter = (base_filter & ~xor_filter) | (~base_filter & xor_filter) def filter(self, update: Update) -> Optional[Union[bool, FilterDataDict]]: return self.merged_filter.check_update(update) @property def name(self) -> str: return f"<{self.base_filter} xor {self.xor_filter}>" @name.setter def name(self, name: str) -> NoReturn: raise RuntimeError("Cannot set name for combined filters.") class _All(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return True ALL = _All(name="filters.ALL") """All Messages.""" class _Animation(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.animation) ANIMATION = _Animation(name="filters.ANIMATION") """Messages that contain :attr:`telegram.Message.animation`.""" class _Attachment(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.effective_attachment) ATTACHMENT = _Attachment(name="filters.ATTACHMENT") """Messages that contain :meth:`telegram.Message.effective_attachment`. .. versionadded:: 13.6""" class _Audio(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.audio) AUDIO = _Audio(name="filters.AUDIO") """Messages that contain :attr:`telegram.Message.audio`.""" class Caption(MessageFilter): """Messages with a caption. If a list of strings is passed, it filters messages to only allow those whose caption is appearing in the given list. Examples: ``MessageHandler(filters.Caption(['PTB rocks!', 'PTB']), callback_method_2)`` .. seealso:: :attr:`telegram.ext.filters.CAPTION` Args: strings (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which captions to allow. Only exact matches are allowed. If not specified, will allow any message with a caption. """ __slots__ = ("strings",) def __init__(self, strings: Optional[Union[List[str], Tuple[str, ...]]] = None): self.strings: Optional[Sequence[str]] = strings super().__init__(name=f"filters.Caption({strings})" if strings else "filters.CAPTION") def filter(self, message: Message) -> bool: if self.strings is None: return bool(message.caption) return message.caption in self.strings if message.caption else False CAPTION = Caption() """Shortcut for :class:`telegram.ext.filters.Caption()`. Examples: To allow any caption, simply use ``MessageHandler(filters.CAPTION, callback_method)``. """ class CaptionEntity(MessageFilter): """ Filters media messages to only allow those which have a :class:`telegram.MessageEntity` where their :class:`~telegram.MessageEntity.type` matches `entity_type`. Examples: ``MessageHandler(filters.CaptionEntity("hashtag"), callback_method)`` Args: entity_type (:obj:`str`): Caption Entity type to check for. All types can be found as constants in :class:`telegram.MessageEntity`. """ __slots__ = ("entity_type",) def __init__(self, entity_type: str): self.entity_type: str = entity_type super().__init__(name=f"filters.CaptionEntity({self.entity_type})") def filter(self, message: Message) -> bool: return any(entity.type == self.entity_type for entity in message.caption_entities) class CaptionRegex(MessageFilter): """ Filters updates by searching for an occurrence of :paramref:`~CaptionRegex.pattern` in the message caption. This filter works similarly to :class:`Regex`, with the only exception being that it applies to the message caption instead of the text. Examples: Use ``MessageHandler(filters.PHOTO & filters.CaptionRegex(r'help'), callback)`` to capture all photos with caption containing the word 'help'. Note: This filter will not work on simple text messages, but only on media with caption. Args: pattern (:obj:`str` | :func:`re.Pattern `): The regex pattern. """ __slots__ = ("pattern",) def __init__(self, pattern: Union[str, Pattern[str]]): if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern: Pattern[str] = pattern super().__init__(name=f"filters.CaptionRegex({self.pattern})", data_filter=True) def filter(self, message: Message) -> Optional[Dict[str, List[Match[str]]]]: if message.caption and (match := self.pattern.search(message.caption)): return {"matches": [match]} return {} class _ChatUserBaseFilter(MessageFilter, ABC): __slots__ = ( "_chat_id_name", "_chat_ids", "_username_name", "_usernames", "allow_empty", ) def __init__( self, chat_id: Optional[SCT[int]] = None, username: Optional[SCT[str]] = None, allow_empty: bool = False, ): super().__init__() self._chat_id_name: str = "chat_id" self._username_name: str = "username" self.allow_empty: bool = allow_empty self._chat_ids: Set[int] = set() self._usernames: Set[str] = set() self._set_chat_ids(chat_id) self._set_usernames(username) @abstractmethod def _get_chat_or_user(self, message: Message) -> Union[TGChat, TGUser, None]: ... def _set_chat_ids(self, chat_id: Optional[SCT[int]]) -> None: if chat_id and self._usernames: raise RuntimeError( f"Can't set {self._chat_id_name} in conjunction with (already set) " f"{self._username_name}s." ) self._chat_ids = set(parse_chat_id(chat_id)) def _set_usernames(self, username: Optional[SCT[str]]) -> None: if username and self._chat_ids: raise RuntimeError( f"Can't set {self._username_name} in conjunction with (already set) " f"{self._chat_id_name}s." ) self._usernames = set(parse_username(username)) @property def chat_ids(self) -> FrozenSet[int]: return frozenset(self._chat_ids) @chat_ids.setter def chat_ids(self, chat_id: SCT[int]) -> None: self._set_chat_ids(chat_id) @property def usernames(self) -> FrozenSet[str]: """Which username(s) to allow through. Warning: :attr:`usernames` will give a *copy* of the saved usernames as :obj:`frozenset`. This is to ensure thread safety. To add/remove a user, you should use :meth:`add_usernames`, and :meth:`remove_usernames`. Only update the entire set by ``filter.usernames = new_set``, if you are entirely sure that it is not causing race conditions, as this will complete replace the current set of allowed users. Returns: frozenset(:obj:`str`) """ return frozenset(self._usernames) @usernames.setter def usernames(self, username: SCT[str]) -> None: self._set_usernames(username) def add_usernames(self, username: SCT[str]) -> None: """ Add one or more chats to the allowed usernames. Args: username(:obj:`str` | Collection[:obj:`str`]): Which username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. """ if self._chat_ids: raise RuntimeError( f"Can't set {self._username_name} in conjunction with (already set) " f"{self._chat_id_name}s." ) parsed_username = set(parse_username(username)) self._usernames |= parsed_username def _add_chat_ids(self, chat_id: SCT[int]) -> None: if self._usernames: raise RuntimeError( f"Can't set {self._chat_id_name} in conjunction with (already set) " f"{self._username_name}s." ) parsed_chat_id = set(parse_chat_id(chat_id)) self._chat_ids |= parsed_chat_id def remove_usernames(self, username: SCT[str]) -> None: """ Remove one or more chats from allowed usernames. Args: username(:obj:`str` | Collection[:obj:`str`]): Which username(s) to disallow through. Leading ``'@'`` s in usernames will be discarded. """ if self._chat_ids: raise RuntimeError( f"Can't set {self._username_name} in conjunction with (already set) " f"{self._chat_id_name}s." ) parsed_username = set(parse_username(username)) self._usernames -= parsed_username def _remove_chat_ids(self, chat_id: SCT[int]) -> None: if self._usernames: raise RuntimeError( f"Can't set {self._chat_id_name} in conjunction with (already set) " f"{self._username_name}s." ) parsed_chat_id = set(parse_chat_id(chat_id)) self._chat_ids -= parsed_chat_id def filter(self, message: Message) -> bool: chat_or_user = self._get_chat_or_user(message) if chat_or_user: if self.chat_ids: return chat_or_user.id in self.chat_ids if self.usernames: return bool(chat_or_user.username and chat_or_user.username in self.usernames) return self.allow_empty return False @property def name(self) -> str: return ( f"filters.{self.__class__.__name__}(" f"{', '.join(str(s) for s in (self.usernames or self.chat_ids))})" ) @name.setter def name(self, name: str) -> NoReturn: raise RuntimeError(f"Cannot set name for filters.{self.__class__.__name__}") class Chat(_ChatUserBaseFilter): """Filters messages to allow only those which are from a specified chat ID or username. Examples: ``MessageHandler(filters.Chat(-1234), callback_method)`` Warning: :attr:`chat_ids` will give a *copy* of the saved chat ids as :class:`frozenset`. This is to ensure thread safety. To add/remove a chat, you should use :meth:`add_chat_ids`, and :meth:`remove_chat_ids`. Only update the entire set by ``filter.chat_ids = new_set``, if you are entirely sure that it is not causing race conditions, as this will complete replace the current set of allowed chats. Args: chat_id(:obj:`int` | Collection[:obj:`int`], optional): Which chat ID(s) to allow through. username(:obj:`str` | Collection[:obj:`str`], optional): Which username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False`. Attributes: chat_ids (set(:obj:`int`)): Which chat ID(s) to allow through. allow_empty (:obj:`bool`): Whether updates should be processed, if no chat is specified in :attr:`chat_ids` and :attr:`usernames`. Raises: RuntimeError: If ``chat_id`` and ``username`` are both present. """ __slots__ = () def _get_chat_or_user(self, message: Message) -> Optional[TGChat]: return message.chat def add_chat_ids(self, chat_id: SCT[int]) -> None: """ Add one or more chats to the allowed chat ids. Args: chat_id(:obj:`int` | Collection[:obj:`int`]): Which chat ID(s) to allow through. """ return super()._add_chat_ids(chat_id) def remove_chat_ids(self, chat_id: SCT[int]) -> None: """ Remove one or more chats from allowed chat ids. Args: chat_id(:obj:`int` | Collection[:obj:`int`]): Which chat ID(s) to disallow through. """ return super()._remove_chat_ids(chat_id) class _Chat(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.chat) CHAT = _Chat(name="filters.CHAT") """This filter filters *any* message that has a :attr:`telegram.Message.chat`. .. deprecated:: 20.8 This filter has no effect since :attr:`telegram.Message.chat` is always present. """ class ChatType: # A convenience namespace for Chat types. """Subset for filtering the type of chat. Examples: Use these filters like: ``filters.ChatType.CHANNEL`` or ``filters.ChatType.SUPERGROUP`` etc. Caution: ``filters.ChatType`` itself is *not* a filter, but just a convenience namespace. """ __slots__ = () class _Channel(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return message.chat.type == TGChat.CHANNEL CHANNEL = _Channel(name="filters.ChatType.CHANNEL") """Updates from channel.""" class _Group(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return message.chat.type == TGChat.GROUP GROUP = _Group(name="filters.ChatType.GROUP") """Updates from group.""" class _Groups(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return message.chat.type in [TGChat.GROUP, TGChat.SUPERGROUP] GROUPS = _Groups(name="filters.ChatType.GROUPS") """Update from group *or* supergroup.""" class _Private(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return message.chat.type == TGChat.PRIVATE PRIVATE = _Private(name="filters.ChatType.PRIVATE") """Update from private chats.""" class _SuperGroup(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return message.chat.type == TGChat.SUPERGROUP SUPERGROUP = _SuperGroup(name="filters.ChatType.SUPERGROUP") """Updates from supergroup.""" class Command(MessageFilter): """ Messages with a :attr:`telegram.MessageEntity.BOT_COMMAND`. By default, only allows messages `starting` with a bot command. Pass :obj:`False` to also allow messages that contain a bot command `anywhere` in the text. Examples: ``MessageHandler(filters.Command(False), command_anywhere_callback)`` .. seealso:: :attr:`telegram.ext.filters.COMMAND`. Note: :attr:`telegram.ext.filters.TEXT` also accepts messages containing a command. Args: only_start (:obj:`bool`, optional): Whether to only allow messages that `start` with a bot command. Defaults to :obj:`True`. """ __slots__ = ("only_start",) def __init__(self, only_start: bool = True): self.only_start: bool = only_start super().__init__(f"filters.Command({only_start})" if not only_start else "filters.COMMAND") def filter(self, message: Message) -> bool: if not message.entities: return False first = message.entities[0] if self.only_start: return bool(first.type == MessageEntity.BOT_COMMAND and first.offset == 0) return bool(any(e.type == MessageEntity.BOT_COMMAND for e in message.entities)) COMMAND = Command() """Shortcut for :class:`telegram.ext.filters.Command()`. Examples: To allow messages starting with a command use ``MessageHandler(filters.COMMAND, command_at_start_callback)``. """ class _Contact(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.contact) CONTACT = _Contact(name="filters.CONTACT") """Messages that contain :attr:`telegram.Message.contact`.""" class _Dice(MessageFilter): __slots__ = ("emoji", "values") def __init__(self, values: Optional[SCT[int]] = None, emoji: Optional[DiceEmojiEnum] = None): super().__init__() self.emoji: Optional[DiceEmojiEnum] = emoji self.values: Optional[Collection[int]] = [values] if isinstance(values, int) else values if emoji: # for filters.Dice.BASKETBALL self.name = f"filters.Dice.{emoji.name}" if self.values and emoji: # for filters.Dice.Dice(4) SLOT_MACHINE -> SlotMachine self.name = f"filters.Dice.{emoji.name.title().replace('_', '')}({self.values})" elif values: # for filters.Dice(4) self.name = f"filters.Dice({self.values})" else: self.name = "filters.Dice.ALL" def filter(self, message: Message) -> bool: if not (dice := message.dice): # no dice return False if self.emoji: emoji_match = dice.emoji == self.emoji if self.values: return dice.value in self.values and emoji_match # emoji and value return emoji_match # emoji, no value return dice.value in self.values if self.values else True # no emoji, only value class Dice(_Dice): """Dice Messages. If an integer or a list of integers is passed, it filters messages to only allow those whose dice value is appearing in the given list. .. versionadded:: 13.4 Examples: To allow any dice message, simply use ``MessageHandler(filters.Dice.ALL, callback_method)``. To allow any dice message, but with value 3 `or` 4, use ``MessageHandler(filters.Dice([3, 4]), callback_method)`` To allow only dice messages with the emoji 🎲, but any value, use ``MessageHandler(filters.Dice.DICE, callback_method)``. To allow only dice messages with the emoji 🎯 and with value 6, use ``MessageHandler(filters.Dice.Darts(6), callback_method)``. To allow only dice messages with the emoji ⚽ and with value 5 `or` 6, use ``MessageHandler(filters.Dice.Football([5, 6]), callback_method)``. Note: Dice messages don't have text. If you want to filter either text or dice messages, use ``filters.TEXT | filters.Dice.ALL``. Args: values (:obj:`int` | Collection[:obj:`int`], optional): Which values to allow. If not specified, will allow the specified dice message. """ __slots__ = () ALL = _Dice() """Dice messages with any value and any emoji.""" class Basketball(_Dice): """Dice messages with the emoji 🏀. Supports passing a list of integers. Args: values (:obj:`int` | Collection[:obj:`int`]): Which values to allow. """ __slots__ = () def __init__(self, values: SCT[int]): super().__init__(values, emoji=DiceEmojiEnum.BASKETBALL) BASKETBALL = _Dice(emoji=DiceEmojiEnum.BASKETBALL) """Dice messages with the emoji 🏀. Matches any dice value.""" class Bowling(_Dice): """Dice messages with the emoji 🎳. Supports passing a list of integers. Args: values (:obj:`int` | Collection[:obj:`int`]): Which values to allow. """ __slots__ = () def __init__(self, values: SCT[int]): super().__init__(values, emoji=DiceEmojiEnum.BOWLING) BOWLING = _Dice(emoji=DiceEmojiEnum.BOWLING) """Dice messages with the emoji 🎳. Matches any dice value.""" class Darts(_Dice): """Dice messages with the emoji 🎯. Supports passing a list of integers. Args: values (:obj:`int` | Collection[:obj:`int`]): Which values to allow. """ __slots__ = () def __init__(self, values: SCT[int]): super().__init__(values, emoji=DiceEmojiEnum.DARTS) DARTS = _Dice(emoji=DiceEmojiEnum.DARTS) """Dice messages with the emoji 🎯. Matches any dice value.""" class Dice(_Dice): """Dice messages with the emoji 🎲. Supports passing a list of integers. Args: values (:obj:`int` | Collection[:obj:`int`]): Which values to allow. """ __slots__ = () def __init__(self, values: SCT[int]): super().__init__(values, emoji=DiceEmojiEnum.DICE) DICE = _Dice(emoji=DiceEmojiEnum.DICE) """Dice messages with the emoji 🎲. Matches any dice value.""" class Football(_Dice): """Dice messages with the emoji ⚽. Supports passing a list of integers. Args: values (:obj:`int` | Collection[:obj:`int`]): Which values to allow. """ __slots__ = () def __init__(self, values: SCT[int]): super().__init__(values, emoji=DiceEmojiEnum.FOOTBALL) FOOTBALL = _Dice(emoji=DiceEmojiEnum.FOOTBALL) """Dice messages with the emoji ⚽. Matches any dice value.""" class SlotMachine(_Dice): """Dice messages with the emoji 🎰. Supports passing a list of integers. Args: values (:obj:`int` | Collection[:obj:`int`]): Which values to allow. """ __slots__ = () def __init__(self, values: SCT[int]): super().__init__(values, emoji=DiceEmojiEnum.SLOT_MACHINE) SLOT_MACHINE = _Dice(emoji=DiceEmojiEnum.SLOT_MACHINE) """Dice messages with the emoji 🎰. Matches any dice value.""" class Document: """ Subset for messages containing a document/file. Examples: Use these filters like: ``filters.Document.MP3``, ``filters.Document.MimeType("text/plain")`` etc. Or just use ``filters.Document.ALL`` for all document messages. Caution: ``filters.Document`` itself is *not* a filter, but just a convenience namespace. """ __slots__ = () class _All(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.document) ALL = _All(name="filters.Document.ALL") """Messages that contain a :attr:`telegram.Message.document`.""" class Category(MessageFilter): """Filters documents by their category in the mime-type attribute. Args: category (:obj:`str`): Category of the media you want to filter. Example: ``filters.Document.Category('audio/')`` returns :obj:`True` for all types of audio sent as a file, for example ``'audio/mpeg'`` or ``'audio/x-wav'``. Note: This Filter only filters by the mime_type of the document, it doesn't check the validity of the document. The user can manipulate the mime-type of a message and send media with wrong types that don't fit to this handler. """ __slots__ = ("_category",) def __init__(self, category: str): self._category = category super().__init__(name=f"filters.Document.Category('{self._category}')") def filter(self, message: Message) -> bool: if message.document and message.document.mime_type: return message.document.mime_type.startswith(self._category) return False APPLICATION = Category("application/") """Use as ``filters.Document.APPLICATION``.""" AUDIO = Category("audio/") """Use as ``filters.Document.AUDIO``.""" IMAGE = Category("image/") """Use as ``filters.Document.IMAGE``.""" VIDEO = Category("video/") """Use as ``filters.Document.VIDEO``.""" TEXT = Category("text/") """Use as ``filters.Document.TEXT``.""" class FileExtension(MessageFilter): """This filter filters documents by their file ending/extension. Args: file_extension (:obj:`str` | :obj:`None`): Media file extension you want to filter. case_sensitive (:obj:`bool`, optional): Pass :obj:`True` to make the filter case sensitive. Default: :obj:`False`. Example: * ``filters.Document.FileExtension("jpg")`` filters files with extension ``".jpg"``. * ``filters.Document.FileExtension(".jpg")`` filters files with extension ``"..jpg"``. * ``filters.Document.FileExtension("Dockerfile", case_sensitive=True)`` filters files with extension ``".Dockerfile"`` minding the case. * ``filters.Document.FileExtension(None)`` filters files without a dot in the filename. Note: * This Filter only filters by the file ending/extension of the document, it doesn't check the validity of document. * The user can manipulate the file extension of a document and send media with wrong types that don't fit to this handler. * Case insensitive by default, you may change this with the flag ``case_sensitive=True``. * Extension should be passed without leading dot unless it's a part of the extension. * Pass :obj:`None` to filter files with no extension, i.e. without a dot in the filename. """ __slots__ = ("_file_extension", "is_case_sensitive") def __init__(self, file_extension: Optional[str], case_sensitive: bool = False): super().__init__() self.is_case_sensitive: bool = case_sensitive if file_extension is None: self._file_extension = None self.name = "filters.Document.FileExtension(None)" elif self.is_case_sensitive: self._file_extension = f".{file_extension}" self.name = ( f"filters.Document.FileExtension({file_extension!r}, case_sensitive=True)" ) else: self._file_extension = f".{file_extension}".lower() self.name = f"filters.Document.FileExtension({file_extension.lower()!r})" def filter(self, message: Message) -> bool: if message.document is None or message.document.file_name is None: return False if self._file_extension is None: return "." not in message.document.file_name if self.is_case_sensitive: filename = message.document.file_name else: filename = message.document.file_name.lower() return filename.endswith(self._file_extension) class MimeType(MessageFilter): """This Filter filters documents by their mime-type attribute. Args: mimetype (:obj:`str`): The mimetype to filter. Example: ``filters.Document.MimeType('audio/mpeg')`` filters all audio in `.mp3` format. Note: This Filter only filters by the mime_type of the document, it doesn't check the validity of document. The user can manipulate the mime-type of a message and send media with wrong types that don't fit to this handler. """ __slots__ = ("mimetype",) def __init__(self, mimetype: str): self.mimetype: str = mimetype super().__init__(name=f"filters.Document.MimeType('{self.mimetype}')") def filter(self, message: Message) -> bool: if message.document: return message.document.mime_type == self.mimetype return False APK = MimeType("application/vnd.android.package-archive") """Use as ``filters.Document.APK``.""" DOC = MimeType(mimetypes.types_map[".doc"]) """Use as ``filters.Document.DOC``.""" DOCX = MimeType("application/vnd.openxmlformats-officedocument.wordprocessingml.document") """Use as ``filters.Document.DOCX``.""" EXE = MimeType(mimetypes.types_map[".exe"]) """Use as ``filters.Document.EXE``.""" MP4 = MimeType(mimetypes.types_map[".mp4"]) """Use as ``filters.Document.MP4``.""" GIF = MimeType(mimetypes.types_map[".gif"]) """Use as ``filters.Document.GIF``.""" JPG = MimeType(mimetypes.types_map[".jpg"]) """Use as ``filters.Document.JPG``.""" MP3 = MimeType(mimetypes.types_map[".mp3"]) """Use as ``filters.Document.MP3``.""" PDF = MimeType(mimetypes.types_map[".pdf"]) """Use as ``filters.Document.PDF``.""" PY = MimeType(mimetypes.types_map[".py"]) """Use as ``filters.Document.PY``.""" SVG = MimeType(mimetypes.types_map[".svg"]) """Use as ``filters.Document.SVG``.""" TXT = MimeType(mimetypes.types_map[".txt"]) """Use as ``filters.Document.TXT``.""" TARGZ = MimeType("application/x-compressed-tar") """Use as ``filters.Document.TARGZ``.""" WAV = MimeType(mimetypes.types_map[".wav"]) """Use as ``filters.Document.WAV``.""" XML = MimeType(mimetypes.types_map[".xml"]) """Use as ``filters.Document.XML``.""" ZIP = MimeType(mimetypes.types_map[".zip"]) """Use as ``filters.Document.ZIP``.""" class Entity(MessageFilter): """ Filters messages to only allow those which have a :class:`telegram.MessageEntity` where their :class:`~telegram.MessageEntity.type` matches `entity_type`. Examples: ``MessageHandler(filters.Entity("hashtag"), callback_method)`` Args: entity_type (:obj:`str`): Entity type to check for. All types can be found as constants in :class:`telegram.MessageEntity`. """ __slots__ = ("entity_type",) def __init__(self, entity_type: str): self.entity_type: str = entity_type super().__init__(name=f"filters.Entity({self.entity_type})") def filter(self, message: Message) -> bool: return any(entity.type == self.entity_type for entity in message.entities) class _Forwarded(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.forward_origin) FORWARDED = _Forwarded(name="filters.FORWARDED") """Messages that contain :attr:`telegram.Message.forward_origin`. .. versionchanged:: 20.8 Now based on :attr:`telegram.Message.forward_origin` instead of ``telegram.Message.forward_date``. """ class ForwardedFrom(_ChatUserBaseFilter): """Filters messages to allow only those which are forwarded from the specified chat ID(s) or username(s) based on :attr:`telegram.Message.forward_origin` and in particular * :attr:`telegram.MessageOriginUser.sender_user` * :attr:`telegram.MessageOriginChat.sender_chat` * :attr:`telegram.MessageOriginChannel.chat` .. versionadded:: 13.5 .. versionchanged:: 20.8 Was previously based on ``telegram.Message.forward_from`` and ``telegram.Message.forward_from_chat``. Examples: ``MessageHandler(filters.ForwardedFrom(chat_id=1234), callback_method)`` Note: When a user has disallowed adding a link to their account while forwarding their messages, this filter will *not* work since :attr:`telegram.Message.forward_origin` will be of type :class:`telegram.MessageOriginHiddenUser`. However, this behaviour is undocumented and might be changed by Telegram. Warning: :attr:`chat_ids` will give a *copy* of the saved chat ids as :class:`frozenset`. This is to ensure thread safety. To add/remove a chat, you should use :meth:`add_chat_ids`, and :meth:`remove_chat_ids`. Only update the entire set by ``filter.chat_ids = new_set``, if you are entirely sure that it is not causing race conditions, as this will complete replace the current set of allowed chats. Args: chat_id(:obj:`int` | Collection[:obj:`int`], optional): Which chat/user ID(s) to allow through. username(:obj:`str` | Collection[:obj:`str`], optional): Which username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False`. Attributes: chat_ids (set(:obj:`int`)): Which chat/user ID(s) to allow through. allow_empty (:obj:`bool`): Whether updates should be processed, if no chat is specified in :attr:`chat_ids` and :attr:`usernames`. Raises: RuntimeError: If both ``chat_id`` and ``username`` are present. """ __slots__ = () def _get_chat_or_user(self, message: Message) -> Union[TGUser, TGChat, None]: if (forward_origin := message.forward_origin) is None: return None if isinstance(forward_origin, MessageOriginUser): return forward_origin.sender_user if isinstance(forward_origin, MessageOriginChat): return forward_origin.sender_chat if isinstance(forward_origin, MessageOriginChannel): return forward_origin.chat return None def add_chat_ids(self, chat_id: SCT[int]) -> None: """ Add one or more chats to the allowed chat ids. Args: chat_id(:obj:`int` | Collection[:obj:`int`]): Which chat/user ID(s) to allow through. """ return super()._add_chat_ids(chat_id) def remove_chat_ids(self, chat_id: SCT[int]) -> None: """ Remove one or more chats from allowed chat ids. Args: chat_id(:obj:`int` | Collection[:obj:`int`]): Which chat/user ID(s) to disallow through. """ return super()._remove_chat_ids(chat_id) class _Game(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.game) GAME = _Game(name="filters.GAME") """Messages that contain :attr:`telegram.Message.game`.""" class _Giveaway(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.giveaway) GIVEAWAY = _Giveaway(name="filters.GIVEAWAY") """Messages that contain :attr:`telegram.Message.giveaway`.""" class _GiveawayWinners(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.giveaway_winners) GIVEAWAY_WINNERS = _GiveawayWinners(name="filters.GIVEAWAY_WINNERS") """Messages that contain :attr:`telegram.Message.giveaway_winners`.""" class _HasMediaSpoiler(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.has_media_spoiler) HAS_MEDIA_SPOILER = _HasMediaSpoiler(name="filters.HAS_MEDIA_SPOILER") """Messages that contain :attr:`telegram.Message.has_media_spoiler`. .. versionadded:: 20.0 """ class _HasProtectedContent(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.has_protected_content) HAS_PROTECTED_CONTENT = _HasProtectedContent(name="filters.HAS_PROTECTED_CONTENT") """Messages that contain :attr:`telegram.Message.has_protected_content`. .. versionadded:: 13.9 """ class _Invoice(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.invoice) INVOICE = _Invoice(name="filters.INVOICE") """Messages that contain :attr:`telegram.Message.invoice`.""" class _IsAutomaticForward(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.is_automatic_forward) IS_AUTOMATIC_FORWARD = _IsAutomaticForward(name="filters.IS_AUTOMATIC_FORWARD") """Messages that contain :attr:`telegram.Message.is_automatic_forward`. .. versionadded:: 13.9 """ class _IsTopicMessage(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.is_topic_message) IS_TOPIC_MESSAGE = _IsTopicMessage(name="filters.IS_TOPIC_MESSAGE") """Messages that contain :attr:`telegram.Message.is_topic_message`. .. versionadded:: 20.0 """ class _IsFromOffline(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.is_from_offline) IS_FROM_OFFLINE = _IsFromOffline(name="filters.IS_FROM_OFFLINE") """Messages that contain :attr:`telegram.Message.is_from_offline`. .. versionadded:: 21.1 """ class Language(MessageFilter): """Filters messages to only allow those which are from users with a certain language code. Note: According to official Telegram Bot API documentation, not every single user has the `language_code` attribute. Do not count on this filter working on all users. Examples: ``MessageHandler(filters.Language("en"), callback_method)`` Args: lang (:obj:`str` | Collection[:obj:`str`]): Which language code(s) to allow through. This will be matched using :obj:`str.startswith` meaning that 'en' will match both 'en_US' and 'en_GB'. """ __slots__ = ("lang",) def __init__(self, lang: SCT[str]): if isinstance(lang, str): lang = cast(str, lang) self.lang: Sequence[str] = [lang] else: lang = cast(List[str], lang) self.lang = lang super().__init__(name=f"filters.Language({self.lang})") def filter(self, message: Message) -> bool: return bool( message.from_user and message.from_user.language_code and any(message.from_user.language_code.startswith(x) for x in self.lang) ) class _Location(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.location) LOCATION = _Location(name="filters.LOCATION") """Messages that contain :attr:`telegram.Message.location`.""" class Mention(MessageFilter): """Messages containing mentions of specified users or chats. Examples: .. code-block:: python MessageHandler(filters.Mention("username"), callback) MessageHandler(filters.Mention(["@username", 123456]), callback) .. versionadded:: 20.7 Args: mentions (:obj:`int` | :obj:`str` | :class:`telegram.User` | Collection[:obj:`int` | \ :obj:`str` | :class:`telegram.User`]): Specifies the users and chats to filter for. Messages that do not mention at least one of the specified users or chats will not be handled. Leading ``'@'`` s in usernames will be discarded. """ __slots__ = ("_mentions",) def __init__(self, mentions: SCT[Union[int, str, TGUser]]): super().__init__(name=f"filters.Mention({mentions})") if isinstance(mentions, Iterable) and not isinstance(mentions, str): self._mentions = {self._fix_mention_username(mention) for mention in mentions} else: self._mentions = {self._fix_mention_username(mentions)} @staticmethod def _fix_mention_username(mention: Union[int, str, TGUser]) -> Union[int, str, TGUser]: if not isinstance(mention, str): return mention return mention.lstrip("@") @classmethod def _check_mention(cls, message: Message, mention: Union[int, str, TGUser]) -> bool: if not message.entities: return False entity_texts = message.parse_entities( types=[MessageEntity.MENTION, MessageEntity.TEXT_MENTION] ) if isinstance(mention, TGUser): return any( mention.id == entity.user.id or mention.username == entity.user.username or mention.username == cls._fix_mention_username(entity_texts[entity]) for entity in message.entities if entity.user ) or any( mention.username == cls._fix_mention_username(entity_text) for entity_text in entity_texts.values() ) if isinstance(mention, int): return bool( any(mention == entity.user.id for entity in message.entities if entity.user) ) return any( mention == cls._fix_mention_username(entity_text) for entity_text in entity_texts.values() ) def filter(self, message: Message) -> bool: return any(self._check_mention(message, mention) for mention in self._mentions) class _PassportData(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.passport_data) PASSPORT_DATA = _PassportData(name="filters.PASSPORT_DATA") """Messages that contain :attr:`telegram.Message.passport_data`.""" class _Photo(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.photo) PHOTO = _Photo("filters.PHOTO") """Messages that contain :attr:`telegram.Message.photo`.""" class _Poll(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.poll) POLL = _Poll(name="filters.POLL") """Messages that contain :attr:`telegram.Message.poll`.""" class Regex(MessageFilter): """ Filters updates by searching for an occurrence of :paramref:`~Regex.pattern` in the message text. The :func:`re.search` function is used to determine whether an update should be filtered. Refer to the documentation of the :obj:`re` module for more information. To get the groups and groupdict matched, see :attr:`telegram.ext.CallbackContext.matches`. Examples: Use ``MessageHandler(filters.Regex(r'help'), callback)`` to capture all messages that contain the word 'help'. You can also use ``MessageHandler(filters.Regex(re.compile(r'help', re.IGNORECASE)), callback)`` if you want your pattern to be case insensitive. This approach is recommended if you need to specify flags on your pattern. Note: Filters use the same short circuiting logic as python's :keyword:`and`, :keyword:`or` and :keyword:`not`. This means that for example: >>> filters.Regex(r'(a?x)') | filters.Regex(r'(b?x)') With a :attr:`telegram.Message.text` of `x`, will only ever return the matches for the first filter, since the second one is never evaluated. .. seealso:: :wiki:`Types of Handlers ` Args: pattern (:obj:`str` | :func:`re.Pattern `): The regex pattern. """ __slots__ = ("pattern",) def __init__(self, pattern: Union[str, Pattern[str]]): if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern: Pattern[str] = pattern super().__init__(name=f"filters.Regex({self.pattern})", data_filter=True) def filter(self, message: Message) -> Optional[Dict[str, List[Match[str]]]]: if message.text and (match := self.pattern.search(message.text)): return {"matches": [match]} return {} class _Reply(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.reply_to_message) REPLY = _Reply(name="filters.REPLY") """Messages that contain :attr:`telegram.Message.reply_to_message`.""" class _SenderChat(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.sender_chat) class SenderChat(_ChatUserBaseFilter): """Filters messages to allow only those which are from a specified sender chat's chat ID or username. Examples: * To filter for messages sent to a group by a channel with ID ``-1234``, use ``MessageHandler(filters.SenderChat(-1234), callback_method)``. * To filter for messages of anonymous admins in a super group with username ``@anonymous``, use ``MessageHandler(filters.SenderChat(username='anonymous'), callback_method)``. * To filter for messages sent to a group by *any* channel, use ``MessageHandler(filters.SenderChat.CHANNEL, callback_method)``. * To filter for messages of anonymous admins in *any* super group, use ``MessageHandler(filters.SenderChat.SUPERGROUP, callback_method)``. * To filter for messages forwarded to a discussion group from *any* channel or of anonymous admins in *any* super group, use ``MessageHandler(filters.SenderChat.ALL, callback)`` Note: Remember, ``sender_chat`` is also set for messages in a channel as the channel itself, so when your bot is an admin in a channel and the linked discussion group, you would receive the message twice (once from inside the channel, once inside the discussion group). Since v13.9, the field :attr:`telegram.Message.is_automatic_forward` will be :obj:`True` for the discussion group message. .. seealso:: :attr:`telegram.ext.filters.IS_AUTOMATIC_FORWARD` Warning: :attr:`chat_ids` will return a *copy* of the saved chat ids as :obj:`frozenset`. This is to ensure thread safety. To add/remove a chat, you should use :meth:`add_chat_ids`, and :meth:`remove_chat_ids`. Only update the entire set by ``filter.chat_ids = new_set``, if you are entirely sure that it is not causing race conditions, as this will complete replace the current set of allowed chats. Args: chat_id(:obj:`int` | Collection[:obj:`int`], optional): Which sender chat chat ID(s) to allow through. username(:obj:`str` | Collection[:obj:`str`], optional): Which sender chat username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no sender chat is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False`. Attributes: chat_ids (set(:obj:`int`)): Which sender chat chat ID(s) to allow through. allow_empty (:obj:`bool`): Whether updates should be processed, if no sender chat is specified in :attr:`chat_ids` and :attr:`usernames`. Raises: RuntimeError: If both ``chat_id`` and ``username`` are present. """ __slots__ = () class _CHANNEL(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: if message.sender_chat: return message.sender_chat.type == TGChat.CHANNEL return False class _SUPERGROUP(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: if message.sender_chat: return message.sender_chat.type == TGChat.SUPERGROUP return False ALL = _SenderChat(name="filters.SenderChat.ALL") """All messages with a :attr:`telegram.Message.sender_chat`.""" SUPER_GROUP = _SUPERGROUP(name="filters.SenderChat.SUPER_GROUP") """Messages whose sender chat is a super group.""" CHANNEL = _CHANNEL(name="filters.SenderChat.CHANNEL") """Messages whose sender chat is a channel.""" def add_chat_ids(self, chat_id: SCT[int]) -> None: """ Add one or more sender chats to the allowed chat ids. Args: chat_id(:obj:`int` | Collection[:obj:`int`]): Which sender chat ID(s) to allow through. """ return super()._add_chat_ids(chat_id) def _get_chat_or_user(self, message: Message) -> Optional[TGChat]: return message.sender_chat def remove_chat_ids(self, chat_id: SCT[int]) -> None: """ Remove one or more sender chats from allowed chat ids. Args: chat_id(:obj:`int` | Collection[:obj:`int`]): Which sender chat ID(s) to disallow through. """ return super()._remove_chat_ids(chat_id) class StatusUpdate: """Subset for messages containing a status update. Examples: Use these filters like: ``filters.StatusUpdate.NEW_CHAT_MEMBERS`` etc. Or use just ``filters.StatusUpdate.ALL`` for all status update messages. Caution: ``filters.StatusUpdate`` itself is *not* a filter, but just a convenience namespace. """ __slots__ = () class _All(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return bool( # keep this alphabetically sorted for easier maintenance StatusUpdate.CHAT_CREATED.check_update(update) or StatusUpdate.CHAT_SHARED.check_update(update) or StatusUpdate.CONNECTED_WEBSITE.check_update(update) or StatusUpdate.DELETE_CHAT_PHOTO.check_update(update) or StatusUpdate.FORUM_TOPIC_CLOSED.check_update(update) or StatusUpdate.FORUM_TOPIC_CREATED.check_update(update) or StatusUpdate.FORUM_TOPIC_EDITED.check_update(update) or StatusUpdate.FORUM_TOPIC_REOPENED.check_update(update) or StatusUpdate.GENERAL_FORUM_TOPIC_HIDDEN.check_update(update) or StatusUpdate.GENERAL_FORUM_TOPIC_UNHIDDEN.check_update(update) or StatusUpdate.GIVEAWAY_COMPLETED.check_update(update) or StatusUpdate.GIVEAWAY_CREATED.check_update(update) or StatusUpdate.LEFT_CHAT_MEMBER.check_update(update) or StatusUpdate.MESSAGE_AUTO_DELETE_TIMER_CHANGED.check_update(update) or StatusUpdate.MIGRATE.check_update(update) or StatusUpdate.NEW_CHAT_MEMBERS.check_update(update) or StatusUpdate.NEW_CHAT_PHOTO.check_update(update) or StatusUpdate.NEW_CHAT_TITLE.check_update(update) or StatusUpdate.PINNED_MESSAGE.check_update(update) or StatusUpdate.PROXIMITY_ALERT_TRIGGERED.check_update(update) or StatusUpdate.USERS_SHARED.check_update(update) or StatusUpdate.USER_SHARED.check_update(update) or StatusUpdate.VIDEO_CHAT_ENDED.check_update(update) or StatusUpdate.VIDEO_CHAT_PARTICIPANTS_INVITED.check_update(update) or StatusUpdate.VIDEO_CHAT_SCHEDULED.check_update(update) or StatusUpdate.VIDEO_CHAT_STARTED.check_update(update) or StatusUpdate.WEB_APP_DATA.check_update(update) or StatusUpdate.WRITE_ACCESS_ALLOWED.check_update(update) ) ALL = _All(name="filters.StatusUpdate.ALL") """Messages that contain any of the below.""" class _ChatCreated(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool( message.group_chat_created or message.supergroup_chat_created or message.channel_chat_created ) CHAT_CREATED = _ChatCreated(name="filters.StatusUpdate.CHAT_CREATED") """Messages that contain :attr:`telegram.Message.group_chat_created`, :attr:`telegram.Message.supergroup_chat_created` or :attr:`telegram.Message.channel_chat_created`.""" class _ChatShared(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.chat_shared) CHAT_SHARED = _ChatShared(name="filters.StatusUpdate.CHAT_SHARED") """Messages that contain :attr:`telegram.Message.chat_shared`. .. versionadded:: 20.1 """ class _ConnectedWebsite(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.connected_website) CONNECTED_WEBSITE = _ConnectedWebsite(name="filters.StatusUpdate.CONNECTED_WEBSITE") """Messages that contain :attr:`telegram.Message.connected_website`.""" class _DeleteChatPhoto(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.delete_chat_photo) DELETE_CHAT_PHOTO = _DeleteChatPhoto(name="filters.StatusUpdate.DELETE_CHAT_PHOTO") """Messages that contain :attr:`telegram.Message.delete_chat_photo`.""" class _ForumTopicClosed(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.forum_topic_closed) FORUM_TOPIC_CLOSED = _ForumTopicClosed(name="filters.StatusUpdate.FORUM_TOPIC_CLOSED") """Messages that contain :attr:`telegram.Message.forum_topic_closed`. .. versionadded:: 20.0 """ class _ForumTopicCreated(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.forum_topic_created) FORUM_TOPIC_CREATED = _ForumTopicCreated(name="filters.StatusUpdate.FORUM_TOPIC_CREATED") """Messages that contain :attr:`telegram.Message.forum_topic_created`. .. versionadded:: 20.0 """ class _ForumTopicEdited(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.forum_topic_edited) FORUM_TOPIC_EDITED = _ForumTopicEdited(name="filters.StatusUpdate.FORUM_TOPIC_EDITED") """Messages that contain :attr:`telegram.Message.forum_topic_edited`. .. versionadded:: 20.0 """ class _ForumTopicReopened(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.forum_topic_reopened) FORUM_TOPIC_REOPENED = _ForumTopicReopened(name="filters.StatusUpdate.FORUM_TOPIC_REOPENED") """Messages that contain :attr:`telegram.Message.forum_topic_reopened`. .. versionadded:: 20.0 """ class _GeneralForumTopicHidden(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.general_forum_topic_hidden) GENERAL_FORUM_TOPIC_HIDDEN = _GeneralForumTopicHidden( name="filters.StatusUpdate.GENERAL_FORUM_TOPIC_HIDDEN" ) """Messages that contain :attr:`telegram.Message.general_forum_topic_hidden`. .. versionadded:: 20.0 """ class _GeneralForumTopicUnhidden(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.general_forum_topic_unhidden) GENERAL_FORUM_TOPIC_UNHIDDEN = _GeneralForumTopicUnhidden( name="filters.StatusUpdate.GENERAL_FORUM_TOPIC_UNHIDDEN" ) """Messages that contain :attr:`telegram.Message.general_forum_topic_unhidden`. .. versionadded:: 20.0 """ class _GiveawayCreated(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.giveaway_created) GIVEAWAY_CREATED = _GiveawayCreated(name="filters.StatusUpdate.GIVEAWAY_CREATED") """Messages that contain :attr:`telegram.Message.giveaway_created`. .. versionadded:: 20.8 """ class _GiveawayCompleted(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.giveaway_completed) GIVEAWAY_COMPLETED = _GiveawayCompleted(name="filters.StatusUpdate.GIVEAWAY_COMPLETED") """Messages that contain :attr:`telegram.Message.giveaway_completed`. .. versionadded:: 20.8 """ class _LeftChatMember(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.left_chat_member) LEFT_CHAT_MEMBER = _LeftChatMember(name="filters.StatusUpdate.LEFT_CHAT_MEMBER") """Messages that contain :attr:`telegram.Message.left_chat_member`.""" class _MessageAutoDeleteTimerChanged(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.message_auto_delete_timer_changed) MESSAGE_AUTO_DELETE_TIMER_CHANGED = _MessageAutoDeleteTimerChanged( "filters.StatusUpdate.MESSAGE_AUTO_DELETE_TIMER_CHANGED" ) """Messages that contain :attr:`telegram.Message.message_auto_delete_timer_changed` .. versionadded:: 13.4 """ class _Migrate(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.migrate_from_chat_id or message.migrate_to_chat_id) MIGRATE = _Migrate(name="filters.StatusUpdate.MIGRATE") """Messages that contain :attr:`telegram.Message.migrate_from_chat_id` or :attr:`telegram.Message.migrate_to_chat_id`.""" class _NewChatMembers(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.new_chat_members) NEW_CHAT_MEMBERS = _NewChatMembers(name="filters.StatusUpdate.NEW_CHAT_MEMBERS") """Messages that contain :attr:`telegram.Message.new_chat_members`.""" class _NewChatPhoto(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.new_chat_photo) NEW_CHAT_PHOTO = _NewChatPhoto(name="filters.StatusUpdate.NEW_CHAT_PHOTO") """Messages that contain :attr:`telegram.Message.new_chat_photo`.""" class _NewChatTitle(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.new_chat_title) NEW_CHAT_TITLE = _NewChatTitle(name="filters.StatusUpdate.NEW_CHAT_TITLE") """Messages that contain :attr:`telegram.Message.new_chat_title`.""" class _PinnedMessage(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.pinned_message) PINNED_MESSAGE = _PinnedMessage(name="filters.StatusUpdate.PINNED_MESSAGE") """Messages that contain :attr:`telegram.Message.pinned_message`.""" class _ProximityAlertTriggered(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.proximity_alert_triggered) PROXIMITY_ALERT_TRIGGERED = _ProximityAlertTriggered( "filters.StatusUpdate.PROXIMITY_ALERT_TRIGGERED" ) """Messages that contain :attr:`telegram.Message.proximity_alert_triggered`.""" class _UserShared(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.api_kwargs.get("user_shared")) USER_SHARED = _UserShared(name="filters.StatusUpdate.USER_SHARED") """Messages that contain ``"user_shared"`` in :attr:`telegram.TelegramObject.api_kwargs`. Warning: This will only catch the legacy ``user_shared`` field, not the new :attr:`telegram.Message.users_shared` attribute! .. versionchanged:: 21.0 Now relies on :attr:`telegram.TelegramObject.api_kwargs` as the native attribute ``Message.user_shared`` was removed. .. versionadded:: 20.1 .. deprecated:: 20.8 Use :attr:`USERS_SHARED` instead. """ class _UsersShared(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.users_shared) USERS_SHARED = _UsersShared(name="filters.StatusUpdate.USERS_SHARED") """Messages that contain :attr:`telegram.Message.users_shared`. .. versionadded:: 20.8 """ class _VideoChatEnded(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.video_chat_ended) VIDEO_CHAT_ENDED = _VideoChatEnded(name="filters.StatusUpdate.VIDEO_CHAT_ENDED") """Messages that contain :attr:`telegram.Message.video_chat_ended`. .. versionadded:: 13.4 .. versionchanged:: 20.0 This filter was formerly named ``VOICE_CHAT_ENDED`` """ class _VideoChatScheduled(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.video_chat_scheduled) VIDEO_CHAT_SCHEDULED = _VideoChatScheduled(name="filters.StatusUpdate.VIDEO_CHAT_SCHEDULED") """Messages that contain :attr:`telegram.Message.video_chat_scheduled`. .. versionadded:: 13.5 .. versionchanged:: 20.0 This filter was formerly named ``VOICE_CHAT_SCHEDULED`` """ class _VideoChatStarted(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.video_chat_started) VIDEO_CHAT_STARTED = _VideoChatStarted(name="filters.StatusUpdate.VIDEO_CHAT_STARTED") """Messages that contain :attr:`telegram.Message.video_chat_started`. .. versionadded:: 13.4 .. versionchanged:: 20.0 This filter was formerly named ``VOICE_CHAT_STARTED`` """ class _VideoChatParticipantsInvited(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.video_chat_participants_invited) VIDEO_CHAT_PARTICIPANTS_INVITED = _VideoChatParticipantsInvited( "filters.StatusUpdate.VIDEO_CHAT_PARTICIPANTS_INVITED" ) """Messages that contain :attr:`telegram.Message.video_chat_participants_invited`. .. versionadded:: 13.4 .. versionchanged:: 20.0 This filter was formerly named ``VOICE_CHAT_PARTICIPANTS_INVITED`` """ class _WebAppData(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.web_app_data) WEB_APP_DATA = _WebAppData(name="filters.StatusUpdate.WEB_APP_DATA") """Messages that contain :attr:`telegram.Message.web_app_data`. .. versionadded:: 20.0 """ class _WriteAccessAllowed(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.write_access_allowed) WRITE_ACCESS_ALLOWED = _WriteAccessAllowed(name="filters.StatusUpdate.WRITE_ACCESS_ALLOWED") """Messages that contain :attr:`telegram.Message.write_access_allowed`. .. versionadded:: 20.0 """ class Sticker: """Filters messages which contain a sticker. Examples: Use this filter like: ``filters.Sticker.VIDEO``. Or, just use ``filters.Sticker.ALL`` for any type of sticker. Caution: ``filters.Sticker`` itself is *not* a filter, but just a convenience namespace. """ __slots__ = () class _All(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.sticker) ALL = _All(name="filters.Sticker.ALL") """Messages that contain :attr:`telegram.Message.sticker`.""" class _Animated(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.sticker) and bool(message.sticker.is_animated) # type: ignore ANIMATED = _Animated(name="filters.Sticker.ANIMATED") """Messages that contain :attr:`telegram.Message.sticker` and :attr:`is animated `. .. versionadded:: 20.0 """ class _Static(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.sticker) and ( not bool(message.sticker.is_animated) # type: ignore[union-attr] and not bool(message.sticker.is_video) # type: ignore[union-attr] ) STATIC = _Static(name="filters.Sticker.STATIC") """Messages that contain :attr:`telegram.Message.sticker` and is a static sticker, i.e. does not contain :attr:`telegram.Sticker.is_animated` or :attr:`telegram.Sticker.is_video`. .. versionadded:: 20.0 """ class _Video(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.sticker) and bool(message.sticker.is_video) # type: ignore VIDEO = _Video(name="filters.Sticker.VIDEO") """Messages that contain :attr:`telegram.Message.sticker` and is a :attr:`video sticker `. .. versionadded:: 20.0 """ class _Premium(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.sticker) and bool( message.sticker.premium_animation # type: ignore ) PREMIUM = _Premium(name="filters.Sticker.PREMIUM") """Messages that contain :attr:`telegram.Message.sticker` and have a :attr:`premium animation `. .. versionadded:: 20.0 """ # neither mask nor emoji can be a message.sticker, so no filters for them class _Story(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.story) STORY = _Story(name="filters.STORY") """Messages that contain :attr:`telegram.Message.story`. .. versionadded:: 20.5 """ class SuccessfulPayment(MessageFilter): """Successful Payment Messages. If a list of invoice payloads is passed, it filters messages to only allow those whose `invoice_payload` is appearing in the given list. Examples: `MessageHandler(filters.SuccessfulPayment(['Custom-Payload']), callback_method)` .. seealso:: :attr:`telegram.ext.filters.SUCCESSFUL_PAYMENT` Args: invoice_payloads (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which invoice payloads to allow. Only exact matches are allowed. If not specified, will allow any invoice payload. .. versionadded:: 20.8 """ __slots__ = ("invoice_payloads",) def __init__(self, invoice_payloads: Optional[Union[List[str], Tuple[str, ...]]] = None): self.invoice_payloads: Optional[Sequence[str]] = invoice_payloads super().__init__( name=( f"filters.SuccessfulPayment({invoice_payloads})" if invoice_payloads else "filters.SUCCESSFUL_PAYMENT" ) ) def filter(self, message: Message) -> bool: if self.invoice_payloads is None: return bool(message.successful_payment) return ( payment.invoice_payload in self.invoice_payloads if (payment := message.successful_payment) else False ) SUCCESSFUL_PAYMENT = SuccessfulPayment() """Messages that contain :attr:`telegram.Message.successful_payment`.""" class Text(MessageFilter): """Text Messages. If a list of strings is passed, it filters messages to only allow those whose text is appearing in the given list. Examples: A simple use case for passing a list is to allow only messages that were sent by a custom :class:`telegram.ReplyKeyboardMarkup`:: buttons = ['Start', 'Settings', 'Back'] markup = ReplyKeyboardMarkup.from_column(buttons) ... MessageHandler(filters.Text(buttons), callback_method) .. seealso:: :attr:`telegram.ext.filters.TEXT` Note: * Dice messages don't have text. If you want to filter either text or dice messages, use ``filters.TEXT | filters.Dice.ALL``. * Messages containing a command are accepted by this filter. Use ``filters.TEXT & (~filters.COMMAND)``, if you want to filter only text messages without commands. Args: strings (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which messages to allow. Only exact matches are allowed. If not specified, will allow any text message. """ __slots__ = ("strings",) def __init__(self, strings: Optional[Union[List[str], Tuple[str, ...]]] = None): self.strings: Optional[Sequence[str]] = strings super().__init__(name=f"filters.Text({strings})" if strings else "filters.TEXT") def filter(self, message: Message) -> bool: if self.strings is None: return bool(message.text) return message.text in self.strings if message.text else False TEXT = Text() """ Shortcut for :class:`telegram.ext.filters.Text()`. Examples: To allow any text message, simply use ``MessageHandler(filters.TEXT, callback_method)``. """ class UpdateType: """ Subset for filtering the type of update. Examples: Use these filters like: ``filters.UpdateType.MESSAGE`` or ``filters.UpdateType.CHANNEL_POSTS`` etc. Caution: ``filters.UpdateType`` itself is *not* a filter, but just a convenience namespace. """ __slots__ = () class _ChannelPost(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return update.channel_post is not None CHANNEL_POST = _ChannelPost(name="filters.UpdateType.CHANNEL_POST") """Updates with :attr:`telegram.Update.channel_post`.""" class _ChannelPosts(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return update.channel_post is not None or update.edited_channel_post is not None CHANNEL_POSTS = _ChannelPosts(name="filters.UpdateType.CHANNEL_POSTS") """Updates with either :attr:`telegram.Update.channel_post` or :attr:`telegram.Update.edited_channel_post`.""" class _Edited(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return ( update.edited_message is not None or update.edited_channel_post is not None or update.edited_business_message is not None ) EDITED = _Edited(name="filters.UpdateType.EDITED") """Updates with :attr:`telegram.Update.edited_message`, :attr:`telegram.Update.edited_channel_post`, or :attr:`telegram.Update.edited_business_message`. .. versionadded:: 20.0 .. versionchanged:: 21.1 Added :attr:`telegram.Update.edited_business_message` to the filter. """ class _EditedChannelPost(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return update.edited_channel_post is not None EDITED_CHANNEL_POST = _EditedChannelPost(name="filters.UpdateType.EDITED_CHANNEL_POST") """Updates with :attr:`telegram.Update.edited_channel_post`.""" class _EditedMessage(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return update.edited_message is not None EDITED_MESSAGE = _EditedMessage(name="filters.UpdateType.EDITED_MESSAGE") """Updates with :attr:`telegram.Update.edited_message`.""" class _Message(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return update.message is not None MESSAGE = _Message(name="filters.UpdateType.MESSAGE") """Updates with :attr:`telegram.Update.message`.""" class _Messages(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return update.message is not None or update.edited_message is not None MESSAGES = _Messages(name="filters.UpdateType.MESSAGES") """Updates with either :attr:`telegram.Update.message` or :attr:`telegram.Update.edited_message`. """ class _BusinessMessage(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return update.business_message is not None BUSINESS_MESSAGE = _BusinessMessage(name="filters.UpdateType.BUSINESS_MESSAGE") """Updates with :attr:`telegram.Update.business_message`. .. versionadded:: 21.1""" class _EditedBusinessMessage(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return update.edited_business_message is not None EDITED_BUSINESS_MESSAGE = _EditedBusinessMessage( name="filters.UpdateType.EDITED_BUSINESS_MESSAGE" ) """Updates with :attr:`telegram.Update.edited_business_message`. .. versionadded:: 21.1 """ class _BusinessMessages(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return ( update.business_message is not None or update.edited_business_message is not None ) BUSINESS_MESSAGES = _BusinessMessages(name="filters.UpdateType.BUSINESS_MESSAGES") """Updates with either :attr:`telegram.Update.business_message` or :attr:`telegram.Update.edited_business_message`. .. versionadded:: 21.1 """ class User(_ChatUserBaseFilter): """Filters messages to allow only those which are from specified user ID(s) or username(s). Examples: ``MessageHandler(filters.User(1234), callback_method)`` Args: user_id(:obj:`int` | Collection[:obj:`int`], optional): Which user ID(s) to allow through. username(:obj:`str` | Collection[:obj:`str`], optional): Which username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user is specified in :attr:`user_ids` and :attr:`usernames`. Defaults to :obj:`False`. Raises: RuntimeError: If ``user_id`` and ``username`` are both present. Attributes: allow_empty (:obj:`bool`): Whether updates should be processed, if no user is specified in :attr:`user_ids` and :attr:`usernames`. """ __slots__ = () def __init__( self, user_id: Optional[SCT[int]] = None, username: Optional[SCT[str]] = None, allow_empty: bool = False, ): super().__init__(chat_id=user_id, username=username, allow_empty=allow_empty) self._chat_id_name = "user_id" def _get_chat_or_user(self, message: Message) -> Optional[TGUser]: return message.from_user @property def user_ids(self) -> FrozenSet[int]: """ Which user ID(s) to allow through. Warning: :attr:`user_ids` will give a *copy* of the saved user ids as :obj:`frozenset`. This is to ensure thread safety. To add/remove a user, you should use :meth:`add_user_ids`, and :meth:`remove_user_ids`. Only update the entire set by ``filter.user_ids = new_set``, if you are entirely sure that it is not causing race conditions, as this will complete replace the current set of allowed users. Returns: frozenset(:obj:`int`) """ return self.chat_ids @user_ids.setter def user_ids(self, user_id: SCT[int]) -> None: self.chat_ids = user_id # type: ignore[assignment] def add_user_ids(self, user_id: SCT[int]) -> None: """ Add one or more users to the allowed user ids. Args: user_id(:obj:`int` | Collection[:obj:`int`]): Which user ID(s) to allow through. """ return super()._add_chat_ids(user_id) def remove_user_ids(self, user_id: SCT[int]) -> None: """ Remove one or more users from allowed user ids. Args: user_id(:obj:`int` | Collection[:obj:`int`]): Which user ID(s) to disallow through. """ return super()._remove_chat_ids(user_id) class _User(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.from_user) USER = _User(name="filters.USER") """This filter filters *any* message that has a :attr:`telegram.Message.from_user`.""" class _UserAttachment(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return bool(update.effective_user) and bool( update.effective_user.added_to_attachment_menu # type: ignore ) USER_ATTACHMENT = _UserAttachment(name="filters.USER_ATTACHMENT") """This filter filters *any* message that have a user who added the bot to their :attr:`attachment menu ` as :attr:`telegram.Update.effective_user`. .. versionadded:: 20.0 """ class _UserPremium(UpdateFilter): __slots__ = () def filter(self, update: Update) -> bool: return bool(update.effective_user) and bool( update.effective_user.is_premium # type: ignore ) PREMIUM_USER = _UserPremium(name="filters.PREMIUM_USER") """This filter filters *any* message from a :attr:`Telegram Premium user ` as :attr:`telegram.Update.effective_user`. .. versionadded:: 20.0 """ class _Venue(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.venue) VENUE = _Venue(name="filters.VENUE") """Messages that contain :attr:`telegram.Message.venue`.""" class ViaBot(_ChatUserBaseFilter): """Filters messages to allow only those which are from specified via_bot ID(s) or username(s). Examples: ``MessageHandler(filters.ViaBot(1234), callback_method)`` .. seealso:: :attr:`~telegram.ext.filters.VIA_BOT` Args: bot_id(:obj:`int` | Collection[:obj:`int`], optional): Which bot ID(s) to allow through. username(:obj:`str` | Collection[:obj:`str`], optional): Which username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user is specified in :attr:`bot_ids` and :attr:`usernames`. Defaults to :obj:`False`. Raises: RuntimeError: If ``bot_id`` and ``username`` are both present. Attributes: allow_empty (:obj:`bool`): Whether updates should be processed, if no bot is specified in :attr:`bot_ids` and :attr:`usernames`. """ __slots__ = () def __init__( self, bot_id: Optional[SCT[int]] = None, username: Optional[SCT[str]] = None, allow_empty: bool = False, ): super().__init__(chat_id=bot_id, username=username, allow_empty=allow_empty) self._chat_id_name = "bot_id" def _get_chat_or_user(self, message: Message) -> Optional[TGUser]: return message.via_bot @property def bot_ids(self) -> FrozenSet[int]: """ Which bot ID(s) to allow through. Warning: :attr:`bot_ids` will give a *copy* of the saved bot ids as :obj:`frozenset`. This is to ensure thread safety. To add/remove a bot, you should use :meth:`add_bot_ids`, and :meth:`remove_bot_ids`. Only update the entire set by ``filter.bot_ids = new_set``, if you are entirely sure that it is not causing race conditions, as this will complete replace the current set of allowed bots. Returns: frozenset(:obj:`int`) """ return self.chat_ids @bot_ids.setter def bot_ids(self, bot_id: SCT[int]) -> None: self.chat_ids = bot_id # type: ignore[assignment] def add_bot_ids(self, bot_id: SCT[int]) -> None: """ Add one or more bots to the allowed bot ids. Args: bot_id(:obj:`int` | Collection[:obj:`int`]): Which bot ID(s) to allow through. """ return super()._add_chat_ids(bot_id) def remove_bot_ids(self, bot_id: SCT[int]) -> None: """ Remove one or more bots from allowed bot ids. Args: bot_id(:obj:`int` | Collection[:obj:`int`], optional): Which bot ID(s) to disallow through. """ return super()._remove_chat_ids(bot_id) class _ViaBot(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.via_bot) VIA_BOT = _ViaBot(name="filters.VIA_BOT") """This filter filters for message that were sent via *any* bot. .. seealso:: :class:`~telegram.ext.filters.ViaBot`""" class _Video(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.video) VIDEO = _Video(name="filters.VIDEO") """Messages that contain :attr:`telegram.Message.video`.""" class _VideoNote(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.video_note) VIDEO_NOTE = _VideoNote(name="filters.VIDEO_NOTE") """Messages that contain :attr:`telegram.Message.video_note`.""" class _Voice(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.voice) VOICE = _Voice("filters.VOICE") """Messages that contain :attr:`telegram.Message.voice`.""" class _ReplyToStory(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.reply_to_story) REPLY_TO_STORY = _ReplyToStory(name="filters.REPLY_TO_STORY") """Messages that contain :attr:`telegram.Message.reply_to_story`.""" class _BoostAdded(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.boost_added) BOOST_ADDED = _BoostAdded(name="filters.BOOST_ADDED") """Messages that contain :attr:`telegram.Message.boost_added`.""" class _SenderBoostCount(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: return bool(message.sender_boost_count) SENDER_BOOST_COUNT = _SenderBoostCount(name="filters.SENDER_BOOST_COUNT") """Messages that contain :attr:`telegram.Message.sender_boost_count`.""" python-telegram-bot-21.1.1/telegram/helpers.py000066400000000000000000000161241460724040100213130ustar00rootroot00000000000000#!/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}" python-telegram-bot-21.1.1/telegram/py.typed000066400000000000000000000000001460724040100207600ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/request/000077500000000000000000000000001460724040100207635ustar00rootroot00000000000000python-telegram-bot-21.1.1/telegram/request/__init__.py000066400000000000000000000020711460724040100230740ustar00rootroot00000000000000# !/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 handle the networking backend of ``python-telegram-bot``.""" from ._baserequest import BaseRequest from ._httpxrequest import HTTPXRequest from ._requestdata import RequestData __all__ = ("BaseRequest", "HTTPXRequest", "RequestData") python-telegram-bot-21.1.1/telegram/request/_baserequest.py000066400000000000000000000477761460724040100240440ustar00rootroot00000000000000#!/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 abstract class to make POST and GET requests.""" import abc import json from http import HTTPStatus from types import TracebackType from typing import AsyncContextManager, Final, List, Optional, Tuple, Type, TypeVar, Union, final from telegram._utils.defaultvalue import DEFAULT_NONE as _DEFAULT_NONE from telegram._utils.defaultvalue import DefaultValue from telegram._utils.logging import get_logger from telegram._utils.types import JSONDict, ODVInput from telegram._utils.warnings import warn from telegram._version import __version__ as ptb_ver from telegram.error import ( BadRequest, ChatMigrated, Conflict, Forbidden, InvalidToken, NetworkError, RetryAfter, TelegramError, ) from telegram.request._requestdata import RequestData from telegram.warnings import PTBDeprecationWarning RT = TypeVar("RT", bound="BaseRequest") _LOGGER = get_logger(__name__, class_name="BaseRequest") class BaseRequest( AsyncContextManager["BaseRequest"], abc.ABC, ): """Abstract interface class that allows python-telegram-bot to make requests to the Bot API. Can be implemented via different asyncio HTTP libraries. An implementation of this class must implement all abstract methods and properties. Instances of this class can be used as asyncio context managers, where .. code:: python async with request_object: # code is roughly equivalent to .. code:: python try: await request_object.initialize() # code finally: await request_object.shutdown() .. seealso:: :meth:`__aenter__` and :meth:`__aexit__`. Tip: JSON encoding and decoding is done with the standard library's :mod:`json` by default. To use a custom library for this, you can override :meth:`parse_json_payload` and implement custom logic to encode the keys of :attr:`telegram.request.RequestData.parameters`. .. seealso:: :wiki:`Architecture Overview `, :wiki:`Builder Pattern ` .. versionadded:: 20.0 """ __slots__ = () USER_AGENT: Final[str] = f"python-telegram-bot v{ptb_ver} (https://python-telegram-bot.org)" """:obj:`str`: A description that can be used as user agent for requests made to the Bot API. """ DEFAULT_NONE: Final[DefaultValue[None]] = _DEFAULT_NONE """:class:`object`: A special object that indicates that an argument of a function was not explicitly passed. Used for the timeout parameters of :meth:`post` and :meth:`do_request`. Example: When calling ``request.post(url)``, ``request`` should use the default timeouts set on initialization. When calling ``request.post(url, connect_timeout=5, read_timeout=None)``, ``request`` should use ``5`` for the connect timeout and :obj:`None` for the read timeout. Use ``if parameter is (not) BaseRequest.DEFAULT_NONE:`` to check if the parameter was set. """ async def __aenter__(self: RT) -> RT: """|async_context_manager| :meth:`initializes ` the Request. Returns: The initialized Request instance. Raises: :exc:`Exception`: If an exception is raised during initialization, :meth:`shutdown` is called in this case. """ try: await self.initialize() return self except Exception as exc: await self.shutdown() raise exc 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 Request.""" # 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() @property def read_timeout(self) -> Optional[float]: """This property must return the default read timeout in seconds used by this class. More precisely, the returned value should be the one used when :paramref:`post.read_timeout` of :meth:post` is not passed/equal to :attr:`DEFAULT_NONE`. .. versionadded:: 20.7 Warning: For now this property does not need to be implemented by subclasses and will raise :exc:`NotImplementedError` if accessed without being overridden. However, in future versions, this property will be abstract and must be implemented by subclasses. Returns: :obj:`float` | :obj:`None`: The read timeout in seconds. """ raise NotImplementedError @abc.abstractmethod async def initialize(self) -> None: """Initialize resources used by this class. Must be implemented by a subclass.""" @abc.abstractmethod async def shutdown(self) -> None: """Stop & clear resources used by this class. Must be implemented by a subclass.""" @final async def post( self, url: str, request_data: Optional[RequestData] = 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, ) -> Union[JSONDict, List[JSONDict], bool]: """Makes a request to the Bot API handles the return code and parses the answer. Warning: This method will be called by the methods of :class:`telegram.Bot` and should *not* be called manually. Args: url (:obj:`str`): The URL to request. request_data (:class:`telegram.request.RequestData`, optional): An object containing information about parameters and files to upload for the request. read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a response from Telegram's server instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a write operation to complete (in terms of a network socket; i.e. POSTing a request or uploading a file) instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection attempt to a server to succeed instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection to become available instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. Returns: The JSON response of the Bot API. """ result = await self._request_wrapper( url=url, method="POST", request_data=request_data, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, ) json_data = self.parse_json_payload(result) # For successful requests, the results are in the 'result' entry # see https://core.telegram.org/bots/api#making-requests return json_data["result"] @final async def retrieve( self, url: 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, ) -> bytes: """Retrieve the contents of a file by its URL. Warning: This method will be called by the methods of :class:`telegram.Bot` and should *not* be called manually. Args: url (:obj:`str`): The web location we want to retrieve. read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a response from Telegram's server instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a write operation to complete (in terms of a network socket; i.e. POSTing a request or uploading a file) instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection attempt to a server to succeed instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection to become available instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. Returns: :obj:`bytes`: The files contents. """ return await self._request_wrapper( url=url, method="GET", read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, ) async def _request_wrapper( self, url: str, method: str, request_data: Optional[RequestData] = 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, ) -> bytes: """Wraps the real implementation request method. Performs the following tasks: * Handle the various HTTP response codes. * Parse the Telegram server response. Args: url (:obj:`str`): The URL to request. method (:obj:`str`): HTTP method (i.e. 'POST', 'GET', etc.). request_data (:class:`telegram.request.RequestData`, optional): An object containing information about parameters and files to upload for the request. read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a response from Telegram's server instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a write operation to complete (in terms of a network socket; i.e. POSTing a request or uploading a file) instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection attempt to a server to succeed instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection to become available instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. Returns: bytes: The payload part of the HTTP server response. Raises: TelegramError """ # Import needs to be here since HTTPXRequest is a subclass of BaseRequest from telegram.request import HTTPXRequest # pylint: disable=import-outside-toplevel # 20 is the documented default value for all the media related bot methods and custom # implementations of BaseRequest may explicitly rely on that. Hence, we follow the # standard deprecation policy and deprecate starting with version 20.7. # For our own implementation HTTPXRequest, we can handle that ourselves, so we skip the # warning in that case. has_files = request_data and request_data.multipart_data if ( has_files and not isinstance(self, HTTPXRequest) and isinstance(write_timeout, DefaultValue) ): warn( f"The `write_timeout` parameter passed to {self.__class__.__name__}.do_request " "will default to `BaseRequest.DEFAULT_NONE` instead of 20 in future versions " "for *all* methods of the `Bot` class, including methods sending media.", PTBDeprecationWarning, stacklevel=3, ) write_timeout = 20 try: code, payload = await self.do_request( url=url, method=method, request_data=request_data, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, ) except TelegramError as exc: raise exc except Exception as exc: raise NetworkError(f"Unknown error in HTTP implementation: {exc!r}") from exc if HTTPStatus.OK <= code <= 299: # 200-299 range are HTTP success statuses return payload response_data = self.parse_json_payload(payload) description = response_data.get("description") message = description if description else "Unknown HTTPError" # In some special cases, we can raise more informative exceptions: # see https://core.telegram.org/bots/api#responseparameters and # https://core.telegram.org/bots/api#making-requests # TGs response also has the fields 'ok' and 'error_code'. # However, we rather rely on the HTTP status code for now. parameters = response_data.get("parameters") if parameters: migrate_to_chat_id = parameters.get("migrate_to_chat_id") if migrate_to_chat_id: raise ChatMigrated(migrate_to_chat_id) retry_after = parameters.get("retry_after") if retry_after: raise RetryAfter(retry_after) message += f"\nThe server response contained unknown parameters: {parameters}" if code == HTTPStatus.FORBIDDEN: # 403 raise Forbidden(message) if code in (HTTPStatus.NOT_FOUND, HTTPStatus.UNAUTHORIZED): # 404 and 401 # TG returns 404 Not found for # 1) malformed tokens # 2) correct tokens but non-existing method, e.g. api.tg.org/botTOKEN/unkonwnMethod # 2) is relevant only for Bot.do_api_request, where we have special handing for it. # TG returns 401 Unauthorized for correctly formatted tokens that are not valid raise InvalidToken(message) if code == HTTPStatus.BAD_REQUEST: # 400 raise BadRequest(message) if code == HTTPStatus.CONFLICT: # 409 raise Conflict(message) if code == HTTPStatus.BAD_GATEWAY: # 502 raise NetworkError(description or "Bad Gateway") raise NetworkError(f"{message} ({code})") @staticmethod def parse_json_payload(payload: bytes) -> JSONDict: """Parse the JSON returned from Telegram. Tip: By default, this method uses the standard library's :func:`json.loads` and ``errors="replace"`` in :meth:`bytes.decode`. You can override it to customize either of these behaviors. Args: payload (:obj:`bytes`): The UTF-8 encoded JSON payload as returned by Telegram. Returns: dict: A JSON parsed as Python dict with results. Raises: TelegramError: If loading the JSON data failed """ decoded_s = payload.decode("utf-8", "replace") try: return json.loads(decoded_s) except ValueError as exc: _LOGGER.error('Can not load invalid JSON data: "%s"', decoded_s) raise TelegramError("Invalid server response") from exc @abc.abstractmethod async def do_request( self, url: str, method: str, request_data: Optional[RequestData] = 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, ) -> Tuple[int, bytes]: """Makes a request to the Bot API. Must be implemented by a subclass. Warning: This method will be called by :meth:`post` and :meth:`retrieve`. It should *not* be called manually. Args: url (:obj:`str`): The URL to request. method (:obj:`str`): HTTP method (i.e. ``'POST'``, ``'GET'``, etc.). request_data (:class:`telegram.request.RequestData`, optional): An object containing information about parameters and files to upload for the request. read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a response from Telegram's server instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a write operation to complete (in terms of a network socket; i.e. POSTing a request or uploading a file) instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection attempt to a server to succeed instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection to become available instead of the time specified during creating of this object. Defaults to :attr:`DEFAULT_NONE`. Returns: Tuple[:obj:`int`, :obj:`bytes`]: The HTTP return code & the payload part of the server response. """ python-telegram-bot-21.1.1/telegram/request/_httpxrequest.py000066400000000000000000000312501460724040100242550ustar00rootroot00000000000000#!/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 methods to make POST and GET requests using the httpx library.""" from typing import Collection, Optional, Tuple, Union import httpx from telegram._utils.defaultvalue import DefaultValue from telegram._utils.logging import get_logger from telegram._utils.types import HTTPVersion, ODVInput, SocketOpt from telegram._utils.warnings import warn from telegram.error import NetworkError, TimedOut from telegram.request._baserequest import BaseRequest from telegram.request._requestdata import RequestData from telegram.warnings import PTBDeprecationWarning # Note to future devs: # Proxies are currently only tested manually. The httpx development docs have a nice guide on that: # https://www.python-httpx.org/contributing/#development-proxy-setup (also saved on archive.org) # That also works with socks5. Just pass `--mode socks5` to mitmproxy _LOGGER = get_logger(__name__, "HTTPXRequest") class HTTPXRequest(BaseRequest): """Implementation of :class:`~telegram.request.BaseRequest` using the library `httpx `_. .. versionadded:: 20.0 Args: connection_pool_size (:obj:`int`, optional): Number of connections to keep in the connection pool. Defaults to ``1``. Note: Independent of the value, one additional connection will be reserved for :meth:`telegram.Bot.get_updates`. proxy_url (:obj:`str`, optional): Legacy name for :paramref:`proxy`, kept for backward compatibility. Defaults to :obj:`None`. .. deprecated:: 20.7 read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a response from Telegram's server. This value is used unless a different value is passed to :meth:`do_request`. Defaults to ``5``. write_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a write operation to complete (in terms of a network socket; i.e. POSTing a request or uploading a file). This value is used unless a different value is passed to :meth:`do_request`. Defaults to ``5``. Hint: This timeout is used for all requests except for those that upload media/files. For the latter, :paramref:`media_write_timeout` is used. connect_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection attempt to a server to succeed. This value is used unless a different value is passed to :meth:`do_request`. Defaults to ``5``. pool_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum amount of time (in seconds) to wait for a connection to become available. This value is used unless a different value is passed to :meth:`do_request`. Defaults to ``1``. Warning: With a finite pool timeout, you must expect :exc:`telegram.error.TimedOut` exceptions to be thrown when more requests are made simultaneously than there are connections in the connection pool! http_version (:obj:`str`, optional): If ``"2"`` or ``"2.0"``, HTTP/2 will be used instead of HTTP/1.1. Defaults to ``"1.1"``. .. versionadded:: 20.1 .. versionchanged:: 20.2 Reset the default version to 1.1. .. versionchanged:: 20.5 Accept ``"2"`` as a valid value. socket_options (Collection[:obj:`tuple`], optional): Socket options to be passed to the underlying `library \ `_. Note: The values accepted by this parameter depend on the operating system. This is a low-level parameter and should only be used if you are familiar with these concepts. .. versionadded:: 20.7 proxy (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``, optional): The URL to a proxy server, a ``httpx.Proxy`` object or a ``httpx.URL`` object. For example ``'http://127.0.0.1:3128'`` or ``'socks5://127.0.0.1:3128'``. Defaults to :obj:`None`. Note: * The proxy URL can also be set via the environment variables ``HTTPS_PROXY`` or ``ALL_PROXY``. See `the docs of httpx`_ for more info. * HTTPS proxies can be configured by passing a ``httpx.Proxy`` object with a corresponding ``ssl_context``. * For Socks5 support, additional dependencies are required. Make sure to install PTB via :command:`pip install "python-telegram-bot[socks]"` in this case. * Socks5 proxies can not be set via environment variables. .. _the docs of httpx: https://www.python-httpx.org/environment_variables/#proxies .. versionadded:: 20.7 media_write_timeout (:obj:`float` | :obj:`None`, optional): Like :paramref:`write_timeout`, but used only for requests that upload media/files. This value is used unless a different value is passed to :paramref:`do_request.write_timeout` of :meth:`do_request`. Defaults to ``20`` seconds. .. versionadded:: 21.0 """ __slots__ = ("_client", "_client_kwargs", "_http_version", "_media_write_timeout") def __init__( self, connection_pool_size: int = 1, proxy_url: Optional[Union[str, httpx.Proxy, httpx.URL]] = None, read_timeout: Optional[float] = 5.0, write_timeout: Optional[float] = 5.0, connect_timeout: Optional[float] = 5.0, pool_timeout: Optional[float] = 1.0, http_version: HTTPVersion = "1.1", socket_options: Optional[Collection[SocketOpt]] = None, proxy: Optional[Union[str, httpx.Proxy, httpx.URL]] = None, media_write_timeout: Optional[float] = 20.0, ): if proxy_url is not None and proxy is not None: raise ValueError("The parameters `proxy_url` and `proxy` are mutually exclusive.") if proxy_url is not None: proxy = proxy_url warn( "The parameter `proxy_url` is deprecated since version 20.7. Use `proxy` " "instead.", PTBDeprecationWarning, stacklevel=2, ) self._http_version = http_version self._media_write_timeout = media_write_timeout timeout = httpx.Timeout( connect=connect_timeout, read=read_timeout, write=write_timeout, pool=pool_timeout, ) limits = httpx.Limits( max_connections=connection_pool_size, max_keepalive_connections=connection_pool_size, ) if http_version not in ("1.1", "2", "2.0"): raise ValueError("`http_version` must be either '1.1', '2.0' or '2'.") http1 = http_version == "1.1" http_kwargs = {"http1": http1, "http2": not http1} transport = ( httpx.AsyncHTTPTransport( socket_options=socket_options, ) if socket_options else None ) self._client_kwargs = { "timeout": timeout, "proxy": proxy, "limits": limits, "transport": transport, **http_kwargs, } try: self._client = self._build_client() except ImportError as exc: if "httpx[http2]" not in str(exc) and "httpx[socks]" not in str(exc): raise exc if "httpx[socks]" in str(exc): raise RuntimeError( "To use Socks5 proxies, PTB must be installed via `pip install " '"python-telegram-bot[socks]"`.' ) from exc raise RuntimeError( "To use HTTP/2, PTB must be installed via `pip install " '"python-telegram-bot[http2]"`.' ) from exc @property def http_version(self) -> str: """ :obj:`str`: Used HTTP version, see :paramref:`http_version`. .. versionadded:: 20.2 """ return self._http_version @property def read_timeout(self) -> Optional[float]: """See :attr:`BaseRequest.read_timeout`. Returns: :obj:`float` | :obj:`None`: The default read timeout in seconds as passed to :paramref:`HTTPXRequest.read_timeout`. """ return self._client.timeout.read def _build_client(self) -> httpx.AsyncClient: return httpx.AsyncClient(**self._client_kwargs) # type: ignore[arg-type] async def initialize(self) -> None: """See :meth:`BaseRequest.initialize`.""" if self._client.is_closed: self._client = self._build_client() async def shutdown(self) -> None: """See :meth:`BaseRequest.shutdown`.""" if self._client.is_closed: _LOGGER.debug("This HTTPXRequest is already shut down. Returning.") return await self._client.aclose() async def do_request( self, url: str, method: str, request_data: Optional[RequestData] = None, read_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, write_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, connect_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, pool_timeout: ODVInput[float] = BaseRequest.DEFAULT_NONE, ) -> Tuple[int, bytes]: """See :meth:`BaseRequest.do_request`.""" if self._client.is_closed: raise RuntimeError("This HTTPXRequest is not initialized!") files = request_data.multipart_data if request_data else None data = request_data.json_parameters if request_data else None # If user did not specify timeouts (for e.g. in a bot method), use the default ones when we # created this instance. if isinstance(read_timeout, DefaultValue): read_timeout = self._client.timeout.read if isinstance(connect_timeout, DefaultValue): connect_timeout = self._client.timeout.connect if isinstance(pool_timeout, DefaultValue): pool_timeout = self._client.timeout.pool if isinstance(write_timeout, DefaultValue): write_timeout = self._client.timeout.write if not files else self._media_write_timeout timeout = httpx.Timeout( connect=connect_timeout, read=read_timeout, write=write_timeout, pool=pool_timeout, ) try: res = await self._client.request( method=method, url=url, headers={"User-Agent": self.USER_AGENT}, timeout=timeout, files=files, data=data, ) except httpx.TimeoutException as err: if isinstance(err, httpx.PoolTimeout): raise TimedOut( message=( "Pool timeout: All connections in the connection pool are occupied. " "Request was *not* sent to Telegram. Consider adjusting the connection " "pool size or the pool timeout." ) ) from err raise TimedOut from err except httpx.HTTPError as err: # HTTPError must come last as its the base httpx exception class # TODO p4: do something smart here; for now just raise NetworkError # We include the class name for easier debugging. Especially useful if the error # message of `err` is empty. raise NetworkError(f"httpx.{err.__class__.__name__}: {err}") from err return res.status_code, res.content python-telegram-bot-21.1.1/telegram/request/_requestdata.py000066400000000000000000000121241460724040100240160ustar00rootroot00000000000000#!/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 holds the parameters of a request to the Bot API.""" import json from typing import Any, Dict, List, Optional, Union, final from urllib.parse import urlencode from telegram._utils.types import UploadFileDict from telegram.request._requestparameter import RequestParameter @final class RequestData: """Instances of this class collect the data needed for one request to the Bot API, including all parameters and files to be sent along with the request. .. versionadded:: 20.0 Warning: How exactly instances of this are created should be considered an implementation detail and not part of PTBs public API. Users should exclusively rely on the documented attributes, properties and methods. Attributes: contains_files (:obj:`bool`): Whether this object contains files to be uploaded via ``multipart/form-data``. """ __slots__ = ("_parameters", "contains_files") def __init__(self, parameters: Optional[List[RequestParameter]] = None): self._parameters: List[RequestParameter] = parameters or [] self.contains_files: bool = any(param.input_files for param in self._parameters) @property def parameters(self) -> Dict[str, Union[str, int, List[Any], Dict[Any, Any]]]: """Gives the parameters as mapping of parameter name to the parameter value, which can be a single object of type :obj:`int`, :obj:`float`, :obj:`str` or :obj:`bool` or any (possibly nested) composition of lists, tuples and dictionaries, where each entry, key and value is of one of the mentioned types. """ return { param.name: param.value # type: ignore[misc] for param in self._parameters if param.value is not None } @property def json_parameters(self) -> Dict[str, str]: """Gives the parameters as mapping of parameter name to the respective JSON encoded value. Tip: By default, this property uses the standard library's :func:`json.dumps`. To use a custom library for JSON encoding, you can directly encode the keys of :attr:`parameters` - note that string valued keys should not be JSON encoded. """ return { param.name: param.json_value for param in self._parameters if param.json_value is not None } def url_encoded_parameters(self, encode_kwargs: Optional[Dict[str, Any]] = None) -> str: """Encodes the parameters with :func:`urllib.parse.urlencode`. Args: encode_kwargs (Dict[:obj:`str`, any], optional): Additional keyword arguments to pass along to :func:`urllib.parse.urlencode`. """ if encode_kwargs: return urlencode(self.json_parameters, **encode_kwargs) return urlencode(self.json_parameters) def parametrized_url(self, url: str, encode_kwargs: Optional[Dict[str, Any]] = None) -> str: """Shortcut for attaching the return value of :meth:`url_encoded_parameters` to the :paramref:`url`. Args: url (:obj:`str`): The URL the parameters will be attached to. encode_kwargs (Dict[:obj:`str`, any], optional): Additional keyword arguments to pass along to :func:`urllib.parse.urlencode`. """ url_parameters = self.url_encoded_parameters(encode_kwargs=encode_kwargs) return f"{url}?{url_parameters}" @property def json_payload(self) -> bytes: """The :attr:`parameters` as UTF-8 encoded JSON payload. Tip: By default, this property uses the standard library's :func:`json.dumps`. To use a custom library for JSON encoding, you can directly encode the keys of :attr:`parameters` - note that string valued keys should not be JSON encoded. """ return json.dumps(self.json_parameters).encode("utf-8") @property def multipart_data(self) -> UploadFileDict: """Gives the files contained in this object as mapping of part name to encoded content.""" multipart_data: UploadFileDict = {} for param in self._parameters: m_data = param.multipart_data if m_data: multipart_data.update(m_data) return multipart_data python-telegram-bot-21.1.1/telegram/request/_requestparameter.py000066400000000000000000000163601460724040100250730ustar00rootroot00000000000000#!/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 describes a single parameter of a request to the Bot API.""" import json from dataclasses import dataclass from datetime import datetime from typing import List, Optional, Sequence, Tuple, final from telegram._files.inputfile import InputFile from telegram._files.inputmedia import InputMedia from telegram._files.inputsticker import InputSticker from telegram._telegramobject import TelegramObject from telegram._utils.datetime import to_timestamp from telegram._utils.enum import StringEnum from telegram._utils.types import UploadFileDict @final @dataclass(repr=True, eq=False, order=False, frozen=True) class RequestParameter: """Instances of this class represent a single parameter to be sent along with a request to the Bot API. .. versionadded:: 20.0 Warning: This class intended is to be used internally by the library and *not* by the user. Changes to this class are not considered breaking changes and may not be documented in the changelog. Args: name (:obj:`str`): The name of the parameter. value (:obj:`object` | :obj:`None`): The value of the parameter. Must be JSON-dumpable. input_files (List[:class:`telegram.InputFile`], optional): A list of files that should be uploaded along with this parameter. Attributes: name (:obj:`str`): The name of the parameter. value (:obj:`object` | :obj:`None`): The value of the parameter. input_files (List[:class:`telegram.InputFile` | :obj:`None`): A list of files that should be uploaded along with this parameter. """ __slots__ = ("input_files", "name", "value") name: str value: object input_files: Optional[List[InputFile]] @property def json_value(self) -> Optional[str]: """The JSON dumped :attr:`value` or :obj:`None` if :attr:`value` is :obj:`None`. The latter can currently only happen if :attr:`input_files` has exactly one element that must not be uploaded via an attach:// URI. """ if isinstance(self.value, str): return self.value if self.value is None: return None return json.dumps(self.value) @property def multipart_data(self) -> Optional[UploadFileDict]: """A dict with the file data to upload, if any.""" if not self.input_files: return None return { (input_file.attach_name or self.name): input_file.field_tuple for input_file in self.input_files } @staticmethod def _value_and_input_files_from_input( # pylint: disable=too-many-return-statements value: object, ) -> Tuple[object, List[InputFile]]: """Converts `value` into something that we can json-dump. Returns two values: 1. the JSON-dumpable value. Maybe be `None` in case the value is an InputFile which must not be uploaded via an attach:// URI 2. A list of InputFiles that should be uploaded for this value Note that we handle files differently depending on whether attaching them via an URI of the form attach:// is documented to be allowed or not. There was some confusion whether this worked for all files, so that we stick to the documented ways for now. See https://github.com/tdlib/telegram-bot-api/issues/167 and https://github.com/tdlib/telegram-bot-api/issues/259 This method only does some special casing for our own helper class StringEnum, but not for general enums. This is because: * tg.constants currently only uses IntEnum as second enum type and json dumping that is no problem * if a user passes a custom enum, it's unlikely that we can actually properly handle it even with some special casing. """ if isinstance(value, datetime): return to_timestamp(value), [] if isinstance(value, StringEnum): return value.value, [] if isinstance(value, InputFile): if value.attach_uri: return value.attach_uri, [value] return None, [value] if isinstance(value, InputMedia) and isinstance(value.media, InputFile): # We call to_dict and change the returned dict instead of overriding # value.media in case the same value is reused for another request data = value.to_dict() if value.media.attach_uri: data["media"] = value.media.attach_uri else: data.pop("media", None) thumbnail = data.get("thumbnail", None) if isinstance(thumbnail, InputFile): if thumbnail.attach_uri: data["thumbnail"] = thumbnail.attach_uri else: data.pop("thumbnail", None) return data, [value.media, thumbnail] return data, [value.media] if isinstance(value, InputSticker) and isinstance(value.sticker, InputFile): # We call to_dict and change the returned dict instead of overriding # value.sticker in case the same value is reused for another request data = value.to_dict() data["sticker"] = value.sticker.attach_uri return data, [value.sticker] if isinstance(value, TelegramObject): # Needs to be last, because InputMedia is a subclass of TelegramObject return value.to_dict(), [] return value, [] @classmethod def from_input(cls, key: str, value: object) -> "RequestParameter": """Builds an instance of this class for a given key-value pair that represents the raw input as passed along from a method of :class:`telegram.Bot`. """ if not isinstance(value, (str, bytes)) and isinstance(value, Sequence): param_values = [] input_files = [] for obj in value: param_value, input_file = cls._value_and_input_files_from_input(obj) if param_value is not None: param_values.append(param_value) input_files.extend(input_file) return RequestParameter( name=key, value=param_values, input_files=input_files if input_files else None ) param_value, input_files = cls._value_and_input_files_from_input(value) return RequestParameter( name=key, value=param_value, input_files=input_files if input_files else None ) python-telegram-bot-21.1.1/telegram/warnings.py000066400000000000000000000035531460724040100215030ustar00rootroot00000000000000#! /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. """ __slots__ = () python-telegram-bot-21.1.1/tests/000077500000000000000000000000001460724040100166355ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/README.rst000066400000000000000000000107351460724040100203320ustar00rootroot00000000000000============== 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 ``requirements-dev.txt`` 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 python-telegram-bot-21.1.1/tests/__init__.py000066400000000000000000000000001460724040100207340ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/_files/000077500000000000000000000000001460724040100200765ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/_files/__init__.py000066400000000000000000000014661460724040100222160ustar00rootroot00000000000000#!/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/]. python-telegram-bot-21.1.1/tests/_files/test_animation.py000066400000000000000000000375461460724040100235050ustar00rootroot00000000000000#!/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 os from pathlib import Path import pytest from telegram import Animation, Bot, InputFile, MessageEntity, PhotoSize, ReplyParameters, Voice from telegram.constants import ParseMode from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData 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.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture() def animation_file(): with data_file("game.gif").open("rb") as f: yield f @pytest.fixture(scope="module") async def animation(bot, chat_id): with data_file("game.gif").open("rb") as f, data_file("thumb.jpg").open("rb") as thumb: return ( await bot.send_animation(chat_id, animation=f, read_timeout=50, thumbnail=thumb) ).animation class TestAnimationBase: animation_file_id = "CgADAQADngIAAuyVeEez0xRovKi9VAI" animation_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" width = 320 height = 180 duration = 1 # animation_file_url = 'https://python-telegram-bot.org/static/testfiles/game.gif' # Shortened link, the above one is cached with the wrong duration. animation_file_url = "http://bit.ly/2L18jua" file_name = "game.gif.webm" mime_type = "video/mp4" file_size = 5859 caption = "Test *animation*" class TestAnimationWithoutRequest(TestAnimationBase): def test_slot_behaviour(self, animation): for attr in animation.__slots__: assert getattr(animation, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(animation)) == len(set(mro_slots(animation))), "duplicate slot" def test_creation(self, animation): assert isinstance(animation, Animation) assert isinstance(animation.file_id, str) assert isinstance(animation.file_unique_id, str) assert animation.file_id assert animation.file_unique_id def test_expected_values(self, animation): assert animation.mime_type == self.mime_type assert animation.file_name.startswith("game.gif") == self.file_name.startswith("game.gif") assert isinstance(animation.thumbnail, PhotoSize) def test_de_json(self, bot, animation): json_dict = { "file_id": self.animation_file_id, "file_unique_id": self.animation_file_unique_id, "width": self.width, "height": self.height, "duration": self.duration, "thumbnail": animation.thumbnail.to_dict(), "file_name": self.file_name, "mime_type": self.mime_type, "file_size": self.file_size, } animation = Animation.de_json(json_dict, bot) assert animation.api_kwargs == {} assert animation.file_id == self.animation_file_id assert animation.file_unique_id == self.animation_file_unique_id assert animation.file_name == self.file_name assert animation.mime_type == self.mime_type assert animation.file_size == self.file_size def test_to_dict(self, animation): animation_dict = animation.to_dict() assert isinstance(animation_dict, dict) assert animation_dict["file_id"] == animation.file_id assert animation_dict["file_unique_id"] == animation.file_unique_id assert animation_dict["width"] == animation.width assert animation_dict["height"] == animation.height assert animation_dict["duration"] == animation.duration assert animation_dict["thumbnail"] == animation.thumbnail.to_dict() assert animation_dict["file_name"] == animation.file_name assert animation_dict["mime_type"] == animation.mime_type assert animation_dict["file_size"] == animation.file_size def test_equality(self): a = Animation( self.animation_file_id, self.animation_file_unique_id, self.height, self.width, self.duration, ) b = Animation("", self.animation_file_unique_id, self.height, self.width, self.duration) d = Animation("", "", 0, 0, 0) e = Voice(self.animation_file_id, self.animation_file_unique_id, 0) assert a == b assert hash(a) == hash(b) assert a is not b assert a != d assert hash(a) != hash(d) assert a != e assert hash(a) != hash(e) async def test_send_animation_custom_filename(self, bot, chat_id, animation_file, monkeypatch): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return next(iter(request_data.multipart_data.values()))[0] == "custom_filename" monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_animation(chat_id, animation_file, filename="custom_filename") @pytest.mark.parametrize("local_mode", [True, False]) async def test_send_animation_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 test_flag = False file = data_file("telegram.jpg") expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag if local_mode: test_flag = ( data.get("animation") == expected and data.get("thumbnail") == expected ) else: test_flag = isinstance(data.get("animation"), InputFile) and isinstance( data.get("thumbnail"), InputFile ) monkeypatch.setattr(bot, "_post", make_assertion) await bot.send_animation(chat_id, file, thumbnail=file) assert test_flag finally: bot._local_mode = False async def test_send_with_animation(self, monkeypatch, bot, chat_id, animation): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.json_parameters["animation"] == animation.file_id monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_animation(animation=animation, chat_id=chat_id) async def test_get_file_instance_method(self, monkeypatch, animation): async def make_assertion(*_, **kwargs): return kwargs["file_id"] == animation.file_id assert check_shortcut_signature(Animation.get_file, Bot.get_file, ["file_id"], []) assert await check_shortcut_call(animation.get_file, animation.get_bot(), "get_file") assert await check_defaults_handling(animation.get_file, animation.get_bot()) monkeypatch.setattr(animation.get_bot(), "get_file", make_assertion) assert await animation.get_file() @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_animation_default_quote_parse_mode( self, default_bot, chat_id, animation, 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_animation( chat_id, animation, reply_parameters=ReplyParameters(**kwargs) ) class TestAnimationWithRequest(TestAnimationBase): async def test_send_all_args(self, bot, chat_id, animation_file, animation, thumb_file): message = await bot.send_animation( chat_id, animation_file, duration=self.duration, width=self.width, height=self.height, caption=self.caption, parse_mode="Markdown", disable_notification=False, protect_content=True, thumbnail=thumb_file, has_spoiler=True, ) assert isinstance(message.animation, Animation) assert isinstance(message.animation.file_id, str) assert isinstance(message.animation.file_unique_id, str) assert message.animation.file_id assert message.animation.file_unique_id assert message.animation.file_name == animation.file_name assert message.animation.mime_type == animation.mime_type assert message.animation.file_size == animation.file_size assert message.animation.thumbnail.width == self.width assert message.animation.thumbnail.height == self.height assert message.has_protected_content try: assert message.has_media_spoiler except AssertionError: pytest.xfail("This is a bug on Telegram's end") async def test_get_and_download(self, bot, animation, tmp_file): new_file = await bot.get_file(animation.file_id) assert new_file.file_path.startswith("https://") new_filepath = await new_file.download_to_drive(tmp_file) assert new_filepath.is_file() async def test_send_animation_url_file(self, bot, chat_id, animation): message = await bot.send_animation( chat_id=chat_id, animation=self.animation_file_url, caption=self.caption ) assert message.caption == self.caption assert isinstance(message.animation, Animation) assert isinstance(message.animation.file_id, str) assert isinstance(message.animation.file_unique_id, str) assert message.animation.file_id assert message.animation.file_unique_id assert message.animation.duration == animation.duration assert message.animation.file_name.startswith( "game.gif" ) == animation.file_name.startswith("game.gif") assert message.animation.mime_type == animation.mime_type async def test_send_animation_caption_entities(self, bot, chat_id, animation): 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_animation( chat_id, animation, caption=test_string, caption_entities=entities ) assert message.caption == test_string assert message.caption_entities == tuple(entities) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_animation_default_parse_mode_1(self, default_bot, chat_id, animation_file): test_string = "Italic Bold Code" test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_animation( chat_id, animation_file, caption=test_markdown_string ) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_animation_default_parse_mode_2(self, default_bot, chat_id, animation_file): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_animation( chat_id, animation_file, caption=test_markdown_string, parse_mode=None ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_animation_default_parse_mode_3(self, default_bot, chat_id, animation_file): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_animation( chat_id, animation_file, caption=test_markdown_string, parse_mode="HTML" ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @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_animation_default_allow_sending_without_reply( self, default_bot, chat_id, animation, 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_animation( chat_id, animation, 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_animation( chat_id, animation, reply_to_message_id=reply_to_message.message_id ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_animation( chat_id, animation, reply_to_message_id=reply_to_message.message_id ) @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_animation_default_protect_content(self, default_bot, chat_id, animation): tasks = asyncio.gather( default_bot.send_animation(chat_id, animation), default_bot.send_animation(chat_id, animation, protect_content=False), ) anim_protected, anim_unprotected = await tasks assert anim_protected.has_protected_content assert not anim_unprotected.has_protected_content async def test_resend(self, bot, chat_id, animation): message = await bot.send_animation(chat_id, animation.file_id) assert message.animation == animation async def test_error_send_empty_file(self, bot, chat_id): with Path(os.devnull).open("rb") as animation_file, pytest.raises(TelegramError): await bot.send_animation(chat_id=chat_id, animation=animation_file) async def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): await bot.send_animation(chat_id=chat_id, animation="") async def test_error_send_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): await bot.send_animation(chat_id=chat_id) python-telegram-bot-21.1.1/tests/_files/test_audio.py000066400000000000000000000372131460724040100226160ustar00rootroot00000000000000#!/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 os from pathlib import Path import pytest from telegram import Audio, Bot, InputFile, MessageEntity, ReplyParameters, Voice from telegram.constants import ParseMode from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData 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.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture() def audio_file(): with data_file("telegram.mp3").open("rb") as f: yield f @pytest.fixture(scope="module") async def audio(bot, chat_id): with data_file("telegram.mp3").open("rb") as f, data_file("thumb.jpg").open("rb") as thumb: return (await bot.send_audio(chat_id, audio=f, read_timeout=50, thumbnail=thumb)).audio class TestAudioBase: caption = "Test *audio*" performer = "Leandro Toledo" title = "Teste" file_name = "telegram.mp3" duration = 3 # audio_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.mp3' # Shortened link, the above one is cached with the wrong duration. audio_file_url = "https://goo.gl/3En24v" mime_type = "audio/mpeg" file_size = 122920 thumb_file_size = 1427 thumb_width = 50 thumb_height = 50 audio_file_id = "5a3128a4d2a04750b5b58397f3b5e812" audio_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" class TestAudioWithoutRequest(TestAudioBase): def test_slot_behaviour(self, audio): for attr in audio.__slots__: assert getattr(audio, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(audio)) == len(set(mro_slots(audio))), "duplicate slot" def test_creation(self, audio): # Make sure file has been uploaded. assert isinstance(audio, Audio) assert isinstance(audio.file_id, str) assert isinstance(audio.file_unique_id, str) assert audio.file_id assert audio.file_unique_id def test_expected_values(self, audio): assert audio.duration == self.duration assert audio.performer is None assert audio.title is None assert audio.mime_type == self.mime_type assert audio.file_size == self.file_size assert audio.thumbnail.file_size == self.thumb_file_size assert audio.thumbnail.width == self.thumb_width assert audio.thumbnail.height == self.thumb_height def test_de_json(self, bot, audio): json_dict = { "file_id": self.audio_file_id, "file_unique_id": self.audio_file_unique_id, "duration": self.duration, "performer": self.performer, "title": self.title, "file_name": self.file_name, "mime_type": self.mime_type, "file_size": self.file_size, "thumbnail": audio.thumbnail.to_dict(), } json_audio = Audio.de_json(json_dict, bot) assert json_audio.api_kwargs == {} assert json_audio.file_id == self.audio_file_id assert json_audio.file_unique_id == self.audio_file_unique_id assert json_audio.duration == self.duration assert json_audio.performer == self.performer assert json_audio.title == self.title assert json_audio.file_name == self.file_name assert json_audio.mime_type == self.mime_type assert json_audio.file_size == self.file_size assert json_audio.thumbnail == audio.thumbnail def test_to_dict(self, audio): audio_dict = audio.to_dict() assert isinstance(audio_dict, dict) assert audio_dict["file_id"] == audio.file_id assert audio_dict["file_unique_id"] == audio.file_unique_id assert audio_dict["duration"] == audio.duration assert audio_dict["mime_type"] == audio.mime_type assert audio_dict["file_size"] == audio.file_size assert audio_dict["file_name"] == audio.file_name def test_equality(self, audio): a = Audio(audio.file_id, audio.file_unique_id, audio.duration) b = Audio("", audio.file_unique_id, audio.duration) c = Audio(audio.file_id, audio.file_unique_id, 0) d = Audio("", "", audio.duration) e = Voice(audio.file_id, audio.file_unique_id, audio.duration) 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_send_with_audio(self, monkeypatch, bot, chat_id, audio): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.json_parameters["audio"] == audio.file_id monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_audio(audio=audio, chat_id=chat_id) async def test_send_audio_custom_filename(self, bot, chat_id, audio_file, monkeypatch): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return next(iter(request_data.multipart_data.values()))[0] == "custom_filename" monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_audio(chat_id, audio_file, filename="custom_filename") @pytest.mark.parametrize("local_mode", [True, False]) async def test_send_audio_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 test_flag = False file = data_file("telegram.jpg") expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag if local_mode: test_flag = data.get("audio") == expected and data.get("thumbnail") == expected else: test_flag = isinstance(data.get("audio"), InputFile) and isinstance( data.get("thumbnail"), InputFile ) monkeypatch.setattr(bot, "_post", make_assertion) await bot.send_audio(chat_id, file, thumbnail=file) assert test_flag finally: bot._local_mode = False async def test_get_file_instance_method(self, monkeypatch, audio): async def make_assertion(*_, **kwargs): return kwargs["file_id"] == audio.file_id assert check_shortcut_signature(Audio.get_file, Bot.get_file, ["file_id"], []) assert await check_shortcut_call(audio.get_file, audio.get_bot(), "get_file") assert await check_defaults_handling(audio.get_file, audio.get_bot()) monkeypatch.setattr(audio._bot, "get_file", make_assertion) assert await audio.get_file() @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_audio_default_quote_parse_mode( self, default_bot, chat_id, audio, 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_audio(chat_id, audio, reply_parameters=ReplyParameters(**kwargs)) class TestAudioWithRequest(TestAudioBase): async def test_send_all_args(self, bot, chat_id, audio_file, thumb_file): message = await bot.send_audio( chat_id, audio=audio_file, caption=self.caption, duration=self.duration, performer=self.performer, title=self.title, disable_notification=False, protect_content=True, parse_mode="Markdown", thumbnail=thumb_file, ) assert message.caption == self.caption.replace("*", "") assert isinstance(message.audio, Audio) assert isinstance(message.audio.file_id, str) assert isinstance(message.audio.file_unique_id, str) assert message.audio.file_unique_id is not None assert message.audio.file_id is not None assert message.audio.duration == self.duration assert message.audio.performer == self.performer assert message.audio.title == self.title assert message.audio.file_name == self.file_name assert message.audio.mime_type == self.mime_type assert message.audio.file_size == self.file_size assert message.audio.thumbnail.file_size == self.thumb_file_size assert message.audio.thumbnail.width == self.thumb_width assert message.audio.thumbnail.height == self.thumb_height assert message.has_protected_content async def test_get_and_download(self, bot, chat_id, audio, tmp_file): new_file = await bot.get_file(audio.file_id) assert new_file.file_size == self.file_size assert new_file.file_unique_id == audio.file_unique_id assert str(new_file.file_path).startswith("https://") await new_file.download_to_drive(tmp_file) assert tmp_file.is_file() async def test_send_mp3_url_file(self, bot, chat_id, audio): message = await bot.send_audio( chat_id=chat_id, audio=self.audio_file_url, caption=self.caption ) assert message.caption == self.caption assert isinstance(message.audio, Audio) assert isinstance(message.audio.file_id, str) assert isinstance(message.audio.file_unique_id, str) assert message.audio.file_unique_id is not None assert message.audio.file_id is not None assert message.audio.duration == audio.duration assert message.audio.mime_type == audio.mime_type assert message.audio.file_size == audio.file_size async def test_send_audio_caption_entities(self, bot, chat_id, audio): 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_audio( chat_id, audio, caption=test_string, caption_entities=entities ) assert message.caption == test_string assert message.caption_entities == tuple(entities) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_audio_default_parse_mode_1(self, default_bot, chat_id, audio_file): test_string = "Italic Bold Code" test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_audio(chat_id, audio_file, caption=test_markdown_string) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_audio_default_parse_mode_2(self, default_bot, chat_id, audio_file): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_audio( chat_id, audio_file, caption=test_markdown_string, parse_mode=None ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_audio_default_parse_mode_3(self, default_bot, chat_id, audio_file): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_audio( chat_id, audio_file, caption=test_markdown_string, parse_mode="HTML" ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_audio_default_protect_content(self, default_bot, chat_id, audio): tasks = asyncio.gather( default_bot.send_audio(chat_id, audio), default_bot.send_audio(chat_id, audio, protect_content=False), ) protected, unprotected = await tasks assert protected.has_protected_content assert not unprotected.has_protected_content async def test_resend(self, bot, chat_id, audio): message = await bot.send_audio(chat_id=chat_id, audio=audio.file_id) assert message.audio == audio async def test_error_send_empty_file(self, bot, chat_id): with Path(os.devnull).open("rb") as audio_file, pytest.raises(TelegramError): await bot.send_audio(chat_id=chat_id, audio=audio_file) async def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): await bot.send_audio(chat_id=chat_id, audio="") async def test_error_send_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): await bot.send_audio(chat_id=chat_id) @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_audio_default_allow_sending_without_reply( self, default_bot, chat_id, audio, 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_audio( chat_id, audio, 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_audio( chat_id, audio, reply_to_message_id=reply_to_message.message_id ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_audio( chat_id, audio, reply_to_message_id=reply_to_message.message_id ) python-telegram-bot-21.1.1/tests/_files/test_chatphoto.py000066400000000000000000000173111460724040100235030ustar00rootroot00000000000000#!/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 os from pathlib import Path import pytest from telegram import Bot, ChatPhoto, Voice from telegram.error import TelegramError from telegram.request import RequestData from tests.auxil.bot_method_checks import ( check_defaults_handling, check_shortcut_call, check_shortcut_signature, ) from tests.auxil.files import data_file from tests.auxil.networking import expect_bad_request from tests.auxil.slots import mro_slots @pytest.fixture() def chatphoto_file(): with data_file("telegram.jpg").open("rb") as f: yield f @pytest.fixture(scope="module") async def chat_photo(bot, super_group_id): async def func(): return (await bot.get_chat(super_group_id, read_timeout=50)).photo return await expect_bad_request( func, "Type of file mismatch", "Telegram did not accept the file." ) class TestChatPhotoBase: chatphoto_small_file_id = "smallCgADAQADngIAAuyVeEez0xRovKi9VAI" chatphoto_big_file_id = "bigCgADAQADngIAAuyVeEez0xRovKi9VAI" chatphoto_small_file_unique_id = "smalladc3145fd2e84d95b64d68eaa22aa33e" chatphoto_big_file_unique_id = "bigadc3145fd2e84d95b64d68eaa22aa33e" chatphoto_file_url = "https://python-telegram-bot.org/static/testfiles/telegram.jpg" class TestChatPhotoWithoutRequest(TestChatPhotoBase): def test_slot_behaviour(self, chat_photo): for attr in chat_photo.__slots__: assert getattr(chat_photo, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(chat_photo)) == len(set(mro_slots(chat_photo))), "duplicate slot" def test_de_json(self, bot, chat_photo): json_dict = { "small_file_id": self.chatphoto_small_file_id, "big_file_id": self.chatphoto_big_file_id, "small_file_unique_id": self.chatphoto_small_file_unique_id, "big_file_unique_id": self.chatphoto_big_file_unique_id, } chat_photo = ChatPhoto.de_json(json_dict, bot) assert chat_photo.api_kwargs == {} assert chat_photo.small_file_id == self.chatphoto_small_file_id assert chat_photo.big_file_id == self.chatphoto_big_file_id assert chat_photo.small_file_unique_id == self.chatphoto_small_file_unique_id assert chat_photo.big_file_unique_id == self.chatphoto_big_file_unique_id async def test_to_dict(self, chat_photo): chat_photo_dict = chat_photo.to_dict() assert isinstance(chat_photo_dict, dict) assert chat_photo_dict["small_file_id"] == chat_photo.small_file_id assert chat_photo_dict["big_file_id"] == chat_photo.big_file_id assert chat_photo_dict["small_file_unique_id"] == chat_photo.small_file_unique_id assert chat_photo_dict["big_file_unique_id"] == chat_photo.big_file_unique_id def test_equality(self): a = ChatPhoto( self.chatphoto_small_file_id, self.chatphoto_big_file_id, self.chatphoto_small_file_unique_id, self.chatphoto_big_file_unique_id, ) b = ChatPhoto( self.chatphoto_small_file_id, self.chatphoto_big_file_id, self.chatphoto_small_file_unique_id, self.chatphoto_big_file_unique_id, ) c = ChatPhoto( "", "", self.chatphoto_small_file_unique_id, self.chatphoto_big_file_unique_id ) d = ChatPhoto("", "", 0, 0) e = Voice(self.chatphoto_small_file_id, self.chatphoto_small_file_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) async def test_send_with_chat_photo(self, monkeypatch, bot, super_group_id, chat_photo): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.parameters["photo"] == chat_photo.to_dict() monkeypatch.setattr(bot.request, "post", make_assertion) message = await bot.set_chat_photo(photo=chat_photo, chat_id=super_group_id) assert message async def test_get_small_file_instance_method(self, monkeypatch, chat_photo): async def make_assertion(*_, **kwargs): return kwargs["file_id"] == chat_photo.small_file_id assert check_shortcut_signature(ChatPhoto.get_small_file, Bot.get_file, ["file_id"], []) assert await check_shortcut_call( chat_photo.get_small_file, chat_photo.get_bot(), "get_file" ) assert await check_defaults_handling(chat_photo.get_small_file, chat_photo.get_bot()) monkeypatch.setattr(chat_photo.get_bot(), "get_file", make_assertion) assert await chat_photo.get_small_file() async def test_get_big_file_instance_method(self, monkeypatch, chat_photo): async def make_assertion(*_, **kwargs): return kwargs["file_id"] == chat_photo.big_file_id assert check_shortcut_signature(ChatPhoto.get_big_file, Bot.get_file, ["file_id"], []) assert await check_shortcut_call(chat_photo.get_big_file, chat_photo.get_bot(), "get_file") assert await check_defaults_handling(chat_photo.get_big_file, chat_photo.get_bot()) monkeypatch.setattr(chat_photo.get_bot(), "get_file", make_assertion) assert await chat_photo.get_big_file() class TestChatPhotoWithRequest: async def test_get_and_download(self, bot, chat_photo, tmp_file): tasks = {bot.get_file(chat_photo.small_file_id), bot.get_file(chat_photo.big_file_id)} asserts = [] for task in asyncio.as_completed(tasks): file = await task if file.file_unique_id == chat_photo.small_file_unique_id: asserts.append("small") elif file.file_unique_id == chat_photo.big_file_unique_id: asserts.append("big") assert file.file_path.startswith("https://") await file.download_to_drive(tmp_file) assert tmp_file.is_file() assert "small" in asserts assert "big" in asserts async def test_send_all_args(self, bot, super_group_id, chatphoto_file): async def func(): assert await bot.set_chat_photo(super_group_id, chatphoto_file) await expect_bad_request( func, "Type of file mismatch", "Telegram did not accept the file." ) async def test_error_send_empty_file(self, bot, super_group_id): with Path(os.devnull).open("rb") as chatphoto_file, pytest.raises(TelegramError): await bot.set_chat_photo(chat_id=super_group_id, photo=chatphoto_file) async def test_error_send_empty_file_id(self, bot, super_group_id): with pytest.raises(TelegramError): await bot.set_chat_photo(chat_id=super_group_id, photo="") async def test_error_send_without_required_args(self, bot, super_group_id): with pytest.raises(TypeError): await bot.set_chat_photo(chat_id=super_group_id) python-telegram-bot-21.1.1/tests/_files/test_contact.py000066400000000000000000000170311460724040100231440ustar00rootroot00000000000000#!/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 pytest from telegram import Contact, ReplyParameters, Voice from telegram.constants import ParseMode from telegram.error import BadRequest from telegram.request import RequestData from tests.auxil.build_messages import make_message from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def contact(): return Contact( TestContactBase.phone_number, TestContactBase.first_name, TestContactBase.last_name, TestContactBase.user_id, ) class TestContactBase: phone_number = "+11234567890" first_name = "Leandro" last_name = "Toledo" user_id = 23 class TestContactWithoutRequest(TestContactBase): def test_slot_behaviour(self, contact): for attr in contact.__slots__: assert getattr(contact, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(contact)) == len(set(mro_slots(contact))), "duplicate slot" def test_de_json_required(self, bot): json_dict = {"phone_number": self.phone_number, "first_name": self.first_name} contact = Contact.de_json(json_dict, bot) assert contact.api_kwargs == {} assert contact.phone_number == self.phone_number assert contact.first_name == self.first_name def test_de_json_all(self, bot): json_dict = { "phone_number": self.phone_number, "first_name": self.first_name, "last_name": self.last_name, "user_id": self.user_id, } contact = Contact.de_json(json_dict, bot) assert contact.api_kwargs == {} assert contact.phone_number == self.phone_number assert contact.first_name == self.first_name assert contact.last_name == self.last_name assert contact.user_id == self.user_id def test_to_dict(self, contact): contact_dict = contact.to_dict() assert isinstance(contact_dict, dict) assert contact_dict["phone_number"] == contact.phone_number assert contact_dict["first_name"] == contact.first_name assert contact_dict["last_name"] == contact.last_name assert contact_dict["user_id"] == contact.user_id def test_equality(self): a = Contact(self.phone_number, self.first_name) b = Contact(self.phone_number, self.first_name) c = Contact(self.phone_number, "") d = Contact("", self.first_name) e = Voice("", "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) async def test_send_contact_without_required(self, bot, chat_id): with pytest.raises(ValueError, match="Either contact or phone_number and first_name"): await bot.send_contact(chat_id=chat_id) async def test_send_mutually_exclusive(self, bot, chat_id, contact): with pytest.raises(ValueError, match="Not both"): await bot.send_contact( chat_id=chat_id, contact=contact, phone_number=contact.phone_number, first_name=contact.first_name, ) async def test_send_with_contact(self, monkeypatch, bot, chat_id, contact): async def make_assertion(url, request_data: RequestData, *args, **kwargs): data = request_data.json_parameters phone = data["phone_number"] == contact.phone_number first = data["first_name"] == contact.first_name last = data["last_name"] == contact.last_name return phone and first and last monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_contact(contact=contact, chat_id=chat_id) @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_contact_default_quote_parse_mode( self, default_bot, chat_id, contact, 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_contact( chat_id, contact=contact, reply_parameters=ReplyParameters(**kwargs) ) class TestContactWithRequest(TestContactBase): @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_contact_default_allow_sending_without_reply( self, default_bot, chat_id, contact, 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_contact( chat_id, contact=contact, 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_contact( chat_id, contact=contact, reply_to_message_id=reply_to_message.message_id ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_contact( chat_id, contact=contact, reply_to_message_id=reply_to_message.message_id ) @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_contact_default_protect_content(self, chat_id, default_bot, contact): tasks = asyncio.gather( default_bot.send_contact(chat_id, contact=contact), default_bot.send_contact(chat_id, contact=contact, protect_content=False), ) protected, unprotected = await tasks assert protected.has_protected_content assert not unprotected.has_protected_content python-telegram-bot-21.1.1/tests/_files/test_document.py000066400000000000000000000360411460724040100233310ustar00rootroot00000000000000#!/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 os from pathlib import Path import pytest from telegram import Bot, Document, InputFile, MessageEntity, PhotoSize, ReplyParameters, Voice from telegram.constants import ParseMode from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData 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.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture() def document_file(): with data_file("telegram.png").open("rb") as f: yield f @pytest.fixture(scope="module") async def document(bot, chat_id): with data_file("telegram.png").open("rb") as f: return (await bot.send_document(chat_id, document=f, read_timeout=50)).document class TestDocumentBase: caption = "DocumentTest - *Caption*" document_file_url = "https://python-telegram-bot.org/static/testfiles/telegram.gif" file_size = 12948 mime_type = "image/png" file_name = "telegram.png" thumb_file_size = 8090 thumb_width = 300 thumb_height = 300 document_file_id = "5a3128a4d2a04750b5b58397f3b5e812" document_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" class TestDocumentWithoutRequest(TestDocumentBase): def test_slot_behaviour(self, document): for attr in document.__slots__: assert getattr(document, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(document)) == len(set(mro_slots(document))), "duplicate slot" def test_creation(self, document): assert isinstance(document, Document) assert isinstance(document.file_id, str) assert isinstance(document.file_unique_id, str) assert document.file_id assert document.file_unique_id def test_expected_values(self, document): assert document.file_size == self.file_size assert document.mime_type == self.mime_type assert document.file_name == self.file_name assert document.thumbnail.file_size == self.thumb_file_size assert document.thumbnail.width == self.thumb_width assert document.thumbnail.height == self.thumb_height def test_de_json(self, bot, document): json_dict = { "file_id": self.document_file_id, "file_unique_id": self.document_file_unique_id, "thumbnail": document.thumbnail.to_dict(), "file_name": self.file_name, "mime_type": self.mime_type, "file_size": self.file_size, } test_document = Document.de_json(json_dict, bot) assert test_document.api_kwargs == {} assert test_document.file_id == self.document_file_id assert test_document.file_unique_id == self.document_file_unique_id assert test_document.thumbnail == document.thumbnail assert test_document.file_name == self.file_name assert test_document.mime_type == self.mime_type assert test_document.file_size == self.file_size def test_to_dict(self, document): document_dict = document.to_dict() assert isinstance(document_dict, dict) assert document_dict["file_id"] == document.file_id assert document_dict["file_unique_id"] == document.file_unique_id assert document_dict["file_name"] == document.file_name assert document_dict["mime_type"] == document.mime_type assert document_dict["file_size"] == document.file_size def test_equality(self, document): a = Document(document.file_id, document.file_unique_id) b = Document("", document.file_unique_id) d = Document("", "") e = Voice(document.file_id, document.file_unique_id, 0) assert a == b assert hash(a) == hash(b) assert a is not b assert a != d assert hash(a) != hash(d) assert a != e assert hash(a) != hash(e) async def test_error_send_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): await bot.send_document(chat_id=chat_id) @pytest.mark.parametrize("disable_content_type_detection", [True, False, None]) async def test_send_with_document( self, monkeypatch, bot, chat_id, document, disable_content_type_detection ): async def make_assertion(url, request_data: RequestData, *args, **kwargs): data = request_data.parameters type_detection = ( data.get("disable_content_type_detection") == disable_content_type_detection ) return data["document"] == document.file_id and type_detection monkeypatch.setattr(bot.request, "post", make_assertion) message = await bot.send_document( document=document, chat_id=chat_id, disable_content_type_detection=disable_content_type_detection, ) assert message @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_document_default_quote_parse_mode( self, default_bot, chat_id, document, 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_document( chat_id, document, reply_parameters=ReplyParameters(**kwargs) ) @pytest.mark.parametrize("local_mode", [True, False]) async def test_send_document_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 test_flag = False file = data_file("telegram.jpg") expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag if local_mode: test_flag = ( data.get("document") == expected and data.get("thumbnail") == expected ) else: test_flag = isinstance(data.get("document"), InputFile) and isinstance( data.get("thumbnail"), InputFile ) monkeypatch.setattr(bot, "_post", make_assertion) await bot.send_document(chat_id, file, thumbnail=file) assert test_flag finally: bot._local_mode = False async def test_get_file_instance_method(self, monkeypatch, document): async def make_assertion(*_, **kwargs): return kwargs["file_id"] == document.file_id assert check_shortcut_signature(Document.get_file, Bot.get_file, ["file_id"], []) assert await check_shortcut_call(document.get_file, document.get_bot(), "get_file") assert await check_defaults_handling(document.get_file, document.get_bot()) monkeypatch.setattr(document.get_bot(), "get_file", make_assertion) assert await document.get_file() class TestDocumentWithRequest(TestDocumentBase): async def test_error_send_empty_file(self, bot, chat_id): with Path(os.devnull).open("rb") as f, pytest.raises(TelegramError): await bot.send_document(chat_id=chat_id, document=f) async def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): await bot.send_document(chat_id=chat_id, document="") async def test_get_and_download(self, bot, document, chat_id, tmp_file): new_file = await bot.get_file(document.file_id) assert new_file.file_size == document.file_size assert new_file.file_unique_id == document.file_unique_id assert new_file.file_path.startswith("https://") await new_file.download_to_drive(tmp_file) assert tmp_file.is_file() async def test_send_resend(self, bot, chat_id, document): message = await bot.send_document(chat_id=chat_id, document=document.file_id) assert message.document == document async def test_send_all_args(self, bot, chat_id, document_file, document, thumb_file): message = await bot.send_document( chat_id, document=document_file, caption=self.caption, disable_notification=False, protect_content=True, filename="telegram_custom.png", parse_mode="Markdown", thumbnail=thumb_file, ) assert isinstance(message.document, Document) assert isinstance(message.document.file_id, str) assert message.document.file_id assert isinstance(message.document.file_unique_id, str) assert message.document.file_unique_id assert isinstance(message.document.thumbnail, PhotoSize) assert message.document.file_name == "telegram_custom.png" assert message.document.mime_type == document.mime_type assert message.document.file_size == document.file_size assert message.caption == self.caption.replace("*", "") assert message.document.thumbnail.width == self.thumb_width assert message.document.thumbnail.height == self.thumb_height assert message.has_protected_content async def test_send_url_gif_file(self, bot, chat_id): message = await bot.send_document(chat_id, self.document_file_url) document = message.document assert isinstance(document, Document) assert isinstance(document.file_id, str) assert document.file_id assert isinstance(message.document.file_unique_id, str) assert message.document.file_unique_id assert isinstance(document.thumbnail, PhotoSize) assert document.file_name == "telegram.gif" assert document.mime_type == "image/gif" assert document.file_size == 3878 @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_document_default_protect_content(self, chat_id, default_bot, document): tasks = asyncio.gather( default_bot.send_document(chat_id, document), default_bot.send_document(chat_id, document, protect_content=False), ) protected, unprotected = await tasks assert protected.has_protected_content assert not unprotected.has_protected_content async def test_send_document_caption_entities(self, bot, chat_id, document): 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_document( chat_id, document, caption=test_string, caption_entities=entities ) assert message.caption == test_string assert message.caption_entities == tuple(entities) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_document_default_parse_mode_1(self, default_bot, chat_id, document): test_string = "Italic Bold Code" test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_document(chat_id, document, caption=test_markdown_string) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_document_default_parse_mode_2(self, default_bot, chat_id, document): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_document( chat_id, document, caption=test_markdown_string, parse_mode=None ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_document_default_parse_mode_3(self, default_bot, chat_id, document): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_document( chat_id, document, caption=test_markdown_string, parse_mode="HTML" ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @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_document_default_allow_sending_without_reply( self, default_bot, chat_id, document, 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_document( chat_id, document, 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_document( chat_id, document, reply_to_message_id=reply_to_message.message_id ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_document( chat_id, document, reply_to_message_id=reply_to_message.message_id ) python-telegram-bot-21.1.1/tests/_files/test_file.py000066400000000000000000000317061460724040100224350ustar00rootroot00000000000000#!/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 from pathlib import Path from tempfile import TemporaryFile, mkstemp import pytest from telegram import File, FileCredentials, Voice from telegram.error import TelegramError from tests.auxil.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def file(bot): file = File( TestFileBase.file_id, TestFileBase.file_unique_id, file_path=TestFileBase.file_path, file_size=TestFileBase.file_size, ) file.set_bot(bot) file._unfreeze() return file @pytest.fixture(scope="module") def encrypted_file(bot): # check https://github.com/python-telegram-bot/python-telegram-bot/wiki/\ # PTB-test-writing-knowledge-base#how-to-generate-encrypted-passport-files # if you want to know the source of these values fc = FileCredentials( "Oq3G4sX+bKZthoyms1YlPqvWou9esb+z0Bi/KqQUG8s=", "Pt7fKPgYWKA/7a8E64Ea1X8C+Wf7Ky1tF4ANBl63vl4=", ) ef = File( TestFileBase.file_id, TestFileBase.file_unique_id, TestFileBase.file_size, TestFileBase.file_path, ) ef.set_bot(bot) ef.set_credentials(fc) return ef @pytest.fixture(scope="module") def encrypted_local_file(bot): # check encrypted_file() for the source of the fc values fc = FileCredentials( "Oq3G4sX+bKZthoyms1YlPqvWou9esb+z0Bi/KqQUG8s=", "Pt7fKPgYWKA/7a8E64Ea1X8C+Wf7Ky1tF4ANBl63vl4=", ) ef = File( TestFileBase.file_id, TestFileBase.file_unique_id, TestFileBase.file_size, file_path=str(data_file("image_encrypted.jpg")), ) ef.set_bot(bot) ef.set_credentials(fc) return ef @pytest.fixture(scope="module") def local_file(bot): file = File( TestFileBase.file_id, TestFileBase.file_unique_id, file_path=str(data_file("local_file.txt")), file_size=TestFileBase.file_size, ) file.set_bot(bot) return file class TestFileBase: file_id = "NOTVALIDDOESNOTMATTER" file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" file_path = ( "https://api.org/file/bot133505823:AAHZFMHno3mzVLErU5b5jJvaeG--qUyLyG0/document/file_3" ) file_size = 28232 file_content = "Saint-Saëns".encode() # Intentionally contains unicode chars. class TestFileWithoutRequest(TestFileBase): def test_slot_behaviour(self, file): for attr in file.__slots__: assert getattr(file, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(file)) == len(set(mro_slots(file))), "duplicate slot" def test_de_json(self, bot): json_dict = { "file_id": self.file_id, "file_unique_id": self.file_unique_id, "file_path": self.file_path, "file_size": self.file_size, } new_file = File.de_json(json_dict, bot) assert new_file.api_kwargs == {} assert new_file.file_id == self.file_id assert new_file.file_unique_id == self.file_unique_id assert new_file.file_path == self.file_path assert new_file.file_size == self.file_size def test_to_dict(self, file): file_dict = file.to_dict() assert isinstance(file_dict, dict) assert file_dict["file_id"] == file.file_id assert file_dict["file_unique_id"] == file.file_unique_id assert file_dict["file_path"] == file.file_path assert file_dict["file_size"] == file.file_size def test_equality(self, bot): a = File(self.file_id, self.file_unique_id, bot) b = File("", self.file_unique_id, bot) c = File(self.file_id, self.file_unique_id, None) d = File("", "", bot) e = Voice(self.file_id, self.file_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) async def test_download(self, monkeypatch, file): async def test(*args, **kwargs): return self.file_content monkeypatch.setattr(file.get_bot().request, "retrieve", test) out_file = await file.download_to_drive() try: assert out_file.read_bytes() == self.file_content finally: out_file.unlink(missing_ok=True) @pytest.mark.parametrize( "custom_path_type", [str, Path], ids=["str custom_path", "pathlib.Path custom_path"] ) async def test_download_custom_path(self, monkeypatch, file, custom_path_type): async def test(*args, **kwargs): return self.file_content monkeypatch.setattr(file.get_bot().request, "retrieve", test) file_handle, custom_path = mkstemp() custom_path = Path(custom_path) try: out_file = await file.download_to_drive(custom_path_type(custom_path)) assert out_file == custom_path assert out_file.read_bytes() == self.file_content finally: os.close(file_handle) custom_path.unlink(missing_ok=True) async def test_download_no_filename(self, monkeypatch, file): async def test(*args, **kwargs): return self.file_content file.file_path = None monkeypatch.setattr(file.get_bot().request, "retrieve", test) out_file = await file.download_to_drive() assert str(out_file)[-len(file.file_id) :] == file.file_id try: assert out_file.read_bytes() == self.file_content finally: out_file.unlink(missing_ok=True) async def test_download_file_obj(self, monkeypatch, file): async def test(*args, **kwargs): return self.file_content monkeypatch.setattr(file.get_bot().request, "retrieve", test) with TemporaryFile() as custom_fobj: await file.download_to_memory(out=custom_fobj) custom_fobj.seek(0) assert custom_fobj.read() == self.file_content async def test_download_bytearray(self, monkeypatch, file): async def test(*args, **kwargs): return self.file_content monkeypatch.setattr(file.get_bot().request, "retrieve", test) # Check that a download to a newly allocated bytearray works. buf = await file.download_as_bytearray() assert buf == bytearray(self.file_content) # Check that a download to a given bytearray works (extends the bytearray). buf2 = buf[:] buf3 = await file.download_as_bytearray(buf=buf2) assert buf3 is buf2 assert buf2[len(buf) :] == buf assert buf2[: len(buf)] == buf async def test_download_encrypted(self, monkeypatch, bot, encrypted_file): async def test(*args, **kwargs): return data_file("image_encrypted.jpg").read_bytes() monkeypatch.setattr(encrypted_file.get_bot().request, "retrieve", test) out_file = await encrypted_file.download_to_drive() try: assert out_file.read_bytes() == data_file("image_decrypted.jpg").read_bytes() finally: out_file.unlink(missing_ok=True) async def test_download_file_obj_encrypted(self, monkeypatch, encrypted_file): async def test(*args, **kwargs): return data_file("image_encrypted.jpg").read_bytes() monkeypatch.setattr(encrypted_file.get_bot().request, "retrieve", test) with TemporaryFile() as custom_fobj: await encrypted_file.download_to_memory(out=custom_fobj) custom_fobj.seek(0) assert custom_fobj.read() == data_file("image_decrypted.jpg").read_bytes() async def test_download_file_obj_local_file_encrypted(self, monkeypatch, encrypted_local_file): async def test(*args, **kwargs): return data_file("image_encrypted.jpg").read_bytes() monkeypatch.setattr(encrypted_local_file.get_bot().request, "retrieve", test) with TemporaryFile() as custom_fobj: await encrypted_local_file.download_to_memory(out=custom_fobj) custom_fobj.seek(0) assert custom_fobj.read() == data_file("image_decrypted.jpg").read_bytes() async def test_download_bytearray_encrypted(self, monkeypatch, encrypted_file): async def test(*args, **kwargs): return data_file("image_encrypted.jpg").read_bytes() monkeypatch.setattr(encrypted_file.get_bot().request, "retrieve", test) # Check that a download to a newly allocated bytearray works. buf = await encrypted_file.download_as_bytearray() assert buf == bytearray(data_file("image_decrypted.jpg").read_bytes()) # Check that a download to a given bytearray works (extends the bytearray). buf2 = buf[:] buf3 = await encrypted_file.download_as_bytearray(buf=buf2) assert buf3 is buf2 assert buf2[len(buf) :] == buf assert buf2[: len(buf)] == buf class TestFileWithRequest(TestFileBase): async def test_error_get_empty_file_id(self, bot): with pytest.raises(TelegramError): await bot.get_file(file_id="") async def test_download_local_file(self, local_file): assert await local_file.download_to_drive() == Path(local_file.file_path) # Ensure that the file contents didn't change assert Path(local_file.file_path).read_bytes() == self.file_content @pytest.mark.parametrize( "custom_path_type", [str, Path], ids=["str custom_path", "pathlib.Path custom_path"] ) async def test_download_custom_path_local_file(self, local_file, custom_path_type): file_handle, custom_path = mkstemp() custom_path = Path(custom_path) try: out_file = await local_file.download_to_drive(custom_path_type(custom_path)) assert out_file == custom_path assert out_file.read_bytes() == self.file_content finally: os.close(file_handle) custom_path.unlink(missing_ok=True) async def test_download_file_obj_local_file(self, local_file): with TemporaryFile() as custom_fobj: await local_file.download_to_memory(out=custom_fobj) custom_fobj.seek(0) assert custom_fobj.read() == self.file_content @pytest.mark.parametrize( "custom_path_type", [str, Path], ids=["str custom_path", "pathlib.Path custom_path"] ) async def test_download_custom_path_local_file_encrypted( self, encrypted_local_file, custom_path_type ): file_handle, custom_path = mkstemp() custom_path = Path(custom_path) try: out_file = await encrypted_local_file.download_to_drive(custom_path_type(custom_path)) assert out_file == custom_path assert out_file.read_bytes() == data_file("image_decrypted.jpg").read_bytes() finally: os.close(file_handle) custom_path.unlink(missing_ok=True) async def test_download_local_file_encrypted(self, encrypted_local_file): out_file = await encrypted_local_file.download_to_drive() try: assert out_file.read_bytes() == data_file("image_decrypted.jpg").read_bytes() finally: out_file.unlink(missing_ok=True) async def test_download_bytearray_local_file(self, local_file): # Check that a download to a newly allocated bytearray works. buf = await local_file.download_as_bytearray() assert buf == bytearray(self.file_content) # Check that a download to a given bytearray works (extends the bytearray). buf2 = buf[:] buf3 = await local_file.download_as_bytearray(buf=buf2) assert buf3 is buf2 assert buf2[len(buf) :] == buf assert buf2[: len(buf)] == buf async def test_download_bytearray_local_file_encrypted(self, encrypted_local_file): # Check that a download to a newly allocated bytearray works. buf = await encrypted_local_file.download_as_bytearray() assert buf == bytearray(data_file("image_decrypted.jpg").read_bytes()) # Check that a download to a given bytearray works (extends the bytearray). buf2 = buf[:] buf3 = await encrypted_local_file.download_as_bytearray(buf=buf2) assert buf3 is buf2 assert buf2[len(buf) :] == buf assert buf2[: len(buf)] == buf python-telegram-bot-21.1.1/tests/_files/test_inputfile.py000066400000000000000000000146131460724040100235130ustar00rootroot00000000000000#!/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 contextlib import subprocess import sys from io import BytesIO import pytest from telegram import InputFile from tests.auxil.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def png_file(): return data_file("game.png") class TestInputFileWithoutRequest: def test_slot_behaviour(self): inst = InputFile(BytesIO(b"blah"), filename="tg.jpg") 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_subprocess_pipe(self, png_file): cmd_str = "type" if sys.platform == "win32" else "cat" cmd = [cmd_str, str(png_file)] proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=(sys.platform == "win32")) in_file = InputFile(proc.stdout) assert in_file.input_file_content == png_file.read_bytes() assert in_file.mimetype == "application/octet-stream" assert in_file.filename == "application.octet-stream" with contextlib.suppress(ProcessLookupError): proc.kill() # This exception may be thrown if the process has finished before we had the chance # to kill it. @pytest.mark.parametrize("attach", [True, False]) def test_attach(self, attach): input_file = InputFile("contents", attach=attach) if attach: assert isinstance(input_file.attach_name, str) assert input_file.attach_uri == f"attach://{input_file.attach_name}" else: assert input_file.attach_name is None assert input_file.attach_uri is None def test_mimetypes(self): # Only test a few to make sure logic works okay assert InputFile(data_file("telegram.jpg").open("rb")).mimetype == "image/jpeg" # For some reason python can guess the type on macOS assert InputFile(data_file("telegram.webp").open("rb")).mimetype in [ "application/octet-stream", "image/webp", ] assert InputFile(data_file("telegram.mp3").open("rb")).mimetype == "audio/mpeg" # For some reason windows drops the trailing i assert InputFile(data_file("telegram.midi").open("rb")).mimetype in [ "audio/mid", "audio/midi", ] # Test guess from file assert InputFile(BytesIO(b"blah"), filename="tg.jpg").mimetype == "image/jpeg" assert InputFile(BytesIO(b"blah"), filename="tg.mp3").mimetype == "audio/mpeg" # Test fallback assert ( InputFile(BytesIO(b"blah"), filename="tg.notaproperext").mimetype == "application/octet-stream" ) assert InputFile(BytesIO(b"blah")).mimetype == "application/octet-stream" # Test string file assert InputFile(data_file("text_file.txt").open()).mimetype == "text/plain" def test_filenames(self): assert InputFile(data_file("telegram.jpg").open("rb")).filename == "telegram.jpg" assert InputFile(data_file("telegram.jpg").open("rb"), filename="blah").filename == "blah" assert ( InputFile(data_file("telegram.jpg").open("rb"), filename="blah.jpg").filename == "blah.jpg" ) assert InputFile(data_file("telegram").open("rb")).filename == "telegram" assert InputFile(data_file("telegram").open("rb"), filename="blah").filename == "blah" assert ( InputFile(data_file("telegram").open("rb"), filename="blah.jpg").filename == "blah.jpg" ) class MockedFileobject: # A open(?, 'rb') without a .name def __init__(self, f): self.f = f.open("rb") def read(self): return self.f.read() assert ( InputFile(MockedFileobject(data_file("telegram.jpg"))).filename == "application.octet-stream" ) assert ( InputFile(MockedFileobject(data_file("telegram.jpg")), filename="blah").filename == "blah" ) assert ( InputFile(MockedFileobject(data_file("telegram.jpg")), filename="blah.jpg").filename == "blah.jpg" ) assert ( InputFile(MockedFileobject(data_file("telegram"))).filename == "application.octet-stream" ) assert ( InputFile(MockedFileobject(data_file("telegram")), filename="blah").filename == "blah" ) assert ( InputFile(MockedFileobject(data_file("telegram")), filename="blah.jpg").filename == "blah.jpg" ) class TestInputFileWithRequest: async def test_send_bytes(self, bot, chat_id): # We test this here and not at the respective test modules because it's not worth # duplicating the test for the different methods message = await bot.send_document(chat_id, data_file("text_file.txt").read_bytes()) out = BytesIO() await (await message.document.get_file()).download_to_memory(out=out) out.seek(0) assert out.read().decode("utf-8") == "PTB Rocks! ⅞" async def test_send_string(self, bot, chat_id): # We test this here and not at the respective test modules because it's not worth # duplicating the test for the different methods message = await bot.send_document( chat_id, InputFile(data_file("text_file.txt").read_text(encoding="utf-8")) ) out = BytesIO() await (await message.document.get_file()).download_to_memory(out=out) out.seek(0) assert out.read().decode("utf-8") == "PTB Rocks! ⅞" python-telegram-bot-21.1.1/tests/_files/test_inputmedia.py000066400000000000000000001255741460724040100236640ustar00rootroot00000000000000#!/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 General 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import asyncio import copy from collections.abc import Sequence from typing import Optional import pytest from telegram import ( InputFile, InputMedia, InputMediaAnimation, InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo, Message, MessageEntity, ReplyParameters, ) from telegram.constants import InputMediaType, ParseMode # noinspection PyUnresolvedReferences from telegram.error import BadRequest from telegram.request import RequestData from tests._files.test_animation import animation, animation_file # noqa: F401 from tests.auxil.files import data_file from tests.auxil.networking import expect_bad_request from tests.auxil.slots import mro_slots # noinspection PyUnresolvedReferences from tests.test_forum import emoji_id, real_topic # noqa: F401 from ..auxil.build_messages import make_message # noinspection PyUnresolvedReferences from .test_audio import audio, audio_file # noqa: F401 # noinspection PyUnresolvedReferences from .test_document import document, document_file # noqa: F401 # noinspection PyUnresolvedReferences from .test_photo import photo, photo_file, photolist, thumb # noqa: F401 # noinspection PyUnresolvedReferences from .test_video import video, video_file # noqa: F401 @pytest.fixture(scope="module") def input_media_video(class_thumb_file): return InputMediaVideo( media=TestInputMediaVideoBase.media, caption=TestInputMediaVideoBase.caption, width=TestInputMediaVideoBase.width, height=TestInputMediaVideoBase.height, duration=TestInputMediaVideoBase.duration, parse_mode=TestInputMediaVideoBase.parse_mode, caption_entities=TestInputMediaVideoBase.caption_entities, thumbnail=class_thumb_file, supports_streaming=TestInputMediaVideoBase.supports_streaming, has_spoiler=TestInputMediaVideoBase.has_spoiler, ) @pytest.fixture(scope="module") def input_media_photo(): return InputMediaPhoto( media=TestInputMediaPhotoBase.media, caption=TestInputMediaPhotoBase.caption, parse_mode=TestInputMediaPhotoBase.parse_mode, caption_entities=TestInputMediaPhotoBase.caption_entities, has_spoiler=TestInputMediaPhotoBase.has_spoiler, ) @pytest.fixture(scope="module") def input_media_animation(class_thumb_file): return InputMediaAnimation( media=TestInputMediaAnimationBase.media, caption=TestInputMediaAnimationBase.caption, parse_mode=TestInputMediaAnimationBase.parse_mode, caption_entities=TestInputMediaAnimationBase.caption_entities, width=TestInputMediaAnimationBase.width, height=TestInputMediaAnimationBase.height, thumbnail=class_thumb_file, duration=TestInputMediaAnimationBase.duration, has_spoiler=TestInputMediaAnimationBase.has_spoiler, ) @pytest.fixture(scope="module") def input_media_audio(class_thumb_file): return InputMediaAudio( media=TestInputMediaAudioBase.media, caption=TestInputMediaAudioBase.caption, duration=TestInputMediaAudioBase.duration, performer=TestInputMediaAudioBase.performer, title=TestInputMediaAudioBase.title, thumbnail=class_thumb_file, parse_mode=TestInputMediaAudioBase.parse_mode, caption_entities=TestInputMediaAudioBase.caption_entities, ) @pytest.fixture(scope="module") def input_media_document(class_thumb_file): return InputMediaDocument( media=TestInputMediaDocumentBase.media, caption=TestInputMediaDocumentBase.caption, thumbnail=class_thumb_file, parse_mode=TestInputMediaDocumentBase.parse_mode, caption_entities=TestInputMediaDocumentBase.caption_entities, disable_content_type_detection=TestInputMediaDocumentBase.disable_content_type_detection, ) class TestInputMediaVideoBase: type_ = "video" media = "NOTAREALFILEID" caption = "My Caption" width = 3 height = 4 duration = 5 parse_mode = "HTML" supports_streaming = True caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] has_spoiler = True class TestInputMediaVideoWithoutRequest(TestInputMediaVideoBase): def test_slot_behaviour(self, input_media_video): inst = input_media_video 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, input_media_video): assert input_media_video.type == self.type_ assert input_media_video.media == self.media assert input_media_video.caption == self.caption assert input_media_video.width == self.width assert input_media_video.height == self.height assert input_media_video.duration == self.duration assert input_media_video.parse_mode == self.parse_mode assert input_media_video.caption_entities == tuple(self.caption_entities) assert input_media_video.supports_streaming == self.supports_streaming assert isinstance(input_media_video.thumbnail, InputFile) assert input_media_video.has_spoiler == self.has_spoiler def test_caption_entities_always_tuple(self): input_media_video = InputMediaVideo(self.media) assert input_media_video.caption_entities == () def test_to_dict(self, input_media_video): input_media_video_dict = input_media_video.to_dict() assert input_media_video_dict["type"] == input_media_video.type assert input_media_video_dict["media"] == input_media_video.media assert input_media_video_dict["caption"] == input_media_video.caption assert input_media_video_dict["width"] == input_media_video.width assert input_media_video_dict["height"] == input_media_video.height assert input_media_video_dict["duration"] == input_media_video.duration assert input_media_video_dict["parse_mode"] == input_media_video.parse_mode assert input_media_video_dict["caption_entities"] == [ ce.to_dict() for ce in input_media_video.caption_entities ] assert input_media_video_dict["supports_streaming"] == input_media_video.supports_streaming assert input_media_video_dict["has_spoiler"] == input_media_video.has_spoiler def test_with_video(self, video): # noqa: F811 # fixture found in test_video input_media_video = InputMediaVideo(video, caption="test 3") assert input_media_video.type == self.type_ assert input_media_video.media == video.file_id assert input_media_video.width == video.width assert input_media_video.height == video.height assert input_media_video.duration == video.duration assert input_media_video.caption == "test 3" def test_with_video_file(self, video_file): # noqa: F811 # fixture found in test_video input_media_video = InputMediaVideo(video_file, caption="test 3") assert input_media_video.type == self.type_ assert isinstance(input_media_video.media, InputFile) assert input_media_video.caption == "test 3" def test_with_local_files(self): input_media_video = InputMediaVideo( data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg") ) assert input_media_video.media == data_file("telegram.mp4").as_uri() assert input_media_video.thumbnail == data_file("telegram.jpg").as_uri() def test_type_enum_conversion(self): # Since we have a lot of different test classes for all the input media types, we test this # conversion only here. It is independent of the specific class assert ( type( InputMedia( media_type="animation", media="media", ).type ) is InputMediaType ) assert ( InputMedia( media_type="unknown", media="media", ).type == "unknown" ) class TestInputMediaPhotoBase: type_ = "photo" media = "NOTAREALFILEID" caption = "My Caption" parse_mode = "Markdown" caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] has_spoiler = True class TestInputMediaPhotoWithoutRequest(TestInputMediaPhotoBase): def test_slot_behaviour(self, input_media_photo): inst = input_media_photo 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, input_media_photo): assert input_media_photo.type == self.type_ assert input_media_photo.media == self.media assert input_media_photo.caption == self.caption assert input_media_photo.parse_mode == self.parse_mode assert input_media_photo.caption_entities == tuple(self.caption_entities) assert input_media_photo.has_spoiler == self.has_spoiler def test_caption_entities_always_tuple(self): input_media_photo = InputMediaPhoto(self.media) assert input_media_photo.caption_entities == () def test_to_dict(self, input_media_photo): input_media_photo_dict = input_media_photo.to_dict() assert input_media_photo_dict["type"] == input_media_photo.type assert input_media_photo_dict["media"] == input_media_photo.media assert input_media_photo_dict["caption"] == input_media_photo.caption assert input_media_photo_dict["parse_mode"] == input_media_photo.parse_mode assert input_media_photo_dict["caption_entities"] == [ ce.to_dict() for ce in input_media_photo.caption_entities ] assert input_media_photo_dict["has_spoiler"] == input_media_photo.has_spoiler def test_with_photo(self, photo): # noqa: F811 # fixture found in test_photo input_media_photo = InputMediaPhoto(photo, caption="test 2") assert input_media_photo.type == self.type_ assert input_media_photo.media == photo.file_id assert input_media_photo.caption == "test 2" def test_with_photo_file(self, photo_file): # noqa: F811 # fixture found in test_photo input_media_photo = InputMediaPhoto(photo_file, caption="test 2") assert input_media_photo.type == self.type_ assert isinstance(input_media_photo.media, InputFile) assert input_media_photo.caption == "test 2" def test_with_local_files(self): input_media_photo = InputMediaPhoto(data_file("telegram.mp4")) assert input_media_photo.media == data_file("telegram.mp4").as_uri() class TestInputMediaAnimationBase: type_ = "animation" media = "NOTAREALFILEID" caption = "My Caption" parse_mode = "Markdown" caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] width = 30 height = 30 duration = 1 has_spoiler = True class TestInputMediaAnimationWithoutRequest(TestInputMediaAnimationBase): def test_slot_behaviour(self, input_media_animation): inst = input_media_animation 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, input_media_animation): assert input_media_animation.type == self.type_ assert input_media_animation.media == self.media assert input_media_animation.caption == self.caption assert input_media_animation.parse_mode == self.parse_mode assert input_media_animation.caption_entities == tuple(self.caption_entities) assert isinstance(input_media_animation.thumbnail, InputFile) assert input_media_animation.has_spoiler == self.has_spoiler def test_caption_entities_always_tuple(self): input_media_animation = InputMediaAnimation(self.media) assert input_media_animation.caption_entities == () def test_to_dict(self, input_media_animation): input_media_animation_dict = input_media_animation.to_dict() assert input_media_animation_dict["type"] == input_media_animation.type assert input_media_animation_dict["media"] == input_media_animation.media assert input_media_animation_dict["caption"] == input_media_animation.caption assert input_media_animation_dict["parse_mode"] == input_media_animation.parse_mode assert input_media_animation_dict["caption_entities"] == [ ce.to_dict() for ce in input_media_animation.caption_entities ] assert input_media_animation_dict["width"] == input_media_animation.width assert input_media_animation_dict["height"] == input_media_animation.height assert input_media_animation_dict["duration"] == input_media_animation.duration assert input_media_animation_dict["has_spoiler"] == input_media_animation.has_spoiler def test_with_animation(self, animation): # noqa: F811 # fixture found in test_animation input_media_animation = InputMediaAnimation(animation, caption="test 2") assert input_media_animation.type == self.type_ assert input_media_animation.media == animation.file_id assert input_media_animation.caption == "test 2" def test_with_animation_file(self, animation_file): # noqa: F811 # fixture found in test_animation input_media_animation = InputMediaAnimation(animation_file, caption="test 2") assert input_media_animation.type == self.type_ assert isinstance(input_media_animation.media, InputFile) assert input_media_animation.caption == "test 2" def test_with_local_files(self): input_media_animation = InputMediaAnimation( data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg") ) assert input_media_animation.media == data_file("telegram.mp4").as_uri() assert input_media_animation.thumbnail == data_file("telegram.jpg").as_uri() class TestInputMediaAudioBase: type_ = "audio" media = "NOTAREALFILEID" caption = "My Caption" duration = 3 performer = "performer" title = "title" parse_mode = "HTML" caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] class TestInputMediaAudioWithoutRequest(TestInputMediaAudioBase): def test_slot_behaviour(self, input_media_audio): inst = input_media_audio 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, input_media_audio): assert input_media_audio.type == self.type_ assert input_media_audio.media == self.media assert input_media_audio.caption == self.caption assert input_media_audio.duration == self.duration assert input_media_audio.performer == self.performer assert input_media_audio.title == self.title assert input_media_audio.parse_mode == self.parse_mode assert input_media_audio.caption_entities == tuple(self.caption_entities) assert isinstance(input_media_audio.thumbnail, InputFile) def test_caption_entities_always_tuple(self): input_media_audio = InputMediaAudio(self.media) assert input_media_audio.caption_entities == () def test_to_dict(self, input_media_audio): input_media_audio_dict = input_media_audio.to_dict() assert input_media_audio_dict["type"] == input_media_audio.type assert input_media_audio_dict["media"] == input_media_audio.media assert input_media_audio_dict["caption"] == input_media_audio.caption assert input_media_audio_dict["duration"] == input_media_audio.duration assert input_media_audio_dict["performer"] == input_media_audio.performer assert input_media_audio_dict["title"] == input_media_audio.title assert input_media_audio_dict["parse_mode"] == input_media_audio.parse_mode assert input_media_audio_dict["caption_entities"] == [ ce.to_dict() for ce in input_media_audio.caption_entities ] def test_with_audio(self, audio): # noqa: F811 # fixture found in test_audio input_media_audio = InputMediaAudio(audio, caption="test 3") assert input_media_audio.type == self.type_ assert input_media_audio.media == audio.file_id assert input_media_audio.duration == audio.duration assert input_media_audio.performer == audio.performer assert input_media_audio.title == audio.title assert input_media_audio.caption == "test 3" def test_with_audio_file(self, audio_file): # noqa: F811 # fixture found in test_audio input_media_audio = InputMediaAudio(audio_file, caption="test 3") assert input_media_audio.type == self.type_ assert isinstance(input_media_audio.media, InputFile) assert input_media_audio.caption == "test 3" def test_with_local_files(self): input_media_audio = InputMediaAudio( data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg") ) assert input_media_audio.media == data_file("telegram.mp4").as_uri() assert input_media_audio.thumbnail == data_file("telegram.jpg").as_uri() class TestInputMediaDocumentBase: type_ = "document" media = "NOTAREALFILEID" caption = "My Caption" parse_mode = "HTML" caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] disable_content_type_detection = True class TestInputMediaDocumentWithoutRequest(TestInputMediaDocumentBase): def test_slot_behaviour(self, input_media_document): inst = input_media_document 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, input_media_document): assert input_media_document.type == self.type_ assert input_media_document.media == self.media assert input_media_document.caption == self.caption assert input_media_document.parse_mode == self.parse_mode assert input_media_document.caption_entities == tuple(self.caption_entities) assert ( input_media_document.disable_content_type_detection == self.disable_content_type_detection ) assert isinstance(input_media_document.thumbnail, InputFile) def test_caption_entities_always_tuple(self): input_media_document = InputMediaDocument(self.media) assert input_media_document.caption_entities == () def test_to_dict(self, input_media_document): input_media_document_dict = input_media_document.to_dict() assert input_media_document_dict["type"] == input_media_document.type assert input_media_document_dict["media"] == input_media_document.media assert input_media_document_dict["caption"] == input_media_document.caption assert input_media_document_dict["parse_mode"] == input_media_document.parse_mode assert input_media_document_dict["caption_entities"] == [ ce.to_dict() for ce in input_media_document.caption_entities ] assert ( input_media_document["disable_content_type_detection"] == input_media_document.disable_content_type_detection ) def test_with_document(self, document): # noqa: F811 # fixture found in test_document input_media_document = InputMediaDocument(document, caption="test 3") assert input_media_document.type == self.type_ assert input_media_document.media == document.file_id assert input_media_document.caption == "test 3" def test_with_document_file(self, document_file): # noqa: F811 # fixture found in test_document input_media_document = InputMediaDocument(document_file, caption="test 3") assert input_media_document.type == self.type_ assert isinstance(input_media_document.media, InputFile) assert input_media_document.caption == "test 3" def test_with_local_files(self): input_media_document = InputMediaDocument( data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg") ) assert input_media_document.media == data_file("telegram.mp4").as_uri() assert input_media_document.thumbnail == data_file("telegram.jpg").as_uri() @pytest.fixture(scope="module") def media_group(photo, thumb): # noqa: F811 return [ InputMediaPhoto(photo, caption="*photo* 1", parse_mode="Markdown"), InputMediaPhoto(thumb, caption="photo 2", parse_mode="HTML"), InputMediaPhoto( photo, caption="photo 3", caption_entities=[MessageEntity(MessageEntity.BOLD, 0, 5)] ), ] @pytest.fixture(scope="module") def media_group_no_caption_args(photo, thumb): # noqa: F811 return [InputMediaPhoto(photo), InputMediaPhoto(thumb), InputMediaPhoto(photo)] @pytest.fixture(scope="module") def media_group_no_caption_only_caption_entities(photo, thumb): # noqa: F811 return [ InputMediaPhoto(photo, caption_entities=[MessageEntity(MessageEntity.BOLD, 0, 5)]), InputMediaPhoto(photo, caption_entities=[MessageEntity(MessageEntity.BOLD, 0, 5)]), ] @pytest.fixture(scope="module") def media_group_no_caption_only_parse_mode(photo, thumb): # noqa: F811 return [ InputMediaPhoto(photo, parse_mode="Markdown"), InputMediaPhoto(thumb, parse_mode="HTML"), ] class TestSendMediaGroupWithoutRequest: async def test_send_media_group_throws_error_with_group_caption_and_individual_captions( self, bot, chat_id, media_group, media_group_no_caption_only_caption_entities, media_group_no_caption_only_parse_mode, ): for group in ( media_group, media_group_no_caption_only_caption_entities, media_group_no_caption_only_parse_mode, ): with pytest.raises( ValueError, match="You can only supply either group caption or media with captions.", ): await bot.send_media_group(chat_id, group, caption="foo") async def test_send_media_group_custom_filename( self, bot, chat_id, photo_file, # noqa: F811 animation_file, # noqa: F811 audio_file, # noqa: F811 video_file, # noqa: F811 monkeypatch, ): async def make_assertion(url, request_data: RequestData, *args, **kwargs): result = all( field_tuple[0] == "custom_filename" for field_tuple in request_data.multipart_data.values() ) if result is True: raise Exception("Test was successful") monkeypatch.setattr(bot.request, "post", make_assertion) media = [ InputMediaAnimation(animation_file, filename="custom_filename"), InputMediaAudio(audio_file, filename="custom_filename"), InputMediaPhoto(photo_file, filename="custom_filename"), InputMediaVideo(video_file, filename="custom_filename"), ] with pytest.raises(Exception, match="Test was successful"): await bot.send_media_group(chat_id, media) async def test_send_media_group_with_thumbs( self, bot, chat_id, video_file, photo_file, monkeypatch # noqa: F811 ): async def make_assertion(method, url, request_data: RequestData, *args, **kwargs): nonlocal input_video files = request_data.multipart_data video_check = files[input_video.media.attach_name] == input_video.media.field_tuple thumb_check = ( files[input_video.thumbnail.attach_name] == input_video.thumbnail.field_tuple ) result = video_check and thumb_check raise Exception(f"Test was {'successful' if result else 'failing'}") monkeypatch.setattr(bot.request, "_request_wrapper", make_assertion) input_video = InputMediaVideo(video_file, thumbnail=photo_file) with pytest.raises(Exception, match="Test was successful"): await bot.send_media_group(chat_id, [input_video, input_video]) async def test_edit_message_media_with_thumb( self, bot, chat_id, video_file, photo_file, monkeypatch # noqa: F811 ): async def make_assertion( method: str, url: str, request_data: Optional[RequestData] = None, *args, **kwargs ): files = request_data.multipart_data video_check = files[input_video.media.attach_name] == input_video.media.field_tuple thumb_check = ( files[input_video.thumbnail.attach_name] == input_video.thumbnail.field_tuple ) result = video_check and thumb_check raise Exception(f"Test was {'successful' if result else 'failing'}") monkeypatch.setattr(bot.request, "_request_wrapper", make_assertion) input_video = InputMediaVideo(video_file, thumbnail=photo_file) with pytest.raises(Exception, match="Test was successful"): await bot.edit_message_media(chat_id=chat_id, message_id=123, media=input_video) @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_media_group_default_quote_parse_mode( self, default_bot, chat_id, media_group, 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_media_group( chat_id, media_group, reply_parameters=ReplyParameters(**kwargs) ) class CustomSequence(Sequence): def __init__(self, items): self.items = items def __getitem__(self, index): return self.items[index] def __len__(self): return len(self.items) class TestSendMediaGroupWithRequest: async def test_send_media_group_photo(self, bot, chat_id, media_group): messages = await bot.send_media_group(chat_id, media_group) assert isinstance(messages, tuple) assert len(messages) == 3 assert all(isinstance(mes, Message) for mes in messages) assert all(mes.media_group_id == messages[0].media_group_id for mes in messages) assert all(mes.caption == f"photo {idx + 1}" for idx, mes in enumerate(messages)) assert all( mes.caption_entities == (MessageEntity(MessageEntity.BOLD, 0, 5),) for mes in messages ) async def test_send_media_group_new_files( self, bot, chat_id, video_file, photo_file # noqa: F811 ): async def func(): return await bot.send_media_group( chat_id, [ InputMediaVideo(video_file), InputMediaPhoto(photo_file), InputMediaPhoto(data_file("telegram.jpg").read_bytes()), ], ) messages = await expect_bad_request( func, "Type of file mismatch", "Telegram did not accept the file." ) assert isinstance(messages, tuple) assert len(messages) == 3 assert all(isinstance(mes, Message) for mes in messages) assert all(mes.media_group_id == messages[0].media_group_id for mes in messages) @pytest.mark.parametrize("sequence_type", [list, tuple, CustomSequence]) @pytest.mark.parametrize("bot_class", ["raw_bot", "ext_bot"]) async def test_send_media_group_different_sequences( self, bot, chat_id, media_group, sequence_type, bot_class, raw_bot ): """Test that send_media_group accepts different sequence types. This test ensures that Bot._insert_defaults works for arbitrary sequence types.""" bot = bot if bot_class == "ext_bot" else raw_bot messages = await bot.send_media_group(chat_id, sequence_type(media_group)) assert isinstance(messages, tuple) assert len(messages) == 3 assert all(isinstance(mes, Message) for mes in messages) assert all(mes.media_group_id == messages[0].media_group_id for mes in messages) async def test_send_media_group_with_message_thread_id( self, bot, real_topic, forum_group_id, media_group # noqa: F811 ): messages = await bot.send_media_group( forum_group_id, media_group, message_thread_id=real_topic.message_thread_id, ) assert isinstance(messages, tuple) assert len(messages) == 3 assert all(isinstance(mes, Message) for mes in messages) assert all(i.message_thread_id == real_topic.message_thread_id for i in messages) @pytest.mark.parametrize( ("caption", "parse_mode", "caption_entities"), [ # same combinations of caption options as in media_group fixture ("*photo* 1", "Markdown", None), ("photo 1", "HTML", None), ("photo 1", None, [MessageEntity(MessageEntity.BOLD, 0, 5)]), ], ) async def test_send_media_group_with_group_caption( self, bot, chat_id, media_group_no_caption_args, caption, parse_mode, caption_entities, ): # prepare a copy to check later on if calling the method has caused side effects copied_media_group = media_group_no_caption_args.copy() messages = await bot.send_media_group( chat_id, media_group_no_caption_args, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, ) # Check that the method had no side effects: # original group was not changed and 1st item still points to the same object # (1st item must be copied within the method before adding the caption) assert media_group_no_caption_args == copied_media_group assert media_group_no_caption_args[0] is copied_media_group[0] assert not any(item.parse_mode for item in media_group_no_caption_args) assert isinstance(messages, tuple) assert len(messages) == 3 assert all(isinstance(mes, Message) for mes in messages) first_message, other_messages = messages[0], messages[1:] assert all(mes.media_group_id == first_message.media_group_id for mes in messages) # Make sure first message got the caption, which will lead # to Telegram displaying its caption as group caption assert first_message.caption assert first_message.caption_entities == (MessageEntity(MessageEntity.BOLD, 0, 5),) # Check that other messages have no captions assert all(mes.caption is None for mes in other_messages) assert not any(mes.caption_entities for mes in other_messages) async def test_send_media_group_all_args(self, bot, raw_bot, chat_id, media_group): ext_bot = bot # We need to test 1) below both the bot and raw_bot and setting this up with # pytest.parametrize appears to be difficult ... aws = {b.send_message(chat_id, text="test") for b in (ext_bot, raw_bot)} for msg_task in asyncio.as_completed(aws): m1 = await msg_task copied_media_group = copy.copy(media_group) messages = await m1.get_bot().send_media_group( chat_id, media_group, disable_notification=True, reply_to_message_id=m1.message_id, protect_content=True, ) # 1) # make sure that the media_group was not modified assert media_group == copied_media_group assert all( a.parse_mode == b.parse_mode for a, b in zip(media_group, copied_media_group) ) assert isinstance(messages, tuple) assert len(messages) == 3 assert all(isinstance(mes, Message) for mes in messages) assert all(mes.media_group_id == messages[0].media_group_id for mes in messages) assert all(mes.caption == f"photo {idx + 1}" for idx, mes in enumerate(messages)) assert all( mes.caption_entities == (MessageEntity(MessageEntity.BOLD, 0, 5),) for mes in messages ) assert all(mes.has_protected_content for mes in messages) async def test_send_media_group_with_spoiler( self, bot, chat_id, photo_file, video_file # noqa: F811 ): # Media groups can't contain Animations, so that is tested in test_animation.py media = [ InputMediaPhoto(photo_file, has_spoiler=True), InputMediaVideo(video_file, has_spoiler=True), ] messages = await bot.send_media_group(chat_id, media) assert isinstance(messages, tuple) assert len(messages) == 2 assert all(isinstance(mes, Message) for mes in messages) assert all(mes.media_group_id == messages[0].media_group_id for mes in messages) assert all(mes.has_media_spoiler for mes in messages) async def test_edit_message_media(self, bot, raw_bot, chat_id, media_group): ext_bot = bot # We need to test 1) below both the bot and raw_bot and setting this up with # pytest.parametrize appears to be difficult ... aws = {b.send_media_group(chat_id, media_group) for b in (ext_bot, raw_bot)} for msg_task in asyncio.as_completed(aws): messages = await msg_task cid = messages[-1].chat.id mid = messages[-1].message_id copied_media = copy.copy(media_group[0]) new_message = ( await messages[-1] .get_bot() .edit_message_media(chat_id=cid, message_id=mid, media=media_group[0]) ) assert isinstance(new_message, Message) # 1) # make sure that the media was not modified assert media_group[0].parse_mode == copied_media.parse_mode async def test_edit_message_media_new_file(self, bot, chat_id, media_group, thumb_file): messages = await bot.send_media_group(chat_id, media_group) cid = messages[-1].chat.id mid = messages[-1].message_id new_message = await bot.edit_message_media( chat_id=cid, message_id=mid, media=InputMediaPhoto(thumb_file) ) assert isinstance(new_message, Message) @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_media_group_default_allow_sending_without_reply( self, default_bot, chat_id, media_group, custom ): reply_to_message = await default_bot.send_message(chat_id, "test") await reply_to_message.delete() if custom is not None: messages = await default_bot.send_media_group( chat_id, media_group, allow_sending_without_reply=custom, reply_to_message_id=reply_to_message.message_id, ) assert [m.reply_to_message is None for m in messages] elif default_bot.defaults.allow_sending_without_reply: messages = await default_bot.send_media_group( chat_id, media_group, reply_to_message_id=reply_to_message.message_id ) assert [m.reply_to_message is None for m in messages] else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_media_group( chat_id, media_group, reply_to_message_id=reply_to_message.message_id ) @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_media_group_default_protect_content( self, chat_id, media_group, default_bot ): tasks = asyncio.gather( default_bot.send_media_group(chat_id, media_group), default_bot.send_media_group(chat_id, media_group, protect_content=False), ) protected, unprotected = await tasks assert all(msg.has_protected_content for msg in protected) assert not all(msg.has_protected_content for msg in unprotected) @pytest.mark.parametrize("default_bot", [{"parse_mode": ParseMode.HTML}], indirect=True) async def test_send_media_group_default_parse_mode( self, chat_id, media_group_no_caption_args, default_bot ): default = await default_bot.send_media_group( chat_id, media_group_no_caption_args, caption="photo 1" ) # make sure no parse_mode was set as a side effect assert not any(item.parse_mode for item in media_group_no_caption_args) tasks = asyncio.gather( default_bot.send_media_group( chat_id, media_group_no_caption_args.copy(), caption="*photo* 1", parse_mode=ParseMode.MARKDOWN_V2, ), default_bot.send_media_group( chat_id, media_group_no_caption_args.copy(), caption="photo 1", parse_mode=None, ), ) overridden_markdown_v2, overridden_none = await tasks # Make sure first message got the caption, which will lead to Telegram # displaying its caption as group caption assert overridden_none[0].caption == "photo 1" assert not overridden_none[0].caption_entities # First messages in these two groups have to have caption "photo 1" # because of parse mode (default or explicit) for mes_group in (default, overridden_markdown_v2): first_message = mes_group[0] assert first_message.caption == "photo 1" assert first_message.caption_entities == (MessageEntity(MessageEntity.BOLD, 0, 5),) # This check is valid for all 3 groups of messages for mes_group in (default, overridden_markdown_v2, overridden_none): first_message, other_messages = mes_group[0], mes_group[1:] assert all(mes.media_group_id == first_message.media_group_id for mes in mes_group) # Check that messages from 2nd message onwards have no captions assert all(mes.caption is None for mes in other_messages) assert not any(mes.caption_entities for mes in other_messages) @pytest.mark.parametrize( "default_bot", [{"parse_mode": ParseMode.HTML}], indirect=True, ids=["HTML-Bot"] ) @pytest.mark.parametrize("media_type", ["animation", "document", "audio", "photo", "video"]) async def test_edit_message_media_default_parse_mode( self, chat_id, default_bot, media_type, animation, # noqa: F811 document, # noqa: F811 audio, # noqa: F811 photo, # noqa: F811 video, # noqa: F811 ): html_caption = "bold italic code" markdown_caption = "*bold* _italic_ `code`" test_caption = "bold italic code" test_entities = [ MessageEntity(MessageEntity.BOLD, 0, 4), MessageEntity(MessageEntity.ITALIC, 5, 6), MessageEntity(MessageEntity.CODE, 12, 4), ] def build_media(parse_mode, med_type): kwargs = {} if parse_mode != ParseMode.HTML: kwargs["parse_mode"] = parse_mode kwargs["caption"] = markdown_caption else: kwargs["caption"] = html_caption if med_type == "animation": return InputMediaAnimation(animation, **kwargs) if med_type == "document": return InputMediaDocument(document, **kwargs) if med_type == "audio": return InputMediaAudio(audio, **kwargs) if med_type == "photo": return InputMediaPhoto(photo, **kwargs) if med_type == "video": return InputMediaVideo(video, **kwargs) return None message = await default_bot.send_photo(chat_id, photo) media = build_media(parse_mode=ParseMode.HTML, med_type=media_type) copied_media = copy.copy(media) message = await default_bot.edit_message_media( media, message.chat_id, message.message_id, ) assert message.caption == test_caption assert message.caption_entities == tuple(test_entities) # make sure that the media was not modified assert media.parse_mode == copied_media.parse_mode # Remove caption to avoid "Message not changed" await message.edit_caption() media = build_media(parse_mode=ParseMode.MARKDOWN_V2, med_type=media_type) copied_media = copy.copy(media) message = await default_bot.edit_message_media( media, message.chat_id, message.message_id, ) assert message.caption == test_caption assert message.caption_entities == tuple(test_entities) # make sure that the media was not modified assert media.parse_mode == copied_media.parse_mode # Remove caption to avoid "Message not changed" await message.edit_caption() media = build_media(parse_mode=None, med_type=media_type) copied_media = copy.copy(media) message = await default_bot.edit_message_media( media, message.chat_id, message.message_id, ) assert message.caption == markdown_caption assert message.caption_entities == () # make sure that the media was not modified assert media.parse_mode == copied_media.parse_mode python-telegram-bot-21.1.1/tests/_files/test_inputsticker.py000066400000000000000000000070261460724040100242400ustar00rootroot00000000000000#!/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 InputSticker, MaskPosition from telegram._files.inputfile import InputFile from tests._files.test_sticker import video_sticker_file # noqa: F401 from tests.auxil.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def input_sticker(): return InputSticker( sticker=TestInputStickerBase.sticker, emoji_list=TestInputStickerBase.emoji_list, mask_position=TestInputStickerBase.mask_position, keywords=TestInputStickerBase.keywords, format=TestInputStickerBase.format, ) class TestInputStickerBase: sticker = "fake_file_id" emoji_list = ("👍", "👎") mask_position = MaskPosition("forehead", 0.5, 0.5, 0.5) keywords = ("thumbsup", "thumbsdown") format = "static" class TestInputStickerWithoutRequest(TestInputStickerBase): def test_slot_behaviour(self, input_sticker): inst = input_sticker 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, input_sticker): assert input_sticker.sticker == self.sticker assert isinstance(input_sticker.sticker, str) assert input_sticker.emoji_list == self.emoji_list assert input_sticker.mask_position == self.mask_position assert input_sticker.keywords == self.keywords assert input_sticker.format == self.format def test_attributes_tuple(self, input_sticker): assert isinstance(input_sticker.keywords, tuple) assert isinstance(input_sticker.emoji_list, tuple) a = InputSticker("sticker", ["emoji"], "static") assert isinstance(a.emoji_list, tuple) assert a.keywords == () def test_to_dict(self, input_sticker): input_sticker_dict = input_sticker.to_dict() assert isinstance(input_sticker_dict, dict) assert input_sticker_dict["sticker"] == input_sticker.sticker assert input_sticker_dict["emoji_list"] == list(input_sticker.emoji_list) assert input_sticker_dict["mask_position"] == input_sticker.mask_position.to_dict() assert input_sticker_dict["keywords"] == list(input_sticker.keywords) assert input_sticker_dict["format"] == input_sticker.format def test_with_sticker_input_types(self, video_sticker_file): # noqa: F811 sticker = InputSticker(sticker=video_sticker_file, emoji_list=["👍"], format="video") assert isinstance(sticker.sticker, InputFile) sticker = InputSticker(data_file("telegram_video_sticker.webm"), ["👍"], "video") assert sticker.sticker == data_file("telegram_video_sticker.webm").as_uri() python-telegram-bot-21.1.1/tests/_files/test_location.py000066400000000000000000000271131460724040100233230ustar00rootroot00000000000000#!/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 pytest from telegram import Location, ReplyParameters from telegram.constants import ParseMode from telegram.error import BadRequest from telegram.request import RequestData from tests.auxil.build_messages import make_message from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def location(): return Location( latitude=TestLocationBase.latitude, longitude=TestLocationBase.longitude, horizontal_accuracy=TestLocationBase.horizontal_accuracy, live_period=TestLocationBase.live_period, heading=TestLocationBase.live_period, proximity_alert_radius=TestLocationBase.proximity_alert_radius, ) class TestLocationBase: latitude = -23.691288 longitude = -46.788279 horizontal_accuracy = 999 live_period = 60 heading = 90 proximity_alert_radius = 50 class TestLocationWithoutRequest(TestLocationBase): def test_slot_behaviour(self, location): for attr in location.__slots__: assert getattr(location, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(location)) == len(set(mro_slots(location))), "duplicate slot" def test_de_json(self, bot): json_dict = { "latitude": self.latitude, "longitude": self.longitude, "horizontal_accuracy": self.horizontal_accuracy, "live_period": self.live_period, "heading": self.heading, "proximity_alert_radius": self.proximity_alert_radius, } location = Location.de_json(json_dict, bot) assert location.api_kwargs == {} assert location.latitude == self.latitude assert location.longitude == self.longitude assert location.horizontal_accuracy == self.horizontal_accuracy assert location.live_period == self.live_period assert location.heading == self.heading assert location.proximity_alert_radius == self.proximity_alert_radius def test_to_dict(self, location): location_dict = location.to_dict() assert location_dict["latitude"] == location.latitude assert location_dict["longitude"] == location.longitude assert location_dict["horizontal_accuracy"] == location.horizontal_accuracy assert location_dict["live_period"] == location.live_period assert location["heading"] == location.heading assert location["proximity_alert_radius"] == location.proximity_alert_radius def test_equality(self): a = Location(self.longitude, self.latitude) b = Location(self.longitude, self.latitude) d = Location(0, self.latitude) assert a == b assert hash(a) == hash(b) assert a is not b assert a != d assert hash(a) != hash(d) async def test_send_location_without_required(self, bot, chat_id): with pytest.raises(ValueError, match="Either location or latitude and longitude"): await bot.send_location(chat_id=chat_id) async def test_edit_location_without_required(self, bot): with pytest.raises(ValueError, match="Either location or latitude and longitude"): await bot.edit_message_live_location(chat_id=2, message_id=3) async def test_send_location_with_all_args(self, bot, location): with pytest.raises(ValueError, match="Not both"): await bot.send_location(chat_id=1, latitude=2.5, longitude=4.6, location=location) async def test_edit_location_with_all_args(self, bot, location): with pytest.raises(ValueError, match="Not both"): await bot.edit_message_live_location( chat_id=1, message_id=7, latitude=2.5, longitude=4.6, location=location ) # TODO: Needs improvement with in inline sent live location. async def test_edit_live_inline_message(self, monkeypatch, bot, location): async def make_assertion(url, request_data: RequestData, *args, **kwargs): data = request_data.json_parameters lat = data["latitude"] == str(location.latitude) lon = data["longitude"] == str(location.longitude) id_ = data["inline_message_id"] == "1234" ha = data["horizontal_accuracy"] == "50" heading = data["heading"] == "90" prox_alert = data["proximity_alert_radius"] == "1000" return lat and lon and id_ and ha and heading and prox_alert monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.edit_message_live_location( inline_message_id=1234, location=location, horizontal_accuracy=50, heading=90, proximity_alert_radius=1000, ) # TODO: Needs improvement with in inline sent live location. async def test_stop_live_inline_message(self, monkeypatch, bot): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.json_parameters["inline_message_id"] == "1234" monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.stop_message_live_location(inline_message_id=1234) async def test_send_with_location(self, monkeypatch, bot, chat_id, location): async def make_assertion(url, request_data: RequestData, *args, **kwargs): lat = request_data.json_parameters["latitude"] == str(location.latitude) lon = request_data.json_parameters["longitude"] == str(location.longitude) return lat and lon monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_location(location=location, chat_id=chat_id) async def test_edit_live_location_with_location(self, monkeypatch, bot, location): async def make_assertion(url, request_data: RequestData, *args, **kwargs): lat = request_data.json_parameters["latitude"] == str(location.latitude) lon = request_data.json_parameters["longitude"] == str(location.longitude) return lat and lon monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.edit_message_live_location(None, None, location=location) @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_location_default_quote_parse_mode( self, default_bot, chat_id, location, 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_location( chat_id, location=location, reply_parameters=ReplyParameters(**kwargs) ) class TestLocationWithRequest: @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_location_default_allow_sending_without_reply( self, default_bot, chat_id, location, 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_location( chat_id, location=location, 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_location( chat_id, location=location, reply_to_message_id=reply_to_message.message_id ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_location( chat_id, location=location, reply_to_message_id=reply_to_message.message_id ) @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_location_default_protect_content(self, chat_id, default_bot, location): tasks = asyncio.gather( default_bot.send_location(chat_id, location=location), default_bot.send_location(chat_id, location=location, protect_content=False), ) protected, unprotected = await tasks assert protected.has_protected_content assert not unprotected.has_protected_content @pytest.mark.xfail() async def test_send_live_location(self, bot, chat_id): message = await bot.send_location( chat_id=chat_id, latitude=52.223880, longitude=5.166146, live_period=80, horizontal_accuracy=50, heading=90, proximity_alert_radius=1000, protect_content=True, ) assert message.location assert pytest.approx(message.location.latitude, rel=1e-5) == 52.223880 assert pytest.approx(message.location.longitude, rel=1e-5) == 5.166146 assert message.location.live_period == 80 assert message.location.horizontal_accuracy == 50 assert message.location.heading == 90 assert message.location.proximity_alert_radius == 1000 assert message.has_protected_content message2 = await bot.edit_message_live_location( message.chat_id, message.message_id, latitude=52.223098, longitude=5.164306, horizontal_accuracy=30, heading=10, proximity_alert_radius=500, ) assert pytest.approx(message2.location.latitude, rel=1e-5) == 52.223098 assert pytest.approx(message2.location.longitude, rel=1e-5) == 5.164306 assert message2.location.horizontal_accuracy == 30 assert message2.location.heading == 10 assert message2.location.proximity_alert_radius == 500 await bot.stop_message_live_location(message.chat_id, message.message_id) with pytest.raises(BadRequest, match="Message can't be edited"): await bot.edit_message_live_location( message.chat_id, message.message_id, latitude=52.223880, longitude=5.164306 ) python-telegram-bot-21.1.1/tests/_files/test_photo.py000066400000000000000000000465141460724040100226520ustar00rootroot00000000000000#!/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 os from io import BytesIO from pathlib import Path import pytest from telegram import Bot, InputFile, MessageEntity, PhotoSize, ReplyParameters, Sticker from telegram.constants import ParseMode from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData 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.files import data_file from tests.auxil.networking import expect_bad_request from tests.auxil.slots import mro_slots @pytest.fixture() def photo_file(): with data_file("telegram.jpg").open("rb") as f: yield f @pytest.fixture(scope="module") async def photolist(bot, chat_id): async def func(): with data_file("telegram.jpg").open("rb") as f: return (await bot.send_photo(chat_id, photo=f, read_timeout=50)).photo return await expect_bad_request( func, "Type of file mismatch", "Telegram did not accept the file." ) @pytest.fixture(scope="module") def thumb(photolist): return photolist[0] @pytest.fixture(scope="module") def photo(photolist): return photolist[-1] class TestPhotoBase: width = 800 height = 800 caption = "PhotoTest - *Caption*" photo_file_url = "https://python-telegram-bot.org/static/testfiles/telegram_new.jpg" # For some reason the file size is not the same after switching to httpx # so we accept three different sizes here. Shouldn't be too much file_size = [29176, 27662] class TestPhotoWithoutRequest(TestPhotoBase): def test_slot_behaviour(self, photo): for attr in photo.__slots__: assert getattr(photo, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(photo)) == len(set(mro_slots(photo))), "duplicate slot" def test_creation(self, thumb, photo): # Make sure file has been uploaded. assert isinstance(photo, PhotoSize) assert isinstance(photo.file_id, str) assert isinstance(photo.file_unique_id, str) assert photo.file_id assert photo.file_unique_id assert isinstance(thumb, PhotoSize) assert isinstance(thumb.file_id, str) assert isinstance(thumb.file_unique_id, str) assert thumb.file_id assert thumb.file_unique_id def test_expected_values(self, photo, thumb): assert photo.width == self.width assert photo.height == self.height assert photo.file_size in self.file_size assert thumb.width == 90 assert thumb.height == 90 # File sizes don't seem to be consistent, so we use the values that we have observed # so far assert thumb.file_size in [1475, 1477] def test_de_json(self, bot, photo): json_dict = { "file_id": photo.file_id, "file_unique_id": photo.file_unique_id, "width": self.width, "height": self.height, "file_size": self.file_size, } json_photo = PhotoSize.de_json(json_dict, bot) assert json_photo.api_kwargs == {} assert json_photo.file_id == photo.file_id assert json_photo.file_unique_id == photo.file_unique_id assert json_photo.width == self.width assert json_photo.height == self.height assert json_photo.file_size == self.file_size def test_to_dict(self, photo): photo_dict = photo.to_dict() assert isinstance(photo_dict, dict) assert photo_dict["file_id"] == photo.file_id assert photo_dict["file_unique_id"] == photo.file_unique_id assert photo_dict["width"] == photo.width assert photo_dict["height"] == photo.height assert photo_dict["file_size"] == photo.file_size def test_equality(self, photo): a = PhotoSize(photo.file_id, photo.file_unique_id, self.width, self.height) b = PhotoSize("", photo.file_unique_id, self.width, self.height) c = PhotoSize(photo.file_id, photo.file_unique_id, 0, 0) d = PhotoSize("", "", self.width, self.height) e = Sticker( photo.file_id, photo.file_unique_id, self.width, self.height, False, False, Sticker.REGULAR, ) 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_error_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): await bot.send_photo(chat_id=chat_id) async def test_send_photo_custom_filename(self, bot, chat_id, photo_file, monkeypatch): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return next(iter(request_data.multipart_data.values()))[0] == "custom_filename" monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_photo(chat_id, photo_file, filename="custom_filename") @pytest.mark.parametrize("local_mode", [True, False]) async def test_send_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 test_flag = False file = data_file("telegram.jpg") expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag if local_mode: test_flag = data.get("photo") == expected else: test_flag = isinstance(data.get("photo"), InputFile) monkeypatch.setattr(bot, "_post", make_assertion) await bot.send_photo(chat_id, file) assert test_flag finally: bot._local_mode = False async def test_send_with_photosize(self, monkeypatch, bot, chat_id, photo): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.json_parameters["photo"] == photo.file_id monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_photo(photo=photo, chat_id=chat_id) async def test_get_file_instance_method(self, monkeypatch, photo): async def make_assertion(*_, **kwargs): return kwargs["file_id"] == photo.file_id assert check_shortcut_signature(PhotoSize.get_file, Bot.get_file, ["file_id"], []) assert await check_shortcut_call(photo.get_file, photo.get_bot(), "get_file") assert await check_defaults_handling(photo.get_file, photo.get_bot()) monkeypatch.setattr(photo.get_bot(), "get_file", make_assertion) assert await photo.get_file() @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_photo_default_quote_parse_mode( self, default_bot, chat_id, photo, 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_photo(chat_id, photo, reply_parameters=ReplyParameters(**kwargs)) class TestPhotoWithRequest(TestPhotoBase): async def test_send_photo_all_args(self, bot, chat_id, photo_file): message = await bot.send_photo( chat_id, photo_file, caption=self.caption, disable_notification=False, protect_content=True, parse_mode="Markdown", has_spoiler=True, ) assert isinstance(message.photo[-2], PhotoSize) assert isinstance(message.photo[-2].file_id, str) assert isinstance(message.photo[-2].file_unique_id, str) assert message.photo[-2].file_id assert message.photo[-2].file_unique_id assert isinstance(message.photo[-1], PhotoSize) assert isinstance(message.photo[-1].file_id, str) assert isinstance(message.photo[-1].file_unique_id, str) assert message.photo[-1].file_id assert message.photo[-1].file_unique_id assert message.caption == self.caption.replace("*", "") assert message.has_protected_content assert message.has_media_spoiler async def test_send_photo_parse_mode_markdown(self, bot, chat_id, photo_file): message = await bot.send_photo( chat_id, photo_file, caption=self.caption, parse_mode="Markdown" ) assert isinstance(message.photo[-2], PhotoSize) assert isinstance(message.photo[-2].file_id, str) assert isinstance(message.photo[-2].file_unique_id, str) assert message.photo[-2].file_id assert message.photo[-2].file_unique_id assert isinstance(message.photo[-1], PhotoSize) assert isinstance(message.photo[-1].file_id, str) assert isinstance(message.photo[-1].file_unique_id, str) assert message.photo[-1].file_id assert message.photo[-1].file_unique_id assert message.caption == self.caption.replace("*", "") assert len(message.caption_entities) == 1 async def test_send_photo_parse_mode_html(self, bot, chat_id, photo_file): message = await bot.send_photo( chat_id, photo_file, caption=self.caption, parse_mode="HTML" ) assert isinstance(message.photo[-2], PhotoSize) assert isinstance(message.photo[-2].file_id, str) assert isinstance(message.photo[-2].file_unique_id, str) assert message.photo[-2].file_id assert message.photo[-2].file_unique_id assert isinstance(message.photo[-1], PhotoSize) assert isinstance(message.photo[-1].file_id, str) assert isinstance(message.photo[-1].file_unique_id, str) assert message.photo[-1].file_id assert message.photo[-1].file_unique_id assert message.caption == self.caption.replace("", "").replace("", "") assert len(message.caption_entities) == 1 async def test_send_photo_caption_entities(self, bot, chat_id, photo_file): 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_photo( chat_id, photo_file, caption=test_string, caption_entities=entities ) assert message.caption == test_string assert message.caption_entities == tuple(entities) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_photo_default_parse_mode_1(self, default_bot, chat_id, photo_file): test_string = "Italic Bold Code" test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_photo(chat_id, photo_file, caption=test_markdown_string) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_photo_default_parse_mode_2(self, default_bot, chat_id, photo_file): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_photo( chat_id, photo_file, caption=test_markdown_string, parse_mode=None ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_photo_default_parse_mode_3(self, default_bot, chat_id, photo_file): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_photo( chat_id, photo_file, caption=test_markdown_string, parse_mode="HTML" ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_photo_default_protect_content(self, chat_id, default_bot, photo): tasks = asyncio.gather( default_bot.send_photo(chat_id, photo), default_bot.send_photo(chat_id, photo, protect_content=False), ) protected, unprotected = await tasks assert protected.has_protected_content assert not unprotected.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_photo_default_allow_sending_without_reply( self, default_bot, chat_id, photo_file, 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_photo( chat_id, photo_file, 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_photo( chat_id, photo_file, reply_to_message_id=reply_to_message.message_id ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_photo( chat_id, photo_file, reply_to_message_id=reply_to_message.message_id ) async def test_get_and_download(self, bot, photo, tmp_file): new_file = await bot.getFile(photo.file_id) assert new_file.file_size == photo.file_size assert new_file.file_unique_id == photo.file_unique_id assert new_file.file_path.startswith("https://") is True await new_file.download_to_drive(tmp_file) assert tmp_file.is_file() async def test_send_url_jpg_file(self, bot, chat_id): message = await bot.send_photo(chat_id, photo=self.photo_file_url) assert isinstance(message.photo[-2], PhotoSize) assert isinstance(message.photo[-2].file_id, str) assert isinstance(message.photo[-2].file_unique_id, str) assert message.photo[-2].file_id assert message.photo[-2].file_unique_id assert isinstance(message.photo[-1], PhotoSize) assert isinstance(message.photo[-1].file_id, str) assert isinstance(message.photo[-1].file_unique_id, str) assert message.photo[-1].file_id assert message.photo[-1].file_unique_id async def test_send_url_png_file(self, bot, chat_id): message = await bot.send_photo( photo="http://dummyimage.com/600x400/000/fff.png&text=telegram", chat_id=chat_id ) photo = message.photo[-1] assert isinstance(photo, PhotoSize) assert isinstance(photo.file_id, str) assert isinstance(photo.file_unique_id, str) assert photo.file_id assert photo.file_unique_id async def test_send_file_unicode_filename(self, bot, chat_id): """ Regression test for https://github.com/python-telegram-bot/python-telegram-bot/issues/1202 """ with data_file("测试.png").open("rb") as f: message = await bot.send_photo(photo=f, chat_id=chat_id) photo = message.photo[-1] assert isinstance(photo, PhotoSize) assert isinstance(photo.file_id, str) assert isinstance(photo.file_unique_id, str) assert photo.file_id assert photo.file_unique_id async def test_send_bytesio_jpg_file(self, bot, chat_id): filepath = data_file("telegram_no_standard_header.jpg") # raw image bytes raw_bytes = BytesIO(filepath.read_bytes()) input_file = InputFile(raw_bytes) assert input_file.mimetype == "application/octet-stream" # raw image bytes with name info raw_bytes = BytesIO(filepath.read_bytes()) raw_bytes.name = str(filepath) input_file = InputFile(raw_bytes) assert input_file.mimetype == "image/jpeg" # send raw photo raw_bytes = BytesIO(filepath.read_bytes()) message = await bot.send_photo(chat_id, photo=raw_bytes) photo = message.photo[-1] assert isinstance(photo.file_id, str) assert isinstance(photo.file_unique_id, str) assert photo.file_id assert photo.file_unique_id assert isinstance(photo, PhotoSize) assert photo.width == 1280 assert photo.height == 720 assert photo.file_size == 33372 async def test_resend(self, bot, chat_id, photo): message = await bot.send_photo(chat_id=chat_id, photo=photo.file_id) assert isinstance(message.photo[-2], PhotoSize) assert isinstance(message.photo[-2].file_id, str) assert isinstance(message.photo[-2].file_unique_id, str) assert message.photo[-2].file_id assert message.photo[-2].file_unique_id assert isinstance(message.photo[-1], PhotoSize) assert isinstance(message.photo[-1].file_id, str) assert isinstance(message.photo[-1].file_unique_id, str) assert message.photo[-1].file_id assert message.photo[-1].file_unique_id async def test_error_send_empty_file(self, bot, chat_id): with Path(os.devnull).open("rb") as file, pytest.raises(TelegramError): await bot.send_photo(chat_id=chat_id, photo=file) async def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): await bot.send_photo(chat_id=chat_id, photo="") python-telegram-bot-21.1.1/tests/_files/test_sticker.py000066400000000000000000001347241460724040100231660ustar00rootroot00000000000000#!/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 os import random import string from pathlib import Path import pytest from telegram import ( Audio, Bot, File, InputFile, InputSticker, MaskPosition, PhotoSize, ReplyParameters, Sticker, StickerSet, ) from telegram.constants import ParseMode, StickerFormat, StickerType from telegram.error import BadRequest, TelegramError from telegram.request import RequestData from telegram.warnings import PTBDeprecationWarning 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.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture() def sticker_file(): with data_file("telegram.webp").open("rb") as file: yield file @pytest.fixture(scope="module") async def sticker(bot, chat_id): with data_file("telegram.webp").open("rb") as f: sticker = (await bot.send_sticker(chat_id, sticker=f, read_timeout=50)).sticker # necessary to properly test needs_repainting with sticker._unfrozen(): sticker.needs_repainting = TestStickerBase.needs_repainting return sticker @pytest.fixture() def animated_sticker_file(): with data_file("telegram_animated_sticker.tgs").open("rb") as f: yield f @pytest.fixture(scope="module") async def animated_sticker(bot, chat_id): with data_file("telegram_animated_sticker.tgs").open("rb") as f: return (await bot.send_sticker(chat_id, sticker=f, read_timeout=50)).sticker @pytest.fixture() def video_sticker_file(): with data_file("telegram_video_sticker.webm").open("rb") as f: yield f @pytest.fixture(scope="module") def video_sticker(bot, chat_id): with data_file("telegram_video_sticker.webm").open("rb") as f: return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker class TestStickerBase: # sticker_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.webp' # Serving sticker from gh since our server sends wrong content_type sticker_file_url = ( "https://github.com/python-telegram-bot/python-telegram-bot/blob/master" "/tests/data/telegram.webp?raw=true" ) emoji = "💪" width = 510 height = 512 is_animated = False is_video = False file_size = 39518 thumb_width = 319 thumb_height = 320 thumb_file_size = 21448 type = Sticker.REGULAR custom_emoji_id = "ThisIsSuchACustomEmojiID" needs_repainting = True sticker_file_id = "5a3128a4d2a04750b5b58397f3b5e812" sticker_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" premium_animation = File("this_is_an_id", "this_is_an_unique_id") class TestStickerWithoutRequest(TestStickerBase): def test_slot_behaviour(self, sticker): for attr in sticker.__slots__: assert getattr(sticker, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(sticker)) == len(set(mro_slots(sticker))), "duplicate slot" def test_creation(self, sticker): # Make sure file has been uploaded. assert isinstance(sticker, Sticker) assert isinstance(sticker.file_id, str) assert isinstance(sticker.file_unique_id, str) assert sticker.file_id assert sticker.file_unique_id assert isinstance(sticker.thumbnail, PhotoSize) assert isinstance(sticker.thumbnail.file_id, str) assert isinstance(sticker.thumbnail.file_unique_id, str) assert sticker.thumbnail.file_id assert sticker.thumbnail.file_unique_id assert isinstance(sticker.needs_repainting, bool) def test_expected_values(self, sticker): assert sticker.width == self.width assert sticker.height == self.height assert sticker.is_animated == self.is_animated assert sticker.is_video == self.is_video assert sticker.file_size == self.file_size assert sticker.thumbnail.width == self.thumb_width assert sticker.thumbnail.height == self.thumb_height assert sticker.thumbnail.file_size == self.thumb_file_size assert sticker.type == self.type assert sticker.needs_repainting == self.needs_repainting # we need to be a premium TG user to send a premium sticker, so the below is not tested # assert sticker.premium_animation == self.premium_animation def test_to_dict(self, sticker): sticker_dict = sticker.to_dict() assert isinstance(sticker_dict, dict) assert sticker_dict["file_id"] == sticker.file_id assert sticker_dict["file_unique_id"] == sticker.file_unique_id assert sticker_dict["width"] == sticker.width assert sticker_dict["height"] == sticker.height assert sticker_dict["is_animated"] == sticker.is_animated assert sticker_dict["is_video"] == sticker.is_video assert sticker_dict["file_size"] == sticker.file_size assert sticker_dict["thumbnail"] == sticker.thumbnail.to_dict() assert sticker_dict["type"] == sticker.type assert sticker_dict["needs_repainting"] == sticker.needs_repainting def test_de_json(self, bot, sticker): json_dict = { "file_id": self.sticker_file_id, "file_unique_id": self.sticker_file_unique_id, "width": self.width, "height": self.height, "is_animated": self.is_animated, "is_video": self.is_video, "thumbnail": sticker.thumbnail.to_dict(), "emoji": self.emoji, "file_size": self.file_size, "premium_animation": self.premium_animation.to_dict(), "type": self.type, "custom_emoji_id": self.custom_emoji_id, "needs_repainting": self.needs_repainting, } json_sticker = Sticker.de_json(json_dict, bot) assert json_sticker.api_kwargs == {} assert json_sticker.file_id == self.sticker_file_id assert json_sticker.file_unique_id == self.sticker_file_unique_id assert json_sticker.width == self.width assert json_sticker.height == self.height assert json_sticker.is_animated == self.is_animated assert json_sticker.is_video == self.is_video assert json_sticker.emoji == self.emoji assert json_sticker.file_size == self.file_size assert json_sticker.thumbnail == sticker.thumbnail assert json_sticker.premium_animation == self.premium_animation assert json_sticker.type == self.type assert json_sticker.custom_emoji_id == self.custom_emoji_id assert json_sticker.needs_repainting == self.needs_repainting def test_type_enum_conversion(self): assert ( type( Sticker( file_id=self.sticker_file_id, file_unique_id=self.sticker_file_unique_id, width=self.width, height=self.height, is_animated=self.is_animated, is_video=self.is_video, type="regular", ).type ) is StickerType ) assert ( Sticker( file_id=self.sticker_file_id, file_unique_id=self.sticker_file_unique_id, width=self.width, height=self.height, is_animated=self.is_animated, is_video=self.is_video, type="unknown", ).type == "unknown" ) def test_equality(self, sticker): a = Sticker( sticker.file_id, sticker.file_unique_id, self.width, self.height, self.is_animated, self.is_video, self.type, ) b = Sticker( "", sticker.file_unique_id, self.width, self.height, self.is_animated, self.is_video, self.type, ) c = Sticker( sticker.file_id, sticker.file_unique_id, 0, 0, False, True, self.type, ) d = Sticker( "", "", self.width, self.height, self.is_animated, self.is_video, self.type, ) e = PhotoSize( sticker.file_id, sticker.file_unique_id, self.width, self.height, self.is_animated, ) 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_error_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): await bot.send_sticker(chat_id) async def test_send_with_sticker(self, monkeypatch, bot, chat_id, sticker): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.json_parameters["sticker"] == sticker.file_id monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_sticker(sticker=sticker, chat_id=chat_id) @pytest.mark.parametrize("local_mode", [True, False]) async def test_send_sticker_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 test_flag = False file = data_file("telegram.jpg") expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag if local_mode: test_flag = data.get("sticker") == expected else: test_flag = isinstance(data.get("sticker"), InputFile) monkeypatch.setattr(bot, "_post", make_assertion) await bot.send_sticker(chat_id, file) assert test_flag finally: bot._local_mode = False @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_sticker_default_quote_parse_mode( self, default_bot, chat_id, sticker, 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_sticker( chat_id, sticker, reply_parameters=ReplyParameters(**kwargs) ) class TestStickerWithRequest(TestStickerBase): async def test_send_all_args(self, bot, chat_id, sticker_file, sticker): message = await bot.send_sticker( chat_id, sticker=sticker_file, disable_notification=False, protect_content=True ) assert isinstance(message.sticker, Sticker) assert isinstance(message.sticker.file_id, str) assert isinstance(message.sticker.file_unique_id, str) assert message.sticker.file_id assert message.sticker.file_unique_id assert message.sticker.width == sticker.width assert message.sticker.height == sticker.height assert message.sticker.is_animated == sticker.is_animated assert message.sticker.is_video == sticker.is_video assert message.sticker.file_size == sticker.file_size assert message.sticker.type == sticker.type assert message.has_protected_content # we need to be a premium TG user to send a premium sticker, so the below is not tested # assert message.sticker.premium_animation == sticker.premium_animation assert isinstance(message.sticker.thumbnail, PhotoSize) assert isinstance(message.sticker.thumbnail.file_id, str) assert isinstance(message.sticker.thumbnail.file_unique_id, str) assert message.sticker.thumbnail.file_id assert message.sticker.thumbnail.file_unique_id assert message.sticker.thumbnail.width == sticker.thumbnail.width assert message.sticker.thumbnail.height == sticker.thumbnail.height assert message.sticker.thumbnail.file_size == sticker.thumbnail.file_size async def test_get_and_download(self, bot, sticker, tmp_file): new_file = await bot.get_file(sticker.file_id) assert new_file.file_size == sticker.file_size assert new_file.file_unique_id == sticker.file_unique_id assert new_file.file_path.startswith("https://") await new_file.download_to_drive(tmp_file) assert tmp_file.is_file() async def test_resend(self, bot, chat_id, sticker): message = await bot.send_sticker(chat_id=chat_id, sticker=sticker.file_id) assert message.sticker == sticker async def test_send_with_emoji(self, bot, chat_id): message = await bot.send_sticker( chat_id=chat_id, sticker=data_file("telegram.jpg"), emoji=self.emoji ) assert message.sticker.emoji == self.emoji async def test_send_on_server_emoji(self, bot, chat_id): server_file_id = "CAADAQADHAADyIsGAAFZfq1bphjqlgI" message = await bot.send_sticker(chat_id=chat_id, sticker=server_file_id) sticker = message.sticker assert sticker.emoji == self.emoji async def test_send_from_url(self, bot, chat_id): message = await bot.send_sticker(chat_id=chat_id, sticker=self.sticker_file_url) sticker = message.sticker assert isinstance(message.sticker, Sticker) assert isinstance(message.sticker.file_id, str) assert isinstance(message.sticker.file_unique_id, str) assert message.sticker.file_id assert message.sticker.file_unique_id assert message.sticker.width == sticker.width assert message.sticker.height == sticker.height assert message.sticker.is_animated == sticker.is_animated assert message.sticker.is_video == sticker.is_video assert message.sticker.file_size == sticker.file_size assert message.sticker.type == sticker.type assert isinstance(message.sticker.thumbnail, PhotoSize) assert isinstance(message.sticker.thumbnail.file_id, str) assert isinstance(message.sticker.thumbnail.file_unique_id, str) assert message.sticker.thumbnail.file_id assert message.sticker.thumbnail.file_unique_id assert message.sticker.thumbnail.width == sticker.thumbnail.width assert message.sticker.thumbnail.height == sticker.thumbnail.height assert message.sticker.thumbnail.file_size == sticker.thumbnail.file_size @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_sticker_default_allow_sending_without_reply( self, default_bot, chat_id, sticker, 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_sticker( chat_id, sticker, 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_sticker( chat_id, sticker, reply_to_message_id=reply_to_message.message_id ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_sticker( chat_id, sticker, reply_to_message_id=reply_to_message.message_id ) @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_sticker_default_protect_content(self, chat_id, sticker, default_bot): tasks = asyncio.gather( default_bot.send_sticker(chat_id, sticker), default_bot.send_sticker(chat_id, sticker, protect_content=False), ) protected, unprotected = await tasks assert protected.has_protected_content assert not unprotected.has_protected_content async def test_premium_animation(self, bot): # testing animation sucks a bit since we can't create a premium sticker. What we can do is # get a sticker set which includes a premium sticker and check that specific one. premium_sticker_set = await bot.get_sticker_set("Flame") # the first one to appear here is a sticker with unique file id of AQADOBwAAifPOElr # this could change in the future ofc. premium_sticker = premium_sticker_set.stickers[20] assert premium_sticker.premium_animation.file_unique_id == "AQADOBwAAifPOElr" assert isinstance(premium_sticker.premium_animation.file_id, str) assert premium_sticker.premium_animation.file_id premium_sticker_dict = { "file_unique_id": "AQADOBwAAifPOElr", "file_id": premium_sticker.premium_animation.file_id, "file_size": premium_sticker.premium_animation.file_size, } assert premium_sticker.premium_animation.to_dict() == premium_sticker_dict async def test_custom_emoji(self, bot): # testing custom emoji stickers is as much of an annoyance as the premium animation, see # in test_premium_animation custom_emoji_set = await bot.get_sticker_set("PTBStaticEmojiTestPack") # the first one to appear here is a sticker with unique file id of AQADjBsAAkKD0Uty # this could change in the future ofc. custom_emoji_sticker = custom_emoji_set.stickers[0] assert custom_emoji_sticker.custom_emoji_id == "6046140249875156202" async def test_custom_emoji_sticker(self, bot): # we use the same ID as in test_custom_emoji emoji_sticker_list = await bot.get_custom_emoji_stickers(["6046140249875156202"]) assert emoji_sticker_list[0].emoji == "😎" assert emoji_sticker_list[0].height == 100 assert emoji_sticker_list[0].width == 100 assert not emoji_sticker_list[0].is_animated assert not emoji_sticker_list[0].is_video assert emoji_sticker_list[0].set_name == "PTBStaticEmojiTestPack" assert emoji_sticker_list[0].type == Sticker.CUSTOM_EMOJI assert emoji_sticker_list[0].custom_emoji_id == "6046140249875156202" assert emoji_sticker_list[0].thumbnail.width == 100 assert emoji_sticker_list[0].thumbnail.height == 100 assert emoji_sticker_list[0].thumbnail.file_size == 3614 assert emoji_sticker_list[0].thumbnail.file_unique_id == "AQAD6gwAAoY06FNy" assert emoji_sticker_list[0].file_size == 3678 assert emoji_sticker_list[0].file_unique_id == "AgAD6gwAAoY06FM" async def test_error_send_empty_file(self, bot, chat_id): with Path(os.devnull).open("rb") as file, pytest.raises(TelegramError): await bot.send_sticker(chat_id, file) async def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): await bot.send_sticker(chat_id, "") @pytest.fixture() async def sticker_set(bot): ss = await bot.get_sticker_set(f"test_by_{bot.username}") if len(ss.stickers) > 100: try: for i in range(1, 50): await bot.delete_sticker_from_set(ss.stickers[-i].file_id) except BadRequest as e: if e.message == "Stickerset_not_modified": return ss raise Exception("stickerset is growing too large.") from None return ss @pytest.fixture() async def animated_sticker_set(bot): ss = await bot.get_sticker_set(f"animated_test_by_{bot.username}") if len(ss.stickers) > 100: try: for i in range(1, 50): await bot.delete_sticker_from_set(ss.stickers[-i].file_id) except BadRequest as e: if e.message == "Stickerset_not_modified": return ss raise Exception("stickerset is growing too large.") from None return ss @pytest.fixture() async def video_sticker_set(bot): ss = await bot.get_sticker_set(f"video_test_by_{bot.username}") if len(ss.stickers) > 100: try: for i in range(1, 50): await bot.delete_sticker_from_set(ss.stickers[-i].file_id) except BadRequest as e: if e.message == "Stickerset_not_modified": return ss raise Exception("stickerset is growing too large.") from None return ss @pytest.fixture() def sticker_set_thumb_file(): with data_file("sticker_set_thumb.png").open("rb") as file: yield file class TestStickerSetBase: title = "Test stickers" stickers = [Sticker("file_id", "file_un_id", 512, 512, True, True, Sticker.REGULAR)] name = "NOTAREALNAME" sticker_type = Sticker.REGULAR contains_masks = True class TestStickerSetWithoutRequest(TestStickerSetBase): def test_slot_behaviour(self): inst = StickerSet("this", "is", self.stickers, "not") 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, sticker): name = f"test_by_{bot.username}" json_dict = { "name": name, "title": self.title, "stickers": [x.to_dict() for x in self.stickers], "thumbnail": sticker.thumbnail.to_dict(), "sticker_type": self.sticker_type, "contains_masks": self.contains_masks, } sticker_set = StickerSet.de_json(json_dict, bot) assert sticker_set.name == name assert sticker_set.title == self.title assert sticker_set.stickers == tuple(self.stickers) assert sticker_set.thumbnail == sticker.thumbnail assert sticker_set.sticker_type == self.sticker_type assert sticker_set.api_kwargs == {"contains_masks": self.contains_masks} def test_sticker_set_to_dict(self, sticker_set): sticker_set_dict = sticker_set.to_dict() assert isinstance(sticker_set_dict, dict) assert sticker_set_dict["name"] == sticker_set.name assert sticker_set_dict["title"] == sticker_set.title assert sticker_set_dict["stickers"][0] == sticker_set.stickers[0].to_dict() assert sticker_set_dict["thumbnail"] == sticker_set.thumbnail.to_dict() assert sticker_set_dict["sticker_type"] == sticker_set.sticker_type def test_equality(self): a = StickerSet( self.name, self.title, self.stickers, self.sticker_type, ) b = StickerSet( self.name, self.title, self.stickers, self.sticker_type, ) c = StickerSet(self.name, "title", [], Sticker.CUSTOM_EMOJI) d = StickerSet( "blah", self.title, self.stickers, self.sticker_type, ) e = Audio(self.name, "", 0, None, 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) assert a != e assert hash(a) != hash(e) @pytest.mark.parametrize("local_mode", [True, False]) async def test_upload_sticker_file_local_files( self, monkeypatch, bot, chat_id, local_mode, recwarn ): try: bot._local_mode = local_mode # For just test that the correct paths are passed as we have no local bot API set up test_flag = False file = data_file("telegram.jpg") expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = ( data.get("sticker") == expected if local_mode else isinstance(data.get("sticker"), InputFile) ) monkeypatch.setattr(bot, "_post", make_assertion) await bot.upload_sticker_file( chat_id, sticker=file, sticker_format=StickerFormat.STATIC ) assert test_flag finally: bot._local_mode = False @pytest.mark.parametrize("local_mode", [True, False]) async def test_create_new_sticker_set_local_files( self, monkeypatch, bot, chat_id, local_mode, ): monkeypatch.setattr(bot, "_local_mode", local_mode) # For just test that the correct paths are passed as we have no local bot API set up test_flag = False file = data_file("telegram.jpg") # always assumed to be local mode because we don't have access to local_mode setting # within InputFile expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get("stickers")[0].sticker == expected monkeypatch.setattr(bot, "_post", make_assertion) await bot.create_new_sticker_set( chat_id, "name", "title", stickers=[InputSticker(file, emoji_list=["emoji"], format=StickerFormat.STATIC)], ) assert test_flag async def test_create_new_sticker_all_params(self, monkeypatch, bot, chat_id, mask_position): async def make_assertion(_, data, *args, **kwargs): assert data["user_id"] == chat_id assert data["name"] == "name" assert data["title"] == "title" assert data["stickers"] == ["wow.png", "wow.tgs", "wow.webp"] assert data["sticker_format"] == "static" assert data["needs_repainting"] is True monkeypatch.setattr(bot, "_post", make_assertion) await bot.create_new_sticker_set( chat_id, "name", "title", stickers=["wow.png", "wow.tgs", "wow.webp"], sticker_format=StickerFormat.STATIC, needs_repainting=True, ) @pytest.mark.parametrize("local_mode", [True, False]) async def test_add_sticker_to_set_local_files(self, monkeypatch, bot, chat_id, local_mode): monkeypatch.setattr(bot, "_local_mode", local_mode) # For just test that the correct paths are passed as we have no local bot API set up test_flag = False file = data_file("telegram.jpg") # always assumed to be local mode because we don't have access to local_mode setting # within InputFile expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get("sticker").sticker == expected monkeypatch.setattr(bot, "_post", make_assertion) await bot.add_sticker_to_set( chat_id, "name", sticker=InputSticker(sticker=file, emoji_list=["this"], format="static"), ) assert test_flag @pytest.mark.parametrize("local_mode", [True, False]) async def test_set_sticker_set_thumbnail_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 test_flag = False file = data_file("telegram.jpg") expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag if local_mode: test_flag = data.get("thumbnail") == expected else: test_flag = isinstance(data.get("thumbnail"), InputFile) monkeypatch.setattr(bot, "_post", make_assertion) await bot.set_sticker_set_thumbnail("name", chat_id, thumbnail=file, format="static") assert test_flag finally: bot._local_mode = False async def test_get_file_instance_method(self, monkeypatch, sticker): async def make_assertion(*_, **kwargs): return kwargs["file_id"] == sticker.file_id assert check_shortcut_signature(Sticker.get_file, Bot.get_file, ["file_id"], []) assert await check_shortcut_call(sticker.get_file, sticker.get_bot(), "get_file") assert await check_defaults_handling(sticker.get_file, sticker.get_bot()) monkeypatch.setattr(sticker.get_bot(), "get_file", make_assertion) assert await sticker.get_file() async def test_create_new_sticker_set_format_arg_depr( self, bot, chat_id, sticker_file, monkeypatch ): async def make_assertion(*_, **kwargs): pass monkeypatch.setattr(bot, "_post", make_assertion) with pytest.warns(PTBDeprecationWarning, match="`sticker_format` is deprecated"): await bot.create_new_sticker_set( chat_id, "name", "title", stickers=sticker_file, sticker_format="static", ) async def test_deprecation_creation_args(self, recwarn): with pytest.warns(PTBDeprecationWarning, match="The parameters `is_animated` and ") as w: StickerSet("name", "title", [], "static", is_animated=True) assert w[0].filename == __file__, "wrong stacklevel!" @pytest.mark.xdist_group("stickerset") class TestStickerSetWithRequest: async def test_create_sticker_set( self, bot, chat_id, sticker_file, animated_sticker_file, video_sticker_file ): """Creates the sticker set (if needed) which is required for tests. Make sure that this test comes before the tests that actually use the sticker sets! """ test_by = f"test_by_{bot.username}" for sticker_set in [test_by, f"animated_{test_by}", f"video_{test_by}"]: try: ss = await bot.get_sticker_set(sticker_set) assert isinstance(ss, StickerSet) except BadRequest as e: if not e.message == "Stickerset_invalid": raise e if sticker_set.startswith(test_by): s = await bot.create_new_sticker_set( chat_id, name=sticker_set, title="Sticker Test", stickers=[ InputSticker( sticker_file, emoji_list=["😄"], format=StickerFormat.STATIC ) ], ) assert s elif sticker_set.startswith("animated"): a = await bot.create_new_sticker_set( chat_id, name=sticker_set, title="Animated Test", stickers=[ InputSticker( animated_sticker_file, emoji_list=["😄"], format=StickerFormat.ANIMATED, ) ], ) assert a elif sticker_set.startswith("video"): v = await bot.create_new_sticker_set( chat_id, name=sticker_set, title="Video Test", stickers=[ InputSticker( video_sticker_file, emoji_list=["😄"], format=StickerFormat.VIDEO ) ], ) assert v async def test_delete_sticker_set(self, bot, chat_id, sticker_file): # there is currently an issue in the API where this function claims it successfully # creates an already deleted sticker set while it does not. This happens when calling it # too soon after deleting the set. This then leads to delete_sticker_set failing since the # pack does not exist. Making the name random prevents this issue. name = f"{''.join(random.choices(string.ascii_lowercase, k=5))}_temp_set_by_{bot.username}" assert await bot.create_new_sticker_set( chat_id, name=name, title="Stickerset delete Test", stickers=[InputSticker(sticker_file, emoji_list=["😄"], format="static")], ) # this prevents a second issue when calling delete too soon after creating the set leads # to it failing as well await asyncio.sleep(1) assert await bot.delete_sticker_set(name) async def test_set_custom_emoji_sticker_set_thumbnail( self, bot, chat_id, animated_sticker_file ): ss_name = f"custom_emoji_set_by_{bot.username}" try: ss = await bot.get_sticker_set(ss_name) assert ss.sticker_type == Sticker.CUSTOM_EMOJI except BadRequest: assert await bot.create_new_sticker_set( chat_id, name=ss_name, title="Custom Emoji Sticker Set", stickers=[ InputSticker( animated_sticker_file, emoji_list=["😄"], format=StickerFormat.ANIMATED ) ], sticker_type=Sticker.CUSTOM_EMOJI, ) assert await bot.set_custom_emoji_sticker_set_thumbnail(ss_name, "") # Test add_sticker_to_set async def test_bot_methods_1_png(self, bot, chat_id, sticker_file): with data_file("telegram_sticker.png").open("rb") as f: # chat_id was hardcoded as 95205500 but it stopped working for some reason file = await bot.upload_sticker_file( chat_id, sticker=f, sticker_format=StickerFormat.STATIC ) assert file await asyncio.sleep(1) tasks = asyncio.gather( bot.add_sticker_to_set( chat_id, f"test_by_{bot.username}", sticker=InputSticker( sticker=file.file_id, emoji_list=["😄"], format=StickerFormat.STATIC ), ), bot.add_sticker_to_set( # Also test with file input and mask chat_id, f"test_by_{bot.username}", sticker=InputSticker( sticker=sticker_file, emoji_list=["😄"], mask_position=MaskPosition(MaskPosition.EYES, -1, 1, 2), format=StickerFormat.STATIC, ), ), ) assert all(await tasks) async def test_bot_methods_1_tgs(self, bot, chat_id): await asyncio.sleep(1) assert await bot.add_sticker_to_set( chat_id, f"animated_test_by_{bot.username}", sticker=InputSticker( sticker=data_file("telegram_animated_sticker.tgs").open("rb"), emoji_list=["😄"], format=StickerFormat.ANIMATED, ), ) async def test_bot_methods_1_webm(self, bot, chat_id): await asyncio.sleep(1) with data_file("telegram_video_sticker.webm").open("rb") as f: assert await bot.add_sticker_to_set( chat_id, f"video_test_by_{bot.username}", sticker=InputSticker(sticker=f, emoji_list=["🤔"], format=StickerFormat.VIDEO), ) # Test set_sticker_position_in_set async def test_bot_methods_2_png(self, bot, sticker_set): await asyncio.sleep(1) file_id = sticker_set.stickers[0].file_id assert await bot.set_sticker_position_in_set(file_id, 1) async def test_bot_methods_2_tgs(self, bot, animated_sticker_set): await asyncio.sleep(1) file_id = animated_sticker_set.stickers[0].file_id assert await bot.set_sticker_position_in_set(file_id, 1) async def test_bot_methods_2_webm(self, bot, video_sticker_set): await asyncio.sleep(1) file_id = video_sticker_set.stickers[0].file_id assert await bot.set_sticker_position_in_set(file_id, 1) # Test set_sticker_set_thumb async def test_bot_methods_3_png(self, bot, chat_id, sticker_set_thumb_file): await asyncio.sleep(1) assert await bot.set_sticker_set_thumbnail( f"test_by_{bot.username}", chat_id, format="static", thumbnail=sticker_set_thumb_file ) async def test_bot_methods_3_tgs( self, bot, chat_id, animated_sticker_file, animated_sticker_set ): await asyncio.sleep(1) animated_test = f"animated_test_by_{bot.username}" file_id = animated_sticker_set.stickers[-1].file_id tasks = asyncio.gather( bot.set_sticker_set_thumbnail( animated_test, chat_id, "animated", thumbnail=animated_sticker_file, ), bot.set_sticker_set_thumbnail(animated_test, chat_id, "animated", thumbnail=file_id), ) assert all(await tasks) # TODO: Try the below by creating a custom .webm and not by downloading another pack's thumb @pytest.mark.skip( "Skipped for now since Telegram throws a 'File is too big' error " "regardless of the .webm file size." ) def test_bot_methods_3_webm(self, bot, chat_id, video_sticker_file, video_sticker_set): pass # Test delete_sticker_from_set async def test_bot_methods_4_png(self, bot, sticker_set): if len(sticker_set.stickers) <= 1: pytest.skip("Sticker set only has one sticker, deleting it will delete the set.") await asyncio.sleep(1) file_id = sticker_set.stickers[-1].file_id assert await bot.delete_sticker_from_set(file_id) async def test_bot_methods_4_tgs(self, bot, animated_sticker_set): if len(animated_sticker_set.stickers) <= 1: pytest.skip("Sticker set only has one sticker, deleting it will delete the set.") await asyncio.sleep(1) file_id = animated_sticker_set.stickers[-1].file_id assert await bot.delete_sticker_from_set(file_id) async def test_bot_methods_4_webm(self, bot, video_sticker_set): if len(video_sticker_set.stickers) <= 1: pytest.skip("Sticker set only has one sticker, deleting it will delete the set.") await asyncio.sleep(1) file_id = video_sticker_set.stickers[-1].file_id assert await bot.delete_sticker_from_set(file_id) # Test set_sticker_emoji_list. It has been found that the first emoji in the list is the one # that is used in `Sticker.emoji` as string (which is returned in `get_sticker_set`) async def test_bot_methods_5_png(self, bot, sticker_set): file_id = sticker_set.stickers[-1].file_id assert await bot.set_sticker_emoji_list(file_id, ["😔", "😟"]) ss = await bot.get_sticker_set(f"test_by_{bot.username}") assert ss.stickers[-1].emoji == "😔" async def test_bot_methods_5_tgs(self, bot, animated_sticker_set): file_id = animated_sticker_set.stickers[-1].file_id assert await bot.set_sticker_emoji_list(file_id, ["😔", "😟"]) ss = await bot.get_sticker_set(f"animated_test_by_{bot.username}") assert ss.stickers[-1].emoji == "😔" async def test_bot_methods_5_webm(self, bot, video_sticker_set): file_id = video_sticker_set.stickers[-1].file_id assert await bot.set_sticker_emoji_list(file_id, ["😔", "😟"]) ss = await bot.get_sticker_set(f"video_test_by_{bot.username}") assert ss.stickers[-1].emoji == "😔" # Test set_sticker_set_title. async def test_bot_methods_6_png(self, bot): assert await bot.set_sticker_set_title(f"test_by_{bot.username}", "new title") ss = await bot.get_sticker_set(f"test_by_{bot.username}") assert ss.title == "new title" async def test_bot_methods_6_tgs(self, bot): assert await bot.set_sticker_set_title(f"animated_test_by_{bot.username}", "new title") ss = await bot.get_sticker_set(f"animated_test_by_{bot.username}") assert ss.title == "new title" async def test_bot_methods_6_webm(self, bot): assert await bot.set_sticker_set_title(f"video_test_by_{bot.username}", "new title") ss = await bot.get_sticker_set(f"video_test_by_{bot.username}") assert ss.title == "new title" # Test set_sticker_keywords. No way to find out the set keywords on a sticker after setting it. async def test_bot_methods_7_png(self, bot, sticker_set): file_id = sticker_set.stickers[-1].file_id assert await bot.set_sticker_keywords(file_id, ["test", "test2"]) async def test_bot_methods_7_tgs(self, bot, animated_sticker_set): file_id = animated_sticker_set.stickers[-1].file_id assert await bot.set_sticker_keywords(file_id, ["test", "test2"]) async def test_bot_methods_7_webm(self, bot, video_sticker_set): file_id = video_sticker_set.stickers[-1].file_id assert await bot.set_sticker_keywords(file_id, ["test", "test2"]) async def test_bot_methods_8_png(self, bot, sticker_set, sticker_file): file_id = sticker_set.stickers[-1].file_id assert await bot.replace_sticker_in_set( bot.id, f"test_by_{bot.username}", file_id, sticker=InputSticker( sticker=sticker_file, emoji_list=["😄"], format=StickerFormat.STATIC, ), ) @pytest.fixture(scope="module") def mask_position(): return MaskPosition( TestMaskPositionBase.point, TestMaskPositionBase.x_shift, TestMaskPositionBase.y_shift, TestMaskPositionBase.scale, ) class TestMaskPositionBase: point = MaskPosition.EYES x_shift = -1 y_shift = 1 scale = 2 class TestMaskPositionWithoutRequest(TestMaskPositionBase): def test_slot_behaviour(self, mask_position): inst = mask_position 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_mask_position_de_json(self, bot): json_dict = { "point": self.point, "x_shift": self.x_shift, "y_shift": self.y_shift, "scale": self.scale, } mask_position = MaskPosition.de_json(json_dict, bot) assert mask_position.api_kwargs == {} assert mask_position.point == self.point assert mask_position.x_shift == self.x_shift assert mask_position.y_shift == self.y_shift assert mask_position.scale == self.scale def test_mask_position_to_dict(self, mask_position): mask_position_dict = mask_position.to_dict() assert isinstance(mask_position_dict, dict) assert mask_position_dict["point"] == mask_position.point assert mask_position_dict["x_shift"] == mask_position.x_shift assert mask_position_dict["y_shift"] == mask_position.y_shift assert mask_position_dict["scale"] == mask_position.scale def test_equality(self): a = MaskPosition(self.point, self.x_shift, self.y_shift, self.scale) b = MaskPosition(self.point, self.x_shift, self.y_shift, self.scale) c = MaskPosition(MaskPosition.FOREHEAD, self.x_shift, self.y_shift, self.scale) d = MaskPosition(self.point, 0, 0, self.scale) e = Audio("", "", 0, None, 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) assert a != e assert hash(a) != hash(e) class TestMaskPositionWithRequest(TestMaskPositionBase): async def test_create_new_mask_sticker_set(self, bot, chat_id, sticker_file, mask_position): name = f"masks_by_{bot.username}" try: ss = await bot.get_sticker_set(name) assert isinstance(ss, StickerSet) except BadRequest as e: if not e.message == "Stickerset_invalid": raise e sticker_set = await bot.create_new_sticker_set( chat_id, name, "Mask Stickers", stickers=[ InputSticker( sticker=sticker_file, emoji_list=["😔"], mask_position=mask_position, keywords=["sad"], format=StickerFormat.STATIC, ) ], sticker_type=Sticker.MASK, ) assert sticker_set async def test_set_sticker_mask_position(self, bot): ss = await bot.get_sticker_set(f"masks_by_{bot.username}") m = MaskPosition(MaskPosition.FOREHEAD, 0, 0, 4) assert await bot.set_sticker_mask_position(ss.stickers[-1].file_id, m) ss = await bot.get_sticker_set(f"masks_by_{bot.username}") assert ss.stickers[-1].mask_position == m python-telegram-bot-21.1.1/tests/_files/test_venue.py000066400000000000000000000205421460724040100226340ustar00rootroot00000000000000#!/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 pytest from telegram import Location, ReplyParameters, Venue from telegram.constants import ParseMode from telegram.error import BadRequest from telegram.request import RequestData from tests.auxil.build_messages import make_message from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def venue(): return Venue( TestVenueBase.location, TestVenueBase.title, TestVenueBase.address, foursquare_id=TestVenueBase.foursquare_id, foursquare_type=TestVenueBase.foursquare_type, google_place_id=TestVenueBase.google_place_id, google_place_type=TestVenueBase.google_place_type, ) class TestVenueBase: location = Location(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" class TestVenueWithoutRequest(TestVenueBase): def test_slot_behaviour(self, venue): for attr in venue.__slots__: assert getattr(venue, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(venue)) == len(set(mro_slots(venue))), "duplicate slot" def test_de_json(self, bot): json_dict = { "location": self.location.to_dict(), "title": self.title, "address": self.address, "foursquare_id": self.foursquare_id, "foursquare_type": self.foursquare_type, "google_place_id": self.google_place_id, "google_place_type": self.google_place_type, } venue = Venue.de_json(json_dict, bot) assert venue.api_kwargs == {} assert venue.location == self.location assert venue.title == self.title assert venue.address == self.address assert venue.foursquare_id == self.foursquare_id assert venue.foursquare_type == self.foursquare_type assert venue.google_place_id == self.google_place_id assert venue.google_place_type == self.google_place_type def test_to_dict(self, venue): venue_dict = venue.to_dict() assert isinstance(venue_dict, dict) assert venue_dict["location"] == venue.location.to_dict() assert venue_dict["title"] == venue.title assert venue_dict["address"] == venue.address assert venue_dict["foursquare_id"] == venue.foursquare_id assert venue_dict["foursquare_type"] == venue.foursquare_type assert venue_dict["google_place_id"] == venue.google_place_id assert venue_dict["google_place_type"] == venue.google_place_type def test_equality(self): a = Venue(Location(0, 0), self.title, self.address) b = Venue(Location(0, 0), self.title, self.address) c = Venue(Location(0, 0), self.title, "") d = Venue(Location(0, 1), self.title, self.address) d2 = Venue(Location(0, 0), "", self.address) 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 != d2 assert hash(a) != hash(d2) async def test_send_venue_without_required(self, bot, chat_id): with pytest.raises(ValueError, match="Either venue or latitude, longitude, address and"): await bot.send_venue(chat_id=chat_id) async def test_send_venue_mutually_exclusive(self, bot, chat_id, venue): with pytest.raises(ValueError, match="Not both"): await bot.send_venue( chat_id=chat_id, latitude=1, longitude=1, address="address", title="title", venue=venue, ) async def test_send_with_venue(self, monkeypatch, bot, chat_id, venue): async def make_assertion(url, request_data: RequestData, *args, **kwargs): data = request_data.json_parameters return ( data["longitude"] == str(self.location.longitude) and data["latitude"] == str(self.location.latitude) and data["title"] == self.title and data["address"] == self.address and data["foursquare_id"] == self.foursquare_id and data["foursquare_type"] == self.foursquare_type and data["google_place_id"] == self.google_place_id and data["google_place_type"] == self.google_place_type ) monkeypatch.setattr(bot.request, "post", make_assertion) message = await bot.send_venue(chat_id, venue=venue) assert message @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_venue_default_quote_parse_mode( self, default_bot, chat_id, venue, 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_venue( chat_id, venue=venue, reply_parameters=ReplyParameters(**kwargs) ) class TestVenueWithRequest(TestVenueBase): @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_venue_default_allow_sending_without_reply( self, default_bot, chat_id, venue, 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_venue( chat_id, venue=venue, 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_venue( chat_id, venue=venue, reply_to_message_id=reply_to_message.message_id ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_venue( chat_id, venue=venue, reply_to_message_id=reply_to_message.message_id ) @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_venue_default_protect_content(self, default_bot, chat_id, venue): tasks = asyncio.gather( default_bot.send_venue(chat_id, venue=venue), default_bot.send_venue(chat_id, venue=venue, protect_content=False), ) protected, unprotected = await tasks assert protected.has_protected_content assert not unprotected.has_protected_content python-telegram-bot-21.1.1/tests/_files/test_video.py000066400000000000000000000401231460724040100226150ustar00rootroot00000000000000#!/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 os from pathlib import Path import pytest from telegram import Bot, InputFile, MessageEntity, PhotoSize, ReplyParameters, Video, Voice from telegram.constants import ParseMode from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData 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.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture() def video_file(): with data_file("telegram.mp4").open("rb") as f: yield f @pytest.fixture(scope="module") async def video(bot, chat_id): with data_file("telegram.mp4").open("rb") as f: return (await bot.send_video(chat_id, video=f, read_timeout=50)).video class TestVideoBase: width = 360 height = 640 duration = 5 file_size = 326534 mime_type = "video/mp4" supports_streaming = True file_name = "telegram.mp4" thumb_width = 180 thumb_height = 320 thumb_file_size = 1767 caption = "VideoTest - *Caption*" video_file_url = "https://python-telegram-bot.org/static/testfiles/telegram.mp4" video_file_id = "5a3128a4d2a04750b5b58397f3b5e812" video_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" class TestVideoWithoutRequest(TestVideoBase): def test_slot_behaviour(self, video): for attr in video.__slots__: assert getattr(video, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(video)) == len(set(mro_slots(video))), "duplicate slot" def test_creation(self, video): # Make sure file has been uploaded. assert isinstance(video, Video) assert isinstance(video.file_id, str) assert isinstance(video.file_unique_id, str) assert video.file_id assert video.file_unique_id assert isinstance(video.thumbnail, PhotoSize) assert isinstance(video.thumbnail.file_id, str) assert isinstance(video.thumbnail.file_unique_id, str) assert video.thumbnail.file_id assert video.thumbnail.file_unique_id def test_expected_values(self, video): assert video.width == self.width assert video.height == self.height assert video.duration == self.duration assert video.file_size == self.file_size assert video.mime_type == self.mime_type def test_de_json(self, bot): json_dict = { "file_id": self.video_file_id, "file_unique_id": self.video_file_unique_id, "width": self.width, "height": self.height, "duration": self.duration, "mime_type": self.mime_type, "file_size": self.file_size, "file_name": self.file_name, } json_video = Video.de_json(json_dict, bot) assert json_video.api_kwargs == {} assert json_video.file_id == self.video_file_id assert json_video.file_unique_id == self.video_file_unique_id assert json_video.width == self.width assert json_video.height == self.height assert json_video.duration == self.duration assert json_video.mime_type == self.mime_type assert json_video.file_size == self.file_size assert json_video.file_name == self.file_name def test_to_dict(self, video): video_dict = video.to_dict() assert isinstance(video_dict, dict) assert video_dict["file_id"] == video.file_id assert video_dict["file_unique_id"] == video.file_unique_id assert video_dict["width"] == video.width assert video_dict["height"] == video.height assert video_dict["duration"] == video.duration assert video_dict["mime_type"] == video.mime_type assert video_dict["file_size"] == video.file_size assert video_dict["file_name"] == video.file_name def test_equality(self, video): a = Video(video.file_id, video.file_unique_id, self.width, self.height, self.duration) b = Video("", video.file_unique_id, self.width, self.height, self.duration) c = Video(video.file_id, video.file_unique_id, 0, 0, 0) d = Video("", "", self.width, self.height, self.duration) e = Voice(video.file_id, video.file_unique_id, self.duration) 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_error_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): await bot.send_video(chat_id=chat_id) async def test_send_with_video(self, monkeypatch, bot, chat_id, video): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.json_parameters["video"] == video.file_id monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_video(chat_id, video=video) async def test_send_video_custom_filename(self, bot, chat_id, video_file, monkeypatch): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return next(iter(request_data.multipart_data.values()))[0] == "custom_filename" monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_video(chat_id, video_file, filename="custom_filename") @pytest.mark.parametrize("local_mode", [True, False]) async def test_send_video_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 test_flag = False file = data_file("telegram.jpg") expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag if local_mode: test_flag = data.get("video") == expected and data.get("thumbnail") == expected else: test_flag = isinstance(data.get("video"), InputFile) and isinstance( data.get("thumbnail"), InputFile ) monkeypatch.setattr(bot, "_post", make_assertion) await bot.send_video(chat_id, file, thumbnail=file) assert test_flag finally: bot._local_mode = False async def test_get_file_instance_method(self, monkeypatch, video): async def make_assertion(*_, **kwargs): return kwargs["file_id"] == video.file_id assert check_shortcut_signature(Video.get_file, Bot.get_file, ["file_id"], []) assert await check_shortcut_call(video.get_file, video.get_bot(), "get_file") assert await check_defaults_handling(video.get_file, video.get_bot()) monkeypatch.setattr(video.get_bot(), "get_file", make_assertion) assert await video.get_file() @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_video_default_quote_parse_mode( self, default_bot, chat_id, video, 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_video(chat_id, video, reply_parameters=ReplyParameters(**kwargs)) class TestVideoWithRequest(TestVideoBase): async def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file): message = await bot.send_video( chat_id, video_file, duration=self.duration, caption=self.caption, supports_streaming=self.supports_streaming, disable_notification=False, protect_content=True, width=video.width, height=video.height, parse_mode="Markdown", thumbnail=thumb_file, has_spoiler=True, ) assert isinstance(message.video, Video) assert isinstance(message.video.file_id, str) assert isinstance(message.video.file_unique_id, str) assert message.video.file_id assert message.video.file_unique_id assert message.video.width == video.width assert message.video.height == video.height assert message.video.duration == video.duration assert message.video.file_size == video.file_size assert message.caption == self.caption.replace("*", "") assert message.video.thumbnail.file_size == self.thumb_file_size assert message.video.thumbnail.width == self.thumb_width assert message.video.thumbnail.height == self.thumb_height assert message.video.file_name == self.file_name assert message.has_protected_content assert message.has_media_spoiler async def test_get_and_download(self, bot, video, chat_id, tmp_file): new_file = await bot.get_file(video.file_id) assert new_file.file_size == self.file_size assert new_file.file_unique_id == video.file_unique_id assert new_file.file_path.startswith("https://") await new_file.download_to_drive(tmp_file) assert tmp_file.is_file() async def test_send_mp4_file_url(self, bot, chat_id, video): message = await bot.send_video(chat_id, self.video_file_url, caption=self.caption) assert isinstance(message.video, Video) assert isinstance(message.video.file_id, str) assert isinstance(message.video.file_unique_id, str) assert message.video.file_id assert message.video.file_unique_id assert message.video.width == video.width assert message.video.height == video.height assert message.video.duration == video.duration assert message.video.file_size == video.file_size assert isinstance(message.video.thumbnail, PhotoSize) assert isinstance(message.video.thumbnail.file_id, str) assert isinstance(message.video.thumbnail.file_unique_id, str) assert message.video.thumbnail.file_id assert message.video.thumbnail.file_unique_id assert message.video.thumbnail.width == 51 # This seems odd that it's not self.thumb_width assert message.video.thumbnail.height == 90 # Ditto assert message.video.thumbnail.file_size == 645 # same assert message.caption == self.caption async def test_send_video_caption_entities(self, bot, chat_id, video): 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_video( chat_id, video, caption=test_string, caption_entities=entities ) assert message.caption == test_string assert message.caption_entities == tuple(entities) async def test_resend(self, bot, chat_id, video): message = await bot.send_video(chat_id, video.file_id) assert message.video == video @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_video_default_parse_mode_1(self, default_bot, chat_id, video): test_string = "Italic Bold Code" test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_video(chat_id, video, caption=test_markdown_string) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_video_default_parse_mode_2(self, default_bot, chat_id, video): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_video( chat_id, video, caption=test_markdown_string, parse_mode=None ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_video_default_parse_mode_3(self, default_bot, chat_id, video): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_video( chat_id, video, caption=test_markdown_string, parse_mode="HTML" ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_video_default_protect_content(self, chat_id, default_bot, video): tasks = asyncio.gather( default_bot.send_video(chat_id, video), default_bot.send_video(chat_id, video, protect_content=False), ) protected, unprotected = await tasks assert protected.has_protected_content assert not unprotected.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_video_default_allow_sending_without_reply( self, default_bot, chat_id, video, 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_video( chat_id, video, 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_video( chat_id, video, reply_to_message_id=reply_to_message.message_id ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_video( chat_id, video, reply_to_message_id=reply_to_message.message_id ) async def test_error_send_empty_file(self, bot, chat_id): with Path(os.devnull).open("rb") as file, pytest.raises(TelegramError): await bot.send_video(chat_id, file) async def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): await bot.send_video(chat_id, "") python-telegram-bot-21.1.1/tests/_files/test_videonote.py000066400000000000000000000310171460724040100235050ustar00rootroot00000000000000#!/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 os from pathlib import Path import pytest from telegram import Bot, InputFile, PhotoSize, ReplyParameters, VideoNote, Voice from telegram.constants import ParseMode from telegram.error import BadRequest, TelegramError from telegram.request import RequestData 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.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture() def video_note_file(): with data_file("telegram2.mp4").open("rb") as f: yield f @pytest.fixture(scope="module") async def video_note(bot, chat_id): with data_file("telegram2.mp4").open("rb") as f: return (await bot.send_video_note(chat_id, video_note=f, read_timeout=50)).video_note class TestVideoNoteBase: length = 240 duration = 3 file_size = 132084 thumb_width = 240 thumb_height = 240 thumb_file_size = 11547 caption = "VideoNoteTest - Caption" videonote_file_id = "5a3128a4d2a04750b5b58397f3b5e812" videonote_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" class TestVideoNoteWithoutRequest(TestVideoNoteBase): def test_slot_behaviour(self, video_note): for attr in video_note.__slots__: assert getattr(video_note, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(video_note)) == len(set(mro_slots(video_note))), "duplicate slot" def test_creation(self, video_note): # Make sure file has been uploaded. assert isinstance(video_note, VideoNote) assert isinstance(video_note.file_id, str) assert isinstance(video_note.file_unique_id, str) assert video_note.file_id assert video_note.file_unique_id assert isinstance(video_note.thumbnail, PhotoSize) assert isinstance(video_note.thumbnail.file_id, str) assert isinstance(video_note.thumbnail.file_unique_id, str) assert video_note.thumbnail.file_id assert video_note.thumbnail.file_unique_id def test_expected_values(self, video_note): assert video_note.length == self.length assert video_note.duration == self.duration assert video_note.file_size == self.file_size def test_de_json(self, bot): json_dict = { "file_id": self.videonote_file_id, "file_unique_id": self.videonote_file_unique_id, "length": self.length, "duration": self.duration, "file_size": self.file_size, } json_video_note = VideoNote.de_json(json_dict, bot) assert json_video_note.api_kwargs == {} assert json_video_note.file_id == self.videonote_file_id assert json_video_note.file_unique_id == self.videonote_file_unique_id assert json_video_note.length == self.length assert json_video_note.duration == self.duration assert json_video_note.file_size == self.file_size def test_to_dict(self, video_note): video_note_dict = video_note.to_dict() assert isinstance(video_note_dict, dict) assert video_note_dict["file_id"] == video_note.file_id assert video_note_dict["file_unique_id"] == video_note.file_unique_id assert video_note_dict["length"] == video_note.length assert video_note_dict["duration"] == video_note.duration assert video_note_dict["file_size"] == video_note.file_size def test_equality(self, video_note): a = VideoNote(video_note.file_id, video_note.file_unique_id, self.length, self.duration) b = VideoNote("", video_note.file_unique_id, self.length, self.duration) c = VideoNote(video_note.file_id, video_note.file_unique_id, 0, 0) d = VideoNote("", "", self.length, self.duration) e = Voice(video_note.file_id, video_note.file_unique_id, self.duration) 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_error_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): await bot.send_video_note(chat_id=chat_id) async def test_send_with_video_note(self, monkeypatch, bot, chat_id, video_note): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.json_parameters["video_note"] == video_note.file_id monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_video_note(chat_id, video_note=video_note) async def test_send_video_note_custom_filename( self, bot, chat_id, video_note_file, monkeypatch ): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return next(iter(request_data.multipart_data.values()))[0] == "custom_filename" monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_video_note(chat_id, video_note_file, filename="custom_filename") @pytest.mark.parametrize("local_mode", [True, False]) async def test_send_video_note_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 test_flag = False file = data_file("telegram.jpg") expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag if local_mode: test_flag = ( data.get("video_note") == expected and data.get("thumbnail") == expected ) else: test_flag = isinstance(data.get("video_note"), InputFile) and isinstance( data.get("thumbnail"), InputFile ) monkeypatch.setattr(bot, "_post", make_assertion) await bot.send_video_note(chat_id, file, thumbnail=file) assert test_flag finally: bot._local_mode = False async def test_get_file_instance_method(self, monkeypatch, video_note): async def make_assertion(*_, **kwargs): return kwargs["file_id"] == video_note.file_id assert check_shortcut_signature(VideoNote.get_file, Bot.get_file, ["file_id"], []) assert await check_shortcut_call(video_note.get_file, video_note.get_bot(), "get_file") assert await check_defaults_handling(video_note.get_file, video_note.get_bot()) monkeypatch.setattr(video_note.get_bot(), "get_file", make_assertion) assert await video_note.get_file() @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_video_note_default_quote_parse_mode( self, default_bot, chat_id, video_note, 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_video_note( chat_id, video_note, reply_parameters=ReplyParameters(**kwargs) ) class TestVideoNoteWithRequest(TestVideoNoteBase): async def test_send_all_args(self, bot, chat_id, video_note_file, video_note, thumb_file): message = await bot.send_video_note( chat_id, video_note_file, duration=self.duration, length=self.length, disable_notification=False, protect_content=True, thumbnail=thumb_file, ) assert isinstance(message.video_note, VideoNote) assert isinstance(message.video_note.file_id, str) assert isinstance(message.video_note.file_unique_id, str) assert message.video_note.file_id assert message.video_note.file_unique_id assert message.video_note.length == video_note.length assert message.video_note.duration == video_note.duration assert message.video_note.file_size == video_note.file_size assert message.video_note.thumbnail.file_size == self.thumb_file_size assert message.video_note.thumbnail.width == self.thumb_width assert message.video_note.thumbnail.height == self.thumb_height assert message.has_protected_content async def test_get_and_download(self, bot, video_note, chat_id, tmp_file): new_file = await bot.get_file(video_note.file_id) assert new_file.file_size == self.file_size assert new_file.file_unique_id == video_note.file_unique_id assert new_file.file_path.startswith("https://") await new_file.download_to_drive(tmp_file) assert tmp_file.is_file() async def test_resend(self, bot, chat_id, video_note): message = await bot.send_video_note(chat_id, video_note.file_id) assert message.video_note == video_note @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_video_note_default_allow_sending_without_reply( self, default_bot, chat_id, video_note, 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_video_note( chat_id, video_note, 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_video_note( chat_id, video_note, reply_to_message_id=reply_to_message.message_id ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_video_note( chat_id, video_note, reply_to_message_id=reply_to_message.message_id ) @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_video_note_default_protect_content(self, chat_id, default_bot, video_note): tasks = asyncio.gather( default_bot.send_video_note(chat_id, video_note), default_bot.send_video_note(chat_id, video_note, protect_content=False), ) protected, unprotected = await tasks assert protected.has_protected_content assert not unprotected.has_protected_content async def test_error_send_empty_file(self, bot, chat_id): with Path(os.devnull).open("rb") as file, pytest.raises(TelegramError): await bot.send_video_note(chat_id, file) async def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): await bot.send_video_note(chat_id, "") python-telegram-bot-21.1.1/tests/_files/test_voice.py000066400000000000000000000334571460724040100226300ustar00rootroot00000000000000#!/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 os from pathlib import Path import pytest from telegram import Audio, Bot, InputFile, MessageEntity, ReplyParameters, Voice from telegram.constants import ParseMode from telegram.error import BadRequest, TelegramError from telegram.helpers import escape_markdown from telegram.request import RequestData 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.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture() def voice_file(): with data_file("telegram.ogg").open("rb") as f: yield f @pytest.fixture(scope="module") async def voice(bot, chat_id): with data_file("telegram.ogg").open("rb") as f: return (await bot.send_voice(chat_id, voice=f, read_timeout=50)).voice class TestVoiceBase: duration = 3 mime_type = "audio/ogg" file_size = 9199 caption = "Test *voice*" voice_file_url = "https://python-telegram-bot.org/static/testfiles/telegram.ogg" voice_file_id = "5a3128a4d2a04750b5b58397f3b5e812" voice_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" class TestVoiceWithoutRequest(TestVoiceBase): def test_slot_behaviour(self, voice): for attr in voice.__slots__: assert getattr(voice, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(voice)) == len(set(mro_slots(voice))), "duplicate slot" async def test_creation(self, voice): # Make sure file has been uploaded. assert isinstance(voice, Voice) assert isinstance(voice.file_id, str) assert isinstance(voice.file_unique_id, str) assert voice.file_id assert voice.file_unique_id def test_expected_values(self, voice): assert voice.duration == self.duration assert voice.mime_type == self.mime_type assert voice.file_size == self.file_size def test_de_json(self, bot): json_dict = { "file_id": self.voice_file_id, "file_unique_id": self.voice_file_unique_id, "duration": self.duration, "mime_type": self.mime_type, "file_size": self.file_size, } json_voice = Voice.de_json(json_dict, bot) assert json_voice.api_kwargs == {} assert json_voice.file_id == self.voice_file_id assert json_voice.file_unique_id == self.voice_file_unique_id assert json_voice.duration == self.duration assert json_voice.mime_type == self.mime_type assert json_voice.file_size == self.file_size def test_to_dict(self, voice): voice_dict = voice.to_dict() assert isinstance(voice_dict, dict) assert voice_dict["file_id"] == voice.file_id assert voice_dict["file_unique_id"] == voice.file_unique_id assert voice_dict["duration"] == voice.duration assert voice_dict["mime_type"] == voice.mime_type assert voice_dict["file_size"] == voice.file_size def test_equality(self, voice): a = Voice(voice.file_id, voice.file_unique_id, self.duration) b = Voice("", voice.file_unique_id, self.duration) c = Voice(voice.file_id, voice.file_unique_id, 0) d = Voice("", "", self.duration) e = Audio(voice.file_id, voice.file_unique_id, self.duration) 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_error_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): await bot.sendVoice(chat_id) async def test_send_voice_custom_filename(self, bot, chat_id, voice_file, monkeypatch): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return next(iter(request_data.multipart_data.values()))[0] == "custom_filename" monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_voice(chat_id, voice_file, filename="custom_filename") async def test_send_with_voice(self, monkeypatch, bot, chat_id, voice): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.json_parameters["voice"] == voice.file_id monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_voice(chat_id, voice=voice) @pytest.mark.parametrize("local_mode", [True, False]) async def test_send_voice_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 test_flag = False file = data_file("telegram.jpg") expected = file.as_uri() async def make_assertion(_, data, *args, **kwargs): nonlocal test_flag if local_mode: test_flag = data.get("voice") == expected else: test_flag = isinstance(data.get("voice"), InputFile) monkeypatch.setattr(bot, "_post", make_assertion) await bot.send_voice(chat_id, file) assert test_flag finally: bot._local_mode = False async def test_get_file_instance_method(self, monkeypatch, voice): async def make_assertion(*_, **kwargs): return kwargs["file_id"] == voice.file_id assert check_shortcut_signature(Voice.get_file, Bot.get_file, ["file_id"], []) assert await check_shortcut_call(voice.get_file, voice.get_bot(), "get_file") assert await check_defaults_handling(voice.get_file, voice.get_bot()) monkeypatch.setattr(voice.get_bot(), "get_file", make_assertion) assert await voice.get_file() @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_voice_default_quote_parse_mode( self, default_bot, chat_id, voice, 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_voice(chat_id, voice, reply_parameters=ReplyParameters(**kwargs)) class TestVoiceWithRequest(TestVoiceBase): async def test_send_all_args(self, bot, chat_id, voice_file, voice): message = await bot.send_voice( chat_id, voice_file, duration=self.duration, caption=self.caption, disable_notification=False, protect_content=True, parse_mode="Markdown", ) assert isinstance(message.voice, Voice) assert isinstance(message.voice.file_id, str) assert isinstance(message.voice.file_unique_id, str) assert message.voice.file_id assert message.voice.file_unique_id assert message.voice.duration == voice.duration assert message.voice.mime_type == voice.mime_type assert message.voice.file_size == voice.file_size assert message.caption == self.caption.replace("*", "") assert message.has_protected_content async def test_get_and_download(self, bot, voice, chat_id, tmp_file): new_file = await bot.get_file(voice.file_id) assert new_file.file_size == voice.file_size assert new_file.file_unique_id == voice.file_unique_id assert new_file.file_path.startswith("https://") await new_file.download_to_drive(tmp_file) assert tmp_file.is_file() async def test_send_ogg_url_file(self, bot, chat_id, voice): message = await bot.sendVoice(chat_id, self.voice_file_url, duration=self.duration) assert isinstance(message.voice, Voice) assert isinstance(message.voice.file_id, str) assert isinstance(message.voice.file_unique_id, str) assert message.voice.file_id assert message.voice.file_unique_id assert message.voice.duration == voice.duration assert message.voice.mime_type == voice.mime_type assert message.voice.file_size == voice.file_size async def test_resend(self, bot, chat_id, voice): message = await bot.sendVoice(chat_id, voice.file_id) assert message.voice == voice async def test_send_voice_caption_entities(self, bot, chat_id, voice_file): 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_voice( chat_id, voice_file, caption=test_string, caption_entities=entities ) assert message.caption == test_string assert message.caption_entities == tuple(entities) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_voice_default_parse_mode_1(self, default_bot, chat_id, voice): test_string = "Italic Bold Code" test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_voice(chat_id, voice, caption=test_markdown_string) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_voice_default_parse_mode_2(self, default_bot, chat_id, voice): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_voice( chat_id, voice, caption=test_markdown_string, parse_mode=None ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_voice_default_parse_mode_3(self, default_bot, chat_id, voice): test_markdown_string = "_Italic_ *Bold* `Code`" message = await default_bot.send_voice( chat_id, voice, caption=test_markdown_string, parse_mode="HTML" ) assert message.caption == test_markdown_string assert message.caption_markdown == escape_markdown(test_markdown_string) @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_voice_default_protect_content(self, chat_id, default_bot, voice): tasks = asyncio.gather( default_bot.send_voice(chat_id, voice), default_bot.send_voice(chat_id, voice, protect_content=False), ) protected, unprotected = await tasks assert protected.has_protected_content assert not unprotected.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_voice_default_allow_sending_without_reply( self, default_bot, chat_id, voice, 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_voice( chat_id, voice, 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_voice( chat_id, voice, reply_to_message_id=reply_to_message.message_id ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_voice( chat_id, voice, reply_to_message_id=reply_to_message.message_id ) async def test_error_send_empty_file(self, bot, chat_id): with Path(os.devnull).open("rb") as file, pytest.raises(TelegramError): await bot.sendVoice(chat_id, file) async def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): await bot.sendVoice(chat_id, "") python-telegram-bot-21.1.1/tests/_games/000077500000000000000000000000001460724040100200705ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/_games/__init__.py000066400000000000000000000014661460724040100222100ustar00rootroot00000000000000#!/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/]. python-telegram-bot-21.1.1/tests/_games/test_game.py000066400000000000000000000116051460724040100224150ustar00rootroot00000000000000#!/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( TestGameBase.title, TestGameBase.description, TestGameBase.photo, text=TestGameBase.text, text_entities=TestGameBase.text_entities, animation=TestGameBase.animation, ) game._unfreeze() return game class TestGameBase: 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(TestGameBase): 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"} python-telegram-bot-21.1.1/tests/_games/test_gamehighscore.py000066400000000000000000000055731460724040100243200ustar00rootroot00000000000000#!/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( TestGameHighScoreBase.position, TestGameHighScoreBase.user, TestGameHighScoreBase.score ) class TestGameHighScoreBase: position = 12 user = User(2, "test user", False) score = 42 class TestGameHighScoreWithoutRequest(TestGameHighScoreBase): 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) python-telegram-bot-21.1.1/tests/_inline/000077500000000000000000000000001460724040100202525ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/_inline/__init__.py000066400000000000000000000014661460724040100223720ustar00rootroot00000000000000#!/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/]. python-telegram-bot-21.1.1/tests/_inline/test_inlinekeyboardbutton.py000066400000000000000000000203261460724040100261210ustar00rootroot00000000000000#!/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 ( CallbackGame, InlineKeyboardButton, LoginUrl, SwitchInlineQueryChosenChat, WebAppInfo, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_keyboard_button(): return InlineKeyboardButton( TestInlineKeyboardButtonBase.text, url=TestInlineKeyboardButtonBase.url, callback_data=TestInlineKeyboardButtonBase.callback_data, switch_inline_query=TestInlineKeyboardButtonBase.switch_inline_query, switch_inline_query_current_chat=( TestInlineKeyboardButtonBase.switch_inline_query_current_chat ), callback_game=TestInlineKeyboardButtonBase.callback_game, pay=TestInlineKeyboardButtonBase.pay, login_url=TestInlineKeyboardButtonBase.login_url, web_app=TestInlineKeyboardButtonBase.web_app, switch_inline_query_chosen_chat=( TestInlineKeyboardButtonBase.switch_inline_query_chosen_chat ), ) class TestInlineKeyboardButtonBase: text = "text" url = "url" callback_data = "callback data" switch_inline_query = "switch_inline_query" switch_inline_query_current_chat = "switch_inline_query_current_chat" callback_game = CallbackGame() pay = True login_url = LoginUrl("http://google.com") web_app = WebAppInfo(url="https://example.com") switch_inline_query_chosen_chat = SwitchInlineQueryChosenChat("a_bot", True, False, True, True) class TestInlineKeyboardButtonWithoutRequest(TestInlineKeyboardButtonBase): def test_slot_behaviour(self, inline_keyboard_button): inst = inline_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, inline_keyboard_button): assert inline_keyboard_button.text == self.text assert inline_keyboard_button.url == self.url assert inline_keyboard_button.callback_data == self.callback_data assert inline_keyboard_button.switch_inline_query == self.switch_inline_query assert ( inline_keyboard_button.switch_inline_query_current_chat == self.switch_inline_query_current_chat ) assert isinstance(inline_keyboard_button.callback_game, CallbackGame) assert inline_keyboard_button.pay == self.pay assert inline_keyboard_button.login_url == self.login_url assert inline_keyboard_button.web_app == self.web_app assert ( inline_keyboard_button.switch_inline_query_chosen_chat == self.switch_inline_query_chosen_chat ) def test_to_dict(self, inline_keyboard_button): inline_keyboard_button_dict = inline_keyboard_button.to_dict() assert isinstance(inline_keyboard_button_dict, dict) assert inline_keyboard_button_dict["text"] == inline_keyboard_button.text assert inline_keyboard_button_dict["url"] == inline_keyboard_button.url assert inline_keyboard_button_dict["callback_data"] == inline_keyboard_button.callback_data assert ( inline_keyboard_button_dict["switch_inline_query"] == inline_keyboard_button.switch_inline_query ) assert ( inline_keyboard_button_dict["switch_inline_query_current_chat"] == inline_keyboard_button.switch_inline_query_current_chat ) assert ( inline_keyboard_button_dict["callback_game"] == inline_keyboard_button.callback_game.to_dict() ) assert inline_keyboard_button_dict["pay"] == inline_keyboard_button.pay assert ( inline_keyboard_button_dict["login_url"] == inline_keyboard_button.login_url.to_dict() ) assert inline_keyboard_button_dict["web_app"] == inline_keyboard_button.web_app.to_dict() assert ( inline_keyboard_button_dict["switch_inline_query_chosen_chat"] == inline_keyboard_button.switch_inline_query_chosen_chat.to_dict() ) def test_de_json(self, bot): json_dict = { "text": self.text, "url": self.url, "callback_data": self.callback_data, "switch_inline_query": self.switch_inline_query, "switch_inline_query_current_chat": self.switch_inline_query_current_chat, "callback_game": self.callback_game.to_dict(), "web_app": self.web_app.to_dict(), "login_url": self.login_url.to_dict(), "pay": self.pay, "switch_inline_query_chosen_chat": self.switch_inline_query_chosen_chat.to_dict(), } inline_keyboard_button = InlineKeyboardButton.de_json(json_dict, None) assert inline_keyboard_button.api_kwargs == {} assert inline_keyboard_button.text == self.text assert inline_keyboard_button.url == self.url assert inline_keyboard_button.callback_data == self.callback_data assert inline_keyboard_button.switch_inline_query == self.switch_inline_query assert ( inline_keyboard_button.switch_inline_query_current_chat == self.switch_inline_query_current_chat ) # CallbackGame has empty _id_attrs, so just test if the class is created. assert isinstance(inline_keyboard_button.callback_game, CallbackGame) assert inline_keyboard_button.pay == self.pay assert inline_keyboard_button.login_url == self.login_url assert inline_keyboard_button.web_app == self.web_app assert ( inline_keyboard_button.switch_inline_query_chosen_chat == self.switch_inline_query_chosen_chat ) none = InlineKeyboardButton.de_json({}, bot) assert none is None def test_equality(self): a = InlineKeyboardButton("text", callback_data="data") b = InlineKeyboardButton("text", callback_data="data") c = InlineKeyboardButton("texts", callback_data="data") d = InlineKeyboardButton("text", callback_data="info") e = InlineKeyboardButton("text", url="http://google.com") f = InlineKeyboardButton("text", web_app=WebAppInfo(url="https://ptb.org")) g = LoginUrl("http://google.com") 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 a != g assert hash(a) != hash(g) @pytest.mark.parametrize("callback_data", ["foo", 1, ("da", "ta"), object()]) def test_update_callback_data(self, callback_data): button = InlineKeyboardButton(text="test", callback_data="data") button_b = InlineKeyboardButton(text="test", callback_data="data") assert button == button_b assert hash(button) == hash(button_b) button.update_callback_data(callback_data) assert button.callback_data is callback_data assert button != button_b assert hash(button) != hash(button_b) button_b.update_callback_data(callback_data) assert button_b.callback_data is callback_data assert button == button_b assert hash(button) == hash(button_b) button.update_callback_data({}) assert button.callback_data == {} with pytest.raises(TypeError, match="unhashable"): hash(button) python-telegram-bot-21.1.1/tests/_inline/test_inlinekeyboardmarkup.py000066400000000000000000000220671460724040100261110ustar00rootroot00000000000000#!/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, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_keyboard_markup(): return InlineKeyboardMarkup(TestInlineKeyboardMarkupBase.inline_keyboard) class TestInlineKeyboardMarkupBase: inline_keyboard = [ [ InlineKeyboardButton(text="button1", callback_data="data1"), InlineKeyboardButton(text="button2", callback_data="data2"), ] ] class TestInlineKeyboardMarkupWithoutRequest(TestInlineKeyboardMarkupBase): def test_slot_behaviour(self, inline_keyboard_markup): inst = inline_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_to_dict(self, inline_keyboard_markup): inline_keyboard_markup_dict = inline_keyboard_markup.to_dict() assert isinstance(inline_keyboard_markup_dict, dict) assert inline_keyboard_markup_dict["inline_keyboard"] == [ [self.inline_keyboard[0][0].to_dict(), self.inline_keyboard[0][1].to_dict()] ] def test_de_json(self): json_dict = { "inline_keyboard": [ [ {"text": "start", "url": "http://google.com"}, {"text": "next", "callback_data": "abcd"}, ], [{"text": "Cancel", "callback_data": "Cancel"}], ] } inline_keyboard_markup = InlineKeyboardMarkup.de_json(json_dict, None) assert inline_keyboard_markup.api_kwargs == {} assert isinstance(inline_keyboard_markup, InlineKeyboardMarkup) keyboard = inline_keyboard_markup.inline_keyboard assert len(keyboard) == 2 assert len(keyboard[0]) == 2 assert len(keyboard[1]) == 1 assert isinstance(keyboard[0][0], InlineKeyboardButton) assert isinstance(keyboard[0][1], InlineKeyboardButton) assert isinstance(keyboard[1][0], InlineKeyboardButton) assert keyboard[0][0].text == "start" assert keyboard[0][0].url == "http://google.com" def test_equality(self): a = InlineKeyboardMarkup.from_column( [ InlineKeyboardButton(label, callback_data="data") for label in ["button1", "button2", "button3"] ] ) b = InlineKeyboardMarkup.from_column( [ InlineKeyboardButton(label, callback_data="data") for label in ["button1", "button2", "button3"] ] ) c = InlineKeyboardMarkup.from_column( [InlineKeyboardButton(label, callback_data="data") for label in ["button1", "button2"]] ) d = InlineKeyboardMarkup.from_column( [ InlineKeyboardButton(label, callback_data=label) for label in ["button1", "button2", "button3"] ] ) e = InlineKeyboardMarkup.from_column( [InlineKeyboardButton(label, url=label) for label in ["button1", "button2", "button3"]] ) f = InlineKeyboardMarkup( [ [ InlineKeyboardButton(label, callback_data="data") for label in ["button1", "button2"] ], [ InlineKeyboardButton(label, callback_data="data") for label in ["button1", "button2"] ], [ InlineKeyboardButton(label, callback_data="data") for label in ["button1", "button2"] ], ] ) g = ReplyKeyboardMarkup.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) assert a != g assert hash(a) != hash(g) def test_from_button(self): inline_keyboard_markup = InlineKeyboardMarkup.from_button( InlineKeyboardButton(text="button1", callback_data="data1") ).inline_keyboard assert len(inline_keyboard_markup) == 1 assert len(inline_keyboard_markup[0]) == 1 def test_from_row(self): inline_keyboard_markup = InlineKeyboardMarkup.from_row( [ InlineKeyboardButton(text="button1", callback_data="data1"), InlineKeyboardButton(text="button1", callback_data="data1"), ] ).inline_keyboard assert len(inline_keyboard_markup) == 1 assert len(inline_keyboard_markup[0]) == 2 def test_from_column(self): inline_keyboard_markup = InlineKeyboardMarkup.from_column( [ InlineKeyboardButton(text="button1", callback_data="data1"), InlineKeyboardButton(text="button1", callback_data="data1"), ] ).inline_keyboard assert len(inline_keyboard_markup) == 2 assert len(inline_keyboard_markup[0]) == 1 assert len(inline_keyboard_markup[1]) == 1 def test_expected_values(self, inline_keyboard_markup): assert inline_keyboard_markup.inline_keyboard == tuple( tuple(row) for row in self.inline_keyboard ) def test_wrong_keyboard_inputs(self): with pytest.raises(ValueError, match="should be a sequence of sequences"): InlineKeyboardMarkup( [[InlineKeyboardButton("b1", "1")], InlineKeyboardButton("b2", "2")] ) with pytest.raises(ValueError, match="should be a sequence of sequences"): InlineKeyboardMarkup("strings_are_not_allowed") with pytest.raises(ValueError, match="should be a sequence of sequences"): InlineKeyboardMarkup(["strings_are_not_allowed_in_the_rows_either"]) with pytest.raises(ValueError, match="should be a sequence of sequences"): InlineKeyboardMarkup(InlineKeyboardButton("b1", "1")) with pytest.raises(ValueError, match="should be a sequence of sequences"): InlineKeyboardMarkup([[[InlineKeyboardButton("only_2d_array_is_allowed", "1")]]]) async def test_expected_values_empty_switch(self, inline_keyboard_markup, bot, monkeypatch): async def make_assertion( url, data, reply_to_message_id=None, disable_notification=None, reply_markup=None, timeout=None, **kwargs, ): if reply_markup is not None: markups = ( InlineKeyboardMarkup, ReplyKeyboardMarkup, ForceReply, ReplyKeyboardRemove, ) if isinstance(reply_markup, markups): data["reply_markup"] = reply_markup.to_dict() else: data["reply_markup"] = reply_markup assert bool("'switch_inline_query': ''" in str(data["reply_markup"])) assert bool("'switch_inline_query_current_chat': ''" in str(data["reply_markup"])) inline_keyboard_markup.inline_keyboard[0][0]._unfreeze() inline_keyboard_markup.inline_keyboard[0][0].callback_data = None inline_keyboard_markup.inline_keyboard[0][0].switch_inline_query = "" inline_keyboard_markup.inline_keyboard[0][1]._unfreeze() inline_keyboard_markup.inline_keyboard[0][1].callback_data = None inline_keyboard_markup.inline_keyboard[0][1].switch_inline_query_current_chat = "" monkeypatch.setattr(bot, "_send_message", make_assertion) await bot.send_message(123, "test", reply_markup=inline_keyboard_markup) class TestInlineKeyborardMarkupWithRequest(TestInlineKeyboardMarkupBase): async def test_send_message_with_inline_keyboard_markup( self, bot, chat_id, inline_keyboard_markup ): message = await bot.send_message( chat_id, "Testing InlineKeyboardMarkup", reply_markup=inline_keyboard_markup ) assert message.text == "Testing InlineKeyboardMarkup" python-telegram-bot-21.1.1/tests/_inline/test_inlinequery.py000066400000000000000000000116121460724040100242300ustar00rootroot00000000000000#!/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, InlineQuery, Location, Update, 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(scope="module") def inline_query(bot): ilq = InlineQuery( TestInlineQueryBase.id_, TestInlineQueryBase.from_user, TestInlineQueryBase.query, TestInlineQueryBase.offset, location=TestInlineQueryBase.location, ) ilq.set_bot(bot) return ilq class TestInlineQueryBase: id_ = 1234 from_user = User(1, "First name", False) query = "query text" offset = "offset" location = Location(8.8, 53.1) class TestInlineQueryWithoutRequest(TestInlineQueryBase): def test_slot_behaviour(self, inline_query): for attr in inline_query.__slots__: assert getattr(inline_query, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(inline_query)) == len(set(mro_slots(inline_query))), "duplicate slot" def test_de_json(self, bot): json_dict = { "id": self.id_, "from": self.from_user.to_dict(), "query": self.query, "offset": self.offset, "location": self.location.to_dict(), } inline_query_json = InlineQuery.de_json(json_dict, bot) assert inline_query_json.api_kwargs == {} assert inline_query_json.id == self.id_ assert inline_query_json.from_user == self.from_user assert inline_query_json.location == self.location assert inline_query_json.query == self.query assert inline_query_json.offset == self.offset def test_to_dict(self, inline_query): inline_query_dict = inline_query.to_dict() assert isinstance(inline_query_dict, dict) assert inline_query_dict["id"] == inline_query.id assert inline_query_dict["from"] == inline_query.from_user.to_dict() assert inline_query_dict["location"] == inline_query.location.to_dict() assert inline_query_dict["query"] == inline_query.query assert inline_query_dict["offset"] == inline_query.offset def test_equality(self): a = InlineQuery(self.id_, User(1, "", False), "", "") b = InlineQuery(self.id_, User(1, "", False), "", "") c = InlineQuery(self.id_, User(0, "", False), "", "") d = InlineQuery(0, User(1, "", False), "", "") 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) async def test_answer_error(self, inline_query): with pytest.raises(ValueError, match="mutually exclusive"): await inline_query.answer(results=[], auto_pagination=True, current_offset="foobar") async def test_answer(self, monkeypatch, inline_query): async def make_assertion(*_, **kwargs): return kwargs["inline_query_id"] == inline_query.id assert check_shortcut_signature( InlineQuery.answer, Bot.answer_inline_query, ["inline_query_id"], ["auto_pagination"] ) assert await check_shortcut_call( inline_query.answer, inline_query.get_bot(), "answer_inline_query" ) assert await check_defaults_handling(inline_query.answer, inline_query.get_bot()) monkeypatch.setattr(inline_query.get_bot(), "answer_inline_query", make_assertion) assert await inline_query.answer(results=[]) async def test_answer_auto_pagination(self, monkeypatch, inline_query): async def make_assertion(*_, **kwargs): inline_query_id_matches = kwargs["inline_query_id"] == inline_query.id offset_matches = kwargs.get("current_offset") == inline_query.offset return offset_matches and inline_query_id_matches monkeypatch.setattr(inline_query.get_bot(), "answer_inline_query", make_assertion) assert await inline_query.answer(results=[], auto_pagination=True) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryhandler.py000066400000000000000000000134431460724040100255720ustar00rootroot00000000000000#!/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 pytest from telegram import ( Bot, CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Location, Message, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram.ext import CallbackContext, InlineQueryHandler, JobQueue from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, {"channel_post": message}, {"edited_channel_post": message}, {"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 = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "chosen_inline_result", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=2, **request.param) @pytest.fixture() def inline_query(bot): update = Update( 0, inline_query=InlineQuery( "id", User(2, "test user", False), "test query", offset="22", location=Location(latitude=-23.691288, longitude=-46.788279), ), ) update._unfreeze() update.inline_query._unfreeze() return update class TestInlineQueryHandler: test_flag = False def test_slot_behaviour(self): handler = InlineQueryHandler(self.callback) for attr in handler.__slots__: assert getattr(handler, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.user_data, dict) and context.chat_data is None and isinstance(context.bot_data, dict) and isinstance(update.inline_query, InlineQuery) ) def callback_pattern(self, update, context): if context.matches[0].groups(): self.test_flag = context.matches[0].groups() == ("t", " query") if context.matches[0].groupdict(): self.test_flag = context.matches[0].groupdict() == {"begin": "t", "end": " query"} def test_other_update_types(self, false_update): handler = InlineQueryHandler(self.callback) assert not handler.check_update(false_update) async def test_context(self, app, inline_query): handler = InlineQueryHandler(self.callback) app.add_handler(handler) async with app: await app.process_update(inline_query) assert self.test_flag async def test_context_pattern(self, app, inline_query): handler = InlineQueryHandler(self.callback_pattern, pattern=r"(?P.*)est(?P.*)") app.add_handler(handler) async with app: await app.process_update(inline_query) assert self.test_flag app.remove_handler(handler) handler = InlineQueryHandler(self.callback_pattern, pattern=r"(t)est(.*)") app.add_handler(handler) await app.process_update(inline_query) assert self.test_flag update = Update( update_id=0, inline_query=InlineQuery(id="id", from_user=None, query="", offset="") ) update.inline_query._unfreeze() assert not handler.check_update(update) update.inline_query.query = "not_a_match" assert not handler.check_update(update) @pytest.mark.parametrize("chat_types", [[Chat.SENDER], [Chat.SENDER, Chat.SUPERGROUP], []]) @pytest.mark.parametrize( ("chat_type", "result"), [(Chat.SENDER, True), (Chat.CHANNEL, False), (None, False)] ) async def test_chat_types(self, app, inline_query, chat_types, chat_type, result): try: inline_query.inline_query.chat_type = chat_type handler = InlineQueryHandler(self.callback, chat_types=chat_types) app.add_handler(handler) async with app: await app.process_update(inline_query) if not chat_types: assert self.test_flag is False else: assert self.test_flag == result finally: inline_query.inline_query.chat_type = None python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultarticle.py000066400000000000000000000144661460724040100270450ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResult, InlineQueryResultArticle, InlineQueryResultAudio, InputTextMessageContent, ) from telegram.constants import InlineQueryResultType from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_article(): return InlineQueryResultArticle( TestInlineQueryResultArticleBase.id_, TestInlineQueryResultArticleBase.title, input_message_content=TestInlineQueryResultArticleBase.input_message_content, reply_markup=TestInlineQueryResultArticleBase.reply_markup, url=TestInlineQueryResultArticleBase.url, hide_url=TestInlineQueryResultArticleBase.hide_url, description=TestInlineQueryResultArticleBase.description, thumbnail_url=TestInlineQueryResultArticleBase.thumbnail_url, thumbnail_height=TestInlineQueryResultArticleBase.thumbnail_height, thumbnail_width=TestInlineQueryResultArticleBase.thumbnail_width, ) class TestInlineQueryResultArticleBase: id_ = "id" type_ = "article" title = "title" input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) url = "url" hide_url = True description = "description" thumbnail_url = "thumb url" thumbnail_height = 10 thumbnail_width = 15 class TestInlineQueryResultArticleWithoutRequest(TestInlineQueryResultArticleBase): def test_slot_behaviour(self, inline_query_result_article): inst = inline_query_result_article 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, inline_query_result_article): assert inline_query_result_article.type == self.type_ assert inline_query_result_article.id == self.id_ assert inline_query_result_article.title == self.title assert ( inline_query_result_article.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_article.reply_markup.to_dict() == self.reply_markup.to_dict() assert inline_query_result_article.url == self.url assert inline_query_result_article.hide_url == self.hide_url assert inline_query_result_article.description == self.description assert inline_query_result_article.thumbnail_url == self.thumbnail_url assert inline_query_result_article.thumbnail_height == self.thumbnail_height assert inline_query_result_article.thumbnail_width == self.thumbnail_width def test_to_dict(self, inline_query_result_article): inline_query_result_article_dict = inline_query_result_article.to_dict() assert isinstance(inline_query_result_article_dict, dict) assert inline_query_result_article_dict["type"] == inline_query_result_article.type assert inline_query_result_article_dict["id"] == inline_query_result_article.id assert inline_query_result_article_dict["title"] == inline_query_result_article.title assert ( inline_query_result_article_dict["input_message_content"] == inline_query_result_article.input_message_content.to_dict() ) assert ( inline_query_result_article_dict["reply_markup"] == inline_query_result_article.reply_markup.to_dict() ) assert inline_query_result_article_dict["url"] == inline_query_result_article.url assert inline_query_result_article_dict["hide_url"] == inline_query_result_article.hide_url assert ( inline_query_result_article_dict["description"] == inline_query_result_article.description ) assert ( inline_query_result_article_dict["thumbnail_url"] == inline_query_result_article.thumbnail_url ) assert ( inline_query_result_article_dict["thumbnail_height"] == inline_query_result_article.thumbnail_height ) assert ( inline_query_result_article_dict["thumbnail_width"] == inline_query_result_article.thumbnail_width ) def test_type_enum_conversion(self): # Since we have a lot of different test files for all the result types, we test this # conversion only here. It is independent of the specific class assert ( type( InlineQueryResult( id="id", type="article", ).type ) is InlineQueryResultType ) assert ( InlineQueryResult( id="id", type="unknown", ).type == "unknown" ) def test_equality(self): a = InlineQueryResultArticle(self.id_, self.title, self.input_message_content) b = InlineQueryResultArticle(self.id_, self.title, self.input_message_content) c = InlineQueryResultArticle(self.id_, "", self.input_message_content) d = InlineQueryResultArticle("", self.title, self.input_message_content) e = InlineQueryResultAudio(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultaudio.py000066400000000000000000000133161460724040100265140ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultAudio, InlineQueryResultVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_audio(): return InlineQueryResultAudio( TestInlineQueryResultAudioBase.id_, TestInlineQueryResultAudioBase.audio_url, TestInlineQueryResultAudioBase.title, performer=TestInlineQueryResultAudioBase.performer, audio_duration=TestInlineQueryResultAudioBase.audio_duration, caption=TestInlineQueryResultAudioBase.caption, parse_mode=TestInlineQueryResultAudioBase.parse_mode, caption_entities=TestInlineQueryResultAudioBase.caption_entities, input_message_content=TestInlineQueryResultAudioBase.input_message_content, reply_markup=TestInlineQueryResultAudioBase.reply_markup, ) class TestInlineQueryResultAudioBase: id_ = "id" type_ = "audio" audio_url = "audio url" title = "title" performer = "performer" audio_duration = "audio_duration" caption = "caption" parse_mode = "Markdown" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultAudioWithoutRequest(TestInlineQueryResultAudioBase): def test_slot_behaviour(self, inline_query_result_audio): inst = inline_query_result_audio 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, inline_query_result_audio): assert inline_query_result_audio.type == self.type_ assert inline_query_result_audio.id == self.id_ assert inline_query_result_audio.audio_url == self.audio_url assert inline_query_result_audio.title == self.title assert inline_query_result_audio.performer == self.performer assert inline_query_result_audio.audio_duration == self.audio_duration assert inline_query_result_audio.caption == self.caption assert inline_query_result_audio.parse_mode == self.parse_mode assert inline_query_result_audio.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_audio.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_audio.reply_markup.to_dict() == self.reply_markup.to_dict() def test_to_dict(self, inline_query_result_audio): inline_query_result_audio_dict = inline_query_result_audio.to_dict() assert isinstance(inline_query_result_audio_dict, dict) assert inline_query_result_audio_dict["type"] == inline_query_result_audio.type assert inline_query_result_audio_dict["id"] == inline_query_result_audio.id assert inline_query_result_audio_dict["audio_url"] == inline_query_result_audio.audio_url assert inline_query_result_audio_dict["title"] == inline_query_result_audio.title assert inline_query_result_audio_dict["performer"] == inline_query_result_audio.performer assert ( inline_query_result_audio_dict["audio_duration"] == inline_query_result_audio.audio_duration ) assert inline_query_result_audio_dict["caption"] == inline_query_result_audio.caption assert inline_query_result_audio_dict["parse_mode"] == inline_query_result_audio.parse_mode assert inline_query_result_audio_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_audio.caption_entities ] assert ( inline_query_result_audio_dict["input_message_content"] == inline_query_result_audio.input_message_content.to_dict() ) assert ( inline_query_result_audio_dict["reply_markup"] == inline_query_result_audio.reply_markup.to_dict() ) def test_caption_entities_always_tuple(self): inline_query_result_audio = InlineQueryResultAudio(self.id_, self.audio_url, self.title) assert inline_query_result_audio.caption_entities == () def test_equality(self): a = InlineQueryResultAudio(self.id_, self.audio_url, self.title) b = InlineQueryResultAudio(self.id_, self.title, self.title) c = InlineQueryResultAudio(self.id_, "", self.title) d = InlineQueryResultAudio("", self.audio_url, self.title) e = InlineQueryResultVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultcachedaudio.py000066400000000000000000000125411460724040100276430ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultCachedAudio, InlineQueryResultCachedVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_cached_audio(): return InlineQueryResultCachedAudio( TestInlineQueryResultCachedAudioBase.id_, TestInlineQueryResultCachedAudioBase.audio_file_id, caption=TestInlineQueryResultCachedAudioBase.caption, parse_mode=TestInlineQueryResultCachedAudioBase.parse_mode, caption_entities=TestInlineQueryResultCachedAudioBase.caption_entities, input_message_content=TestInlineQueryResultCachedAudioBase.input_message_content, reply_markup=TestInlineQueryResultCachedAudioBase.reply_markup, ) class TestInlineQueryResultCachedAudioBase: id_ = "id" type_ = "audio" audio_file_id = "audio file id" caption = "caption" parse_mode = "HTML" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultCachedAudioWithoutRequest(TestInlineQueryResultCachedAudioBase): def test_slot_behaviour(self, inline_query_result_cached_audio): inst = inline_query_result_cached_audio 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, inline_query_result_cached_audio): assert inline_query_result_cached_audio.type == self.type_ assert inline_query_result_cached_audio.id == self.id_ assert inline_query_result_cached_audio.audio_file_id == self.audio_file_id assert inline_query_result_cached_audio.caption == self.caption assert inline_query_result_cached_audio.parse_mode == self.parse_mode assert inline_query_result_cached_audio.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_cached_audio.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert ( inline_query_result_cached_audio.reply_markup.to_dict() == self.reply_markup.to_dict() ) def test_caption_entities_always_tuple(self): audio = InlineQueryResultCachedAudio(self.id_, self.audio_file_id) assert audio.caption_entities == () def test_to_dict(self, inline_query_result_cached_audio): inline_query_result_cached_audio_dict = inline_query_result_cached_audio.to_dict() assert isinstance(inline_query_result_cached_audio_dict, dict) assert ( inline_query_result_cached_audio_dict["type"] == inline_query_result_cached_audio.type ) assert inline_query_result_cached_audio_dict["id"] == inline_query_result_cached_audio.id assert ( inline_query_result_cached_audio_dict["audio_file_id"] == inline_query_result_cached_audio.audio_file_id ) assert ( inline_query_result_cached_audio_dict["caption"] == inline_query_result_cached_audio.caption ) assert ( inline_query_result_cached_audio_dict["parse_mode"] == inline_query_result_cached_audio.parse_mode ) assert inline_query_result_cached_audio_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_cached_audio.caption_entities ] assert ( inline_query_result_cached_audio_dict["input_message_content"] == inline_query_result_cached_audio.input_message_content.to_dict() ) assert ( inline_query_result_cached_audio_dict["reply_markup"] == inline_query_result_cached_audio.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultCachedAudio(self.id_, self.audio_file_id) b = InlineQueryResultCachedAudio(self.id_, self.audio_file_id) c = InlineQueryResultCachedAudio(self.id_, "") d = InlineQueryResultCachedAudio("", self.audio_file_id) e = InlineQueryResultCachedVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultcacheddocument.py000066400000000000000000000144201460724040100303560ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultCachedDocument, InlineQueryResultCachedVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_cached_document(): return InlineQueryResultCachedDocument( TestInlineQueryResultCachedDocumentBase.id_, TestInlineQueryResultCachedDocumentBase.title, TestInlineQueryResultCachedDocumentBase.document_file_id, caption=TestInlineQueryResultCachedDocumentBase.caption, parse_mode=TestInlineQueryResultCachedDocumentBase.parse_mode, caption_entities=TestInlineQueryResultCachedDocumentBase.caption_entities, description=TestInlineQueryResultCachedDocumentBase.description, input_message_content=TestInlineQueryResultCachedDocumentBase.input_message_content, reply_markup=TestInlineQueryResultCachedDocumentBase.reply_markup, ) class TestInlineQueryResultCachedDocumentBase: id_ = "id" type_ = "document" document_file_id = "document file id" title = "title" caption = "caption" parse_mode = "Markdown" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] description = "description" input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultCachedDocumentWithoutRequest(TestInlineQueryResultCachedDocumentBase): def test_slot_behaviour(self, inline_query_result_cached_document): inst = inline_query_result_cached_document 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, inline_query_result_cached_document): assert inline_query_result_cached_document.id == self.id_ assert inline_query_result_cached_document.type == self.type_ assert inline_query_result_cached_document.document_file_id == self.document_file_id assert inline_query_result_cached_document.title == self.title assert inline_query_result_cached_document.caption == self.caption assert inline_query_result_cached_document.parse_mode == self.parse_mode assert inline_query_result_cached_document.caption_entities == tuple(self.caption_entities) assert inline_query_result_cached_document.description == self.description assert ( inline_query_result_cached_document.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert ( inline_query_result_cached_document.reply_markup.to_dict() == self.reply_markup.to_dict() ) def test_caption_entities_always_tuple(self): test = InlineQueryResultCachedDocument(self.id_, self.title, self.document_file_id) assert test.caption_entities == () def test_to_dict(self, inline_query_result_cached_document): inline_query_result_cached_document_dict = inline_query_result_cached_document.to_dict() assert isinstance(inline_query_result_cached_document_dict, dict) assert ( inline_query_result_cached_document_dict["id"] == inline_query_result_cached_document.id ) assert ( inline_query_result_cached_document_dict["type"] == inline_query_result_cached_document.type ) assert ( inline_query_result_cached_document_dict["document_file_id"] == inline_query_result_cached_document.document_file_id ) assert ( inline_query_result_cached_document_dict["title"] == inline_query_result_cached_document.title ) assert ( inline_query_result_cached_document_dict["caption"] == inline_query_result_cached_document.caption ) assert ( inline_query_result_cached_document_dict["parse_mode"] == inline_query_result_cached_document.parse_mode ) assert inline_query_result_cached_document_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_cached_document.caption_entities ] assert ( inline_query_result_cached_document_dict["description"] == inline_query_result_cached_document.description ) assert ( inline_query_result_cached_document_dict["input_message_content"] == inline_query_result_cached_document.input_message_content.to_dict() ) assert ( inline_query_result_cached_document_dict["reply_markup"] == inline_query_result_cached_document.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultCachedDocument(self.id_, self.title, self.document_file_id) b = InlineQueryResultCachedDocument(self.id_, self.title, self.document_file_id) c = InlineQueryResultCachedDocument(self.id_, self.title, "") d = InlineQueryResultCachedDocument("", self.title, self.document_file_id) e = InlineQueryResultCachedVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultcachedgif.py000066400000000000000000000126521460724040100273120ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultCachedGif, InlineQueryResultCachedVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_cached_gif(): return InlineQueryResultCachedGif( TestInlineQueryResultCachedGifBase.id_, TestInlineQueryResultCachedGifBase.gif_file_id, title=TestInlineQueryResultCachedGifBase.title, caption=TestInlineQueryResultCachedGifBase.caption, parse_mode=TestInlineQueryResultCachedGifBase.parse_mode, caption_entities=TestInlineQueryResultCachedGifBase.caption_entities, input_message_content=TestInlineQueryResultCachedGifBase.input_message_content, reply_markup=TestInlineQueryResultCachedGifBase.reply_markup, ) class TestInlineQueryResultCachedGifBase: id_ = "id" type_ = "gif" gif_file_id = "gif file id" title = "title" caption = "caption" parse_mode = "HTML" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultCachedGifWithoutRequest(TestInlineQueryResultCachedGifBase): def test_slot_behaviour(self, inline_query_result_cached_gif): inst = inline_query_result_cached_gif 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, inline_query_result_cached_gif): assert inline_query_result_cached_gif.type == self.type_ assert inline_query_result_cached_gif.id == self.id_ assert inline_query_result_cached_gif.gif_file_id == self.gif_file_id assert inline_query_result_cached_gif.title == self.title assert inline_query_result_cached_gif.caption == self.caption assert inline_query_result_cached_gif.parse_mode == self.parse_mode assert inline_query_result_cached_gif.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_cached_gif.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_cached_gif.reply_markup.to_dict() == self.reply_markup.to_dict() def test_caption_entities_always_tuple(self): result = InlineQueryResultCachedGif(self.id_, self.gif_file_id) assert result.caption_entities == () def test_to_dict(self, inline_query_result_cached_gif): inline_query_result_cached_gif_dict = inline_query_result_cached_gif.to_dict() assert isinstance(inline_query_result_cached_gif_dict, dict) assert inline_query_result_cached_gif_dict["type"] == inline_query_result_cached_gif.type assert inline_query_result_cached_gif_dict["id"] == inline_query_result_cached_gif.id assert ( inline_query_result_cached_gif_dict["gif_file_id"] == inline_query_result_cached_gif.gif_file_id ) assert inline_query_result_cached_gif_dict["title"] == inline_query_result_cached_gif.title assert ( inline_query_result_cached_gif_dict["caption"] == inline_query_result_cached_gif.caption ) assert ( inline_query_result_cached_gif_dict["parse_mode"] == inline_query_result_cached_gif.parse_mode ) assert inline_query_result_cached_gif_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_cached_gif.caption_entities ] assert ( inline_query_result_cached_gif_dict["input_message_content"] == inline_query_result_cached_gif.input_message_content.to_dict() ) assert ( inline_query_result_cached_gif_dict["reply_markup"] == inline_query_result_cached_gif.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultCachedGif(self.id_, self.gif_file_id) b = InlineQueryResultCachedGif(self.id_, self.gif_file_id) c = InlineQueryResultCachedGif(self.id_, "") d = InlineQueryResultCachedGif("", self.gif_file_id) e = InlineQueryResultCachedVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultcachedmpeg4gif.py000066400000000000000000000136341460724040100302500ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultCachedMpeg4Gif, InlineQueryResultCachedVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_cached_mpeg4_gif(): return InlineQueryResultCachedMpeg4Gif( TestInlineQueryResultCachedMpeg4GifBase.id_, TestInlineQueryResultCachedMpeg4GifBase.mpeg4_file_id, title=TestInlineQueryResultCachedMpeg4GifBase.title, caption=TestInlineQueryResultCachedMpeg4GifBase.caption, parse_mode=TestInlineQueryResultCachedMpeg4GifBase.parse_mode, caption_entities=TestInlineQueryResultCachedMpeg4GifBase.caption_entities, input_message_content=TestInlineQueryResultCachedMpeg4GifBase.input_message_content, reply_markup=TestInlineQueryResultCachedMpeg4GifBase.reply_markup, ) class TestInlineQueryResultCachedMpeg4GifBase: id_ = "id" type_ = "mpeg4_gif" mpeg4_file_id = "mpeg4 file id" title = "title" caption = "caption" parse_mode = "Markdown" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultCachedMpeg4GifWithoutRequest(TestInlineQueryResultCachedMpeg4GifBase): def test_slot_behaviour(self, inline_query_result_cached_mpeg4_gif): inst = inline_query_result_cached_mpeg4_gif 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, inline_query_result_cached_mpeg4_gif): assert inline_query_result_cached_mpeg4_gif.type == self.type_ assert inline_query_result_cached_mpeg4_gif.id == self.id_ assert inline_query_result_cached_mpeg4_gif.mpeg4_file_id == self.mpeg4_file_id assert inline_query_result_cached_mpeg4_gif.title == self.title assert inline_query_result_cached_mpeg4_gif.caption == self.caption assert inline_query_result_cached_mpeg4_gif.parse_mode == self.parse_mode assert inline_query_result_cached_mpeg4_gif.caption_entities == tuple( self.caption_entities ) assert ( inline_query_result_cached_mpeg4_gif.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert ( inline_query_result_cached_mpeg4_gif.reply_markup.to_dict() == self.reply_markup.to_dict() ) def test_caption_entities_always_tuple(self): result = InlineQueryResultCachedMpeg4Gif(self.id_, self.mpeg4_file_id) assert result.caption_entities == () def test_to_dict(self, inline_query_result_cached_mpeg4_gif): inline_query_result_cached_mpeg4_gif_dict = inline_query_result_cached_mpeg4_gif.to_dict() assert isinstance(inline_query_result_cached_mpeg4_gif_dict, dict) assert ( inline_query_result_cached_mpeg4_gif_dict["type"] == inline_query_result_cached_mpeg4_gif.type ) assert ( inline_query_result_cached_mpeg4_gif_dict["id"] == inline_query_result_cached_mpeg4_gif.id ) assert ( inline_query_result_cached_mpeg4_gif_dict["mpeg4_file_id"] == inline_query_result_cached_mpeg4_gif.mpeg4_file_id ) assert ( inline_query_result_cached_mpeg4_gif_dict["title"] == inline_query_result_cached_mpeg4_gif.title ) assert ( inline_query_result_cached_mpeg4_gif_dict["caption"] == inline_query_result_cached_mpeg4_gif.caption ) assert ( inline_query_result_cached_mpeg4_gif_dict["parse_mode"] == inline_query_result_cached_mpeg4_gif.parse_mode ) assert inline_query_result_cached_mpeg4_gif_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_cached_mpeg4_gif.caption_entities ] assert ( inline_query_result_cached_mpeg4_gif_dict["input_message_content"] == inline_query_result_cached_mpeg4_gif.input_message_content.to_dict() ) assert ( inline_query_result_cached_mpeg4_gif_dict["reply_markup"] == inline_query_result_cached_mpeg4_gif.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultCachedMpeg4Gif(self.id_, self.mpeg4_file_id) b = InlineQueryResultCachedMpeg4Gif(self.id_, self.mpeg4_file_id) c = InlineQueryResultCachedMpeg4Gif(self.id_, "") d = InlineQueryResultCachedMpeg4Gif("", self.mpeg4_file_id) e = InlineQueryResultCachedVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultcachedphoto.py000066400000000000000000000137161460724040100277000ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultCachedPhoto, InlineQueryResultCachedVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_cached_photo(): return InlineQueryResultCachedPhoto( TestInlineQueryResultCachedPhotoBase.id_, TestInlineQueryResultCachedPhotoBase.photo_file_id, title=TestInlineQueryResultCachedPhotoBase.title, description=TestInlineQueryResultCachedPhotoBase.description, caption=TestInlineQueryResultCachedPhotoBase.caption, parse_mode=TestInlineQueryResultCachedPhotoBase.parse_mode, caption_entities=TestInlineQueryResultCachedPhotoBase.caption_entities, input_message_content=TestInlineQueryResultCachedPhotoBase.input_message_content, reply_markup=TestInlineQueryResultCachedPhotoBase.reply_markup, ) class TestInlineQueryResultCachedPhotoBase: id_ = "id" type_ = "photo" photo_file_id = "photo file id" title = "title" description = "description" caption = "caption" parse_mode = "HTML" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultCachedPhotoWithoutRequest(TestInlineQueryResultCachedPhotoBase): def test_slot_behaviour(self, inline_query_result_cached_photo): inst = inline_query_result_cached_photo 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, inline_query_result_cached_photo): assert inline_query_result_cached_photo.type == self.type_ assert inline_query_result_cached_photo.id == self.id_ assert inline_query_result_cached_photo.photo_file_id == self.photo_file_id assert inline_query_result_cached_photo.title == self.title assert inline_query_result_cached_photo.description == self.description assert inline_query_result_cached_photo.caption == self.caption assert inline_query_result_cached_photo.parse_mode == self.parse_mode assert inline_query_result_cached_photo.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_cached_photo.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert ( inline_query_result_cached_photo.reply_markup.to_dict() == self.reply_markup.to_dict() ) def test_caption_entities_always_tuple(self): result = InlineQueryResultCachedPhoto(self.id_, self.photo_file_id) assert result.caption_entities == () def test_to_dict(self, inline_query_result_cached_photo): inline_query_result_cached_photo_dict = inline_query_result_cached_photo.to_dict() assert isinstance(inline_query_result_cached_photo_dict, dict) assert ( inline_query_result_cached_photo_dict["type"] == inline_query_result_cached_photo.type ) assert inline_query_result_cached_photo_dict["id"] == inline_query_result_cached_photo.id assert ( inline_query_result_cached_photo_dict["photo_file_id"] == inline_query_result_cached_photo.photo_file_id ) assert ( inline_query_result_cached_photo_dict["title"] == inline_query_result_cached_photo.title ) assert ( inline_query_result_cached_photo_dict["description"] == inline_query_result_cached_photo.description ) assert ( inline_query_result_cached_photo_dict["caption"] == inline_query_result_cached_photo.caption ) assert ( inline_query_result_cached_photo_dict["parse_mode"] == inline_query_result_cached_photo.parse_mode ) assert inline_query_result_cached_photo_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_cached_photo.caption_entities ] assert ( inline_query_result_cached_photo_dict["input_message_content"] == inline_query_result_cached_photo.input_message_content.to_dict() ) assert ( inline_query_result_cached_photo_dict["reply_markup"] == inline_query_result_cached_photo.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultCachedPhoto(self.id_, self.photo_file_id) b = InlineQueryResultCachedPhoto(self.id_, self.photo_file_id) c = InlineQueryResultCachedPhoto(self.id_, "") d = InlineQueryResultCachedPhoto("", self.photo_file_id) e = InlineQueryResultCachedVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultcachedsticker.py000066400000000000000000000104421460724040100302040ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultCachedSticker, InlineQueryResultCachedVoice, InputTextMessageContent, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_cached_sticker(): return InlineQueryResultCachedSticker( TestInlineQueryResultCachedStickerBase.id_, TestInlineQueryResultCachedStickerBase.sticker_file_id, input_message_content=TestInlineQueryResultCachedStickerBase.input_message_content, reply_markup=TestInlineQueryResultCachedStickerBase.reply_markup, ) class TestInlineQueryResultCachedStickerBase: id_ = "id" type_ = "sticker" sticker_file_id = "sticker file id" input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultCachedStickerWithoutRequest(TestInlineQueryResultCachedStickerBase): def test_slot_behaviour(self, inline_query_result_cached_sticker): inst = inline_query_result_cached_sticker 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, inline_query_result_cached_sticker): assert inline_query_result_cached_sticker.type == self.type_ assert inline_query_result_cached_sticker.id == self.id_ assert inline_query_result_cached_sticker.sticker_file_id == self.sticker_file_id assert ( inline_query_result_cached_sticker.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert ( inline_query_result_cached_sticker.reply_markup.to_dict() == self.reply_markup.to_dict() ) def test_to_dict(self, inline_query_result_cached_sticker): inline_query_result_cached_sticker_dict = inline_query_result_cached_sticker.to_dict() assert isinstance(inline_query_result_cached_sticker_dict, dict) assert ( inline_query_result_cached_sticker_dict["type"] == inline_query_result_cached_sticker.type ) assert ( inline_query_result_cached_sticker_dict["id"] == inline_query_result_cached_sticker.id ) assert ( inline_query_result_cached_sticker_dict["sticker_file_id"] == inline_query_result_cached_sticker.sticker_file_id ) assert ( inline_query_result_cached_sticker_dict["input_message_content"] == inline_query_result_cached_sticker.input_message_content.to_dict() ) assert ( inline_query_result_cached_sticker_dict["reply_markup"] == inline_query_result_cached_sticker.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultCachedSticker(self.id_, self.sticker_file_id) b = InlineQueryResultCachedSticker(self.id_, self.sticker_file_id) c = InlineQueryResultCachedSticker(self.id_, "") d = InlineQueryResultCachedSticker("", self.sticker_file_id) e = InlineQueryResultCachedVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultcachedvideo.py000066400000000000000000000140071460724040100276470ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultCachedVideo, InlineQueryResultCachedVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_cached_video(): return InlineQueryResultCachedVideo( TestInlineQueryResultCachedVideoBase.id_, TestInlineQueryResultCachedVideoBase.video_file_id, TestInlineQueryResultCachedVideoBase.title, caption=TestInlineQueryResultCachedVideoBase.caption, parse_mode=TestInlineQueryResultCachedVideoBase.parse_mode, caption_entities=TestInlineQueryResultCachedVideoBase.caption_entities, description=TestInlineQueryResultCachedVideoBase.description, input_message_content=TestInlineQueryResultCachedVideoBase.input_message_content, reply_markup=TestInlineQueryResultCachedVideoBase.reply_markup, ) class TestInlineQueryResultCachedVideoBase: id_ = "id" type_ = "video" video_file_id = "video file id" title = "title" caption = "caption" parse_mode = "Markdown" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] description = "description" input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultCachedVideoWithoutRequest(TestInlineQueryResultCachedVideoBase): def test_slot_behaviour(self, inline_query_result_cached_video): inst = inline_query_result_cached_video 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, inline_query_result_cached_video): assert inline_query_result_cached_video.type == self.type_ assert inline_query_result_cached_video.id == self.id_ assert inline_query_result_cached_video.video_file_id == self.video_file_id assert inline_query_result_cached_video.title == self.title assert inline_query_result_cached_video.description == self.description assert inline_query_result_cached_video.caption == self.caption assert inline_query_result_cached_video.parse_mode == self.parse_mode assert inline_query_result_cached_video.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_cached_video.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert ( inline_query_result_cached_video.reply_markup.to_dict() == self.reply_markup.to_dict() ) def test_caption_entities_always_tuple(self): video = InlineQueryResultCachedVideo(self.id_, self.video_file_id, self.title) assert video.caption_entities == () def test_to_dict(self, inline_query_result_cached_video): inline_query_result_cached_video_dict = inline_query_result_cached_video.to_dict() assert isinstance(inline_query_result_cached_video_dict, dict) assert ( inline_query_result_cached_video_dict["type"] == inline_query_result_cached_video.type ) assert inline_query_result_cached_video_dict["id"] == inline_query_result_cached_video.id assert ( inline_query_result_cached_video_dict["video_file_id"] == inline_query_result_cached_video.video_file_id ) assert ( inline_query_result_cached_video_dict["title"] == inline_query_result_cached_video.title ) assert ( inline_query_result_cached_video_dict["description"] == inline_query_result_cached_video.description ) assert ( inline_query_result_cached_video_dict["caption"] == inline_query_result_cached_video.caption ) assert ( inline_query_result_cached_video_dict["parse_mode"] == inline_query_result_cached_video.parse_mode ) assert inline_query_result_cached_video_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_cached_video.caption_entities ] assert ( inline_query_result_cached_video_dict["input_message_content"] == inline_query_result_cached_video.input_message_content.to_dict() ) assert ( inline_query_result_cached_video_dict["reply_markup"] == inline_query_result_cached_video.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultCachedVideo(self.id_, self.video_file_id, self.title) b = InlineQueryResultCachedVideo(self.id_, self.video_file_id, self.title) c = InlineQueryResultCachedVideo(self.id_, "", self.title) d = InlineQueryResultCachedVideo("", self.video_file_id, self.title) e = InlineQueryResultCachedVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultcachedvoice.py000066400000000000000000000132661460724040100276540ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultCachedAudio, InlineQueryResultCachedVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_cached_voice(): return InlineQueryResultCachedVoice( TestInlineQueryResultCachedVoiceBase.id_, TestInlineQueryResultCachedVoiceBase.voice_file_id, TestInlineQueryResultCachedVoiceBase.title, caption=TestInlineQueryResultCachedVoiceBase.caption, parse_mode=TestInlineQueryResultCachedVoiceBase.parse_mode, caption_entities=TestInlineQueryResultCachedVoiceBase.caption_entities, input_message_content=TestInlineQueryResultCachedVoiceBase.input_message_content, reply_markup=TestInlineQueryResultCachedVoiceBase.reply_markup, ) class TestInlineQueryResultCachedVoiceBase: id_ = "id" type_ = "voice" voice_file_id = "voice file id" title = "title" caption = "caption" parse_mode = "HTML" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultCachedVoiceWithoutRequest(TestInlineQueryResultCachedVoiceBase): def test_slot_behaviour(self, inline_query_result_cached_voice): inst = inline_query_result_cached_voice 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, inline_query_result_cached_voice): assert inline_query_result_cached_voice.type == self.type_ assert inline_query_result_cached_voice.id == self.id_ assert inline_query_result_cached_voice.voice_file_id == self.voice_file_id assert inline_query_result_cached_voice.title == self.title assert inline_query_result_cached_voice.caption == self.caption assert inline_query_result_cached_voice.parse_mode == self.parse_mode assert inline_query_result_cached_voice.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_cached_voice.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert ( inline_query_result_cached_voice.reply_markup.to_dict() == self.reply_markup.to_dict() ) def test_caption_entities_always_tuple(self): result = InlineQueryResultCachedVoice(self.id_, self.voice_file_id, self.title) assert result.caption_entities == () def test_to_dict(self, inline_query_result_cached_voice): inline_query_result_cached_voice_dict = inline_query_result_cached_voice.to_dict() assert isinstance(inline_query_result_cached_voice_dict, dict) assert ( inline_query_result_cached_voice_dict["type"] == inline_query_result_cached_voice.type ) assert inline_query_result_cached_voice_dict["id"] == inline_query_result_cached_voice.id assert ( inline_query_result_cached_voice_dict["voice_file_id"] == inline_query_result_cached_voice.voice_file_id ) assert ( inline_query_result_cached_voice_dict["title"] == inline_query_result_cached_voice.title ) assert ( inline_query_result_cached_voice_dict["caption"] == inline_query_result_cached_voice.caption ) assert ( inline_query_result_cached_voice_dict["parse_mode"] == inline_query_result_cached_voice.parse_mode ) assert inline_query_result_cached_voice_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_cached_voice.caption_entities ] assert ( inline_query_result_cached_voice_dict["input_message_content"] == inline_query_result_cached_voice.input_message_content.to_dict() ) assert ( inline_query_result_cached_voice_dict["reply_markup"] == inline_query_result_cached_voice.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultCachedVoice(self.id_, self.voice_file_id, self.title) b = InlineQueryResultCachedVoice(self.id_, self.voice_file_id, self.title) c = InlineQueryResultCachedVoice(self.id_, "", self.title) d = InlineQueryResultCachedVoice("", self.voice_file_id, self.title) e = InlineQueryResultCachedAudio(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultcontact.py000066400000000000000000000130321460724040100270410ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultContact, InlineQueryResultVoice, InputTextMessageContent, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_contact(): return InlineQueryResultContact( TestInlineQueryResultContactBase.id_, TestInlineQueryResultContactBase.phone_number, TestInlineQueryResultContactBase.first_name, last_name=TestInlineQueryResultContactBase.last_name, thumbnail_url=TestInlineQueryResultContactBase.thumbnail_url, thumbnail_width=TestInlineQueryResultContactBase.thumbnail_width, thumbnail_height=TestInlineQueryResultContactBase.thumbnail_height, input_message_content=TestInlineQueryResultContactBase.input_message_content, reply_markup=TestInlineQueryResultContactBase.reply_markup, ) class TestInlineQueryResultContactBase: id_ = "id" type_ = "contact" phone_number = "phone_number" first_name = "first_name" last_name = "last_name" thumbnail_url = "thumb url" thumbnail_width = 10 thumbnail_height = 15 input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultContactWithoutRequest(TestInlineQueryResultContactBase): def test_slot_behaviour(self, inline_query_result_contact): inst = inline_query_result_contact 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, inline_query_result_contact): assert inline_query_result_contact.id == self.id_ assert inline_query_result_contact.type == self.type_ assert inline_query_result_contact.phone_number == self.phone_number assert inline_query_result_contact.first_name == self.first_name assert inline_query_result_contact.last_name == self.last_name assert inline_query_result_contact.thumbnail_url == self.thumbnail_url assert inline_query_result_contact.thumbnail_width == self.thumbnail_width assert inline_query_result_contact.thumbnail_height == self.thumbnail_height assert ( inline_query_result_contact.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_contact.reply_markup.to_dict() == self.reply_markup.to_dict() def test_to_dict(self, inline_query_result_contact): inline_query_result_contact_dict = inline_query_result_contact.to_dict() assert isinstance(inline_query_result_contact_dict, dict) assert inline_query_result_contact_dict["id"] == inline_query_result_contact.id assert inline_query_result_contact_dict["type"] == inline_query_result_contact.type assert ( inline_query_result_contact_dict["phone_number"] == inline_query_result_contact.phone_number ) assert ( inline_query_result_contact_dict["first_name"] == inline_query_result_contact.first_name ) assert ( inline_query_result_contact_dict["last_name"] == inline_query_result_contact.last_name ) assert ( inline_query_result_contact_dict["thumbnail_url"] == inline_query_result_contact.thumbnail_url ) assert ( inline_query_result_contact_dict["thumbnail_width"] == inline_query_result_contact.thumbnail_width ) assert ( inline_query_result_contact_dict["thumbnail_height"] == inline_query_result_contact.thumbnail_height ) assert ( inline_query_result_contact_dict["input_message_content"] == inline_query_result_contact.input_message_content.to_dict() ) assert ( inline_query_result_contact_dict["reply_markup"] == inline_query_result_contact.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultContact(self.id_, self.phone_number, self.first_name) b = InlineQueryResultContact(self.id_, self.phone_number, self.first_name) c = InlineQueryResultContact(self.id_, "", self.first_name) d = InlineQueryResultContact("", self.phone_number, self.first_name) e = InlineQueryResultVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultdocument.py000066400000000000000000000160221460724040100272260ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultDocument, InlineQueryResultVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_document(): return InlineQueryResultDocument( TestInlineQueryResultDocumentBase.id_, TestInlineQueryResultDocumentBase.document_url, TestInlineQueryResultDocumentBase.title, TestInlineQueryResultDocumentBase.mime_type, caption=TestInlineQueryResultDocumentBase.caption, parse_mode=TestInlineQueryResultDocumentBase.parse_mode, caption_entities=TestInlineQueryResultDocumentBase.caption_entities, description=TestInlineQueryResultDocumentBase.description, thumbnail_url=TestInlineQueryResultDocumentBase.thumbnail_url, thumbnail_width=TestInlineQueryResultDocumentBase.thumbnail_width, thumbnail_height=TestInlineQueryResultDocumentBase.thumbnail_height, input_message_content=TestInlineQueryResultDocumentBase.input_message_content, reply_markup=TestInlineQueryResultDocumentBase.reply_markup, ) class TestInlineQueryResultDocumentBase: id_ = "id" type_ = "document" document_url = "document url" title = "title" caption = "caption" parse_mode = "Markdown" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] mime_type = "mime type" description = "description" thumbnail_url = "thumb url" thumbnail_width = 10 thumbnail_height = 15 input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultDocumentWithoutRequest(TestInlineQueryResultDocumentBase): def test_slot_behaviour(self, inline_query_result_document): inst = inline_query_result_document 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, inline_query_result_document): assert inline_query_result_document.id == self.id_ assert inline_query_result_document.type == self.type_ assert inline_query_result_document.document_url == self.document_url assert inline_query_result_document.title == self.title assert inline_query_result_document.caption == self.caption assert inline_query_result_document.parse_mode == self.parse_mode assert inline_query_result_document.caption_entities == tuple(self.caption_entities) assert inline_query_result_document.mime_type == self.mime_type assert inline_query_result_document.description == self.description assert inline_query_result_document.thumbnail_url == self.thumbnail_url assert inline_query_result_document.thumbnail_width == self.thumbnail_width assert inline_query_result_document.thumbnail_height == self.thumbnail_height assert ( inline_query_result_document.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_document.reply_markup.to_dict() == self.reply_markup.to_dict() def test_caption_entities_always_tuple(self): result = InlineQueryResultDocument(self.id_, self.document_url, self.title, self.mime_type) assert result.caption_entities == () def test_to_dict(self, inline_query_result_document): inline_query_result_document_dict = inline_query_result_document.to_dict() assert isinstance(inline_query_result_document_dict, dict) assert inline_query_result_document_dict["id"] == inline_query_result_document.id assert inline_query_result_document_dict["type"] == inline_query_result_document.type assert ( inline_query_result_document_dict["document_url"] == inline_query_result_document.document_url ) assert inline_query_result_document_dict["title"] == inline_query_result_document.title assert inline_query_result_document_dict["caption"] == inline_query_result_document.caption assert ( inline_query_result_document_dict["parse_mode"] == inline_query_result_document.parse_mode ) assert inline_query_result_document_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_document.caption_entities ] assert ( inline_query_result_document_dict["mime_type"] == inline_query_result_document.mime_type ) assert ( inline_query_result_document_dict["description"] == inline_query_result_document.description ) assert ( inline_query_result_document_dict["thumbnail_url"] == inline_query_result_document.thumbnail_url ) assert ( inline_query_result_document_dict["thumbnail_width"] == inline_query_result_document.thumbnail_width ) assert ( inline_query_result_document_dict["thumbnail_height"] == inline_query_result_document.thumbnail_height ) assert ( inline_query_result_document_dict["input_message_content"] == inline_query_result_document.input_message_content.to_dict() ) assert ( inline_query_result_document_dict["reply_markup"] == inline_query_result_document.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultDocument(self.id_, self.document_url, self.title, self.mime_type) b = InlineQueryResultDocument(self.id_, self.document_url, self.title, self.mime_type) c = InlineQueryResultDocument(self.id_, "", self.title, self.mime_type) d = InlineQueryResultDocument("", self.document_url, self.title, self.mime_type) e = InlineQueryResultVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultgame.py000066400000000000000000000065411460724040100263260ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultGame, InlineQueryResultVoice, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_game(): return InlineQueryResultGame( TestInlineQueryResultGameBase.id_, TestInlineQueryResultGameBase.game_short_name, reply_markup=TestInlineQueryResultGameBase.reply_markup, ) class TestInlineQueryResultGameBase: id_ = "id" type_ = "game" game_short_name = "game short name" reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultGameWithoutRequest(TestInlineQueryResultGameBase): def test_slot_behaviour(self, inline_query_result_game): inst = inline_query_result_game 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, inline_query_result_game): assert inline_query_result_game.type == self.type_ assert inline_query_result_game.id == self.id_ assert inline_query_result_game.game_short_name == self.game_short_name assert inline_query_result_game.reply_markup.to_dict() == self.reply_markup.to_dict() def test_to_dict(self, inline_query_result_game): inline_query_result_game_dict = inline_query_result_game.to_dict() assert isinstance(inline_query_result_game_dict, dict) assert inline_query_result_game_dict["type"] == inline_query_result_game.type assert inline_query_result_game_dict["id"] == inline_query_result_game.id assert ( inline_query_result_game_dict["game_short_name"] == inline_query_result_game.game_short_name ) assert ( inline_query_result_game_dict["reply_markup"] == inline_query_result_game.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultGame(self.id_, self.game_short_name) b = InlineQueryResultGame(self.id_, self.game_short_name) c = InlineQueryResultGame(self.id_, "") d = InlineQueryResultGame("", self.game_short_name) e = InlineQueryResultVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultgif.py000066400000000000000000000145551460724040100261660ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultGif, InlineQueryResultVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_gif(): return InlineQueryResultGif( TestInlineQueryResultGifBase.id_, TestInlineQueryResultGifBase.gif_url, TestInlineQueryResultGifBase.thumbnail_url, gif_width=TestInlineQueryResultGifBase.gif_width, gif_height=TestInlineQueryResultGifBase.gif_height, gif_duration=TestInlineQueryResultGifBase.gif_duration, title=TestInlineQueryResultGifBase.title, caption=TestInlineQueryResultGifBase.caption, parse_mode=TestInlineQueryResultGifBase.parse_mode, caption_entities=TestInlineQueryResultGifBase.caption_entities, input_message_content=TestInlineQueryResultGifBase.input_message_content, reply_markup=TestInlineQueryResultGifBase.reply_markup, thumbnail_mime_type=TestInlineQueryResultGifBase.thumbnail_mime_type, ) class TestInlineQueryResultGifBase: id_ = "id" type_ = "gif" gif_url = "gif url" gif_width = 10 gif_height = 15 gif_duration = 1 thumbnail_url = "thumb url" thumbnail_mime_type = "image/jpeg" title = "title" caption = "caption" parse_mode = "HTML" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultGifWithoutRequest(TestInlineQueryResultGifBase): def test_slot_behaviour(self, inline_query_result_gif): inst = inline_query_result_gif 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_caption_entities_always_tuple(self): result = InlineQueryResultGif(self.id_, self.gif_url, self.thumbnail_url) assert result.caption_entities == () def test_expected_values(self, inline_query_result_gif): assert inline_query_result_gif.type == self.type_ assert inline_query_result_gif.id == self.id_ assert inline_query_result_gif.gif_url == self.gif_url assert inline_query_result_gif.gif_width == self.gif_width assert inline_query_result_gif.gif_height == self.gif_height assert inline_query_result_gif.gif_duration == self.gif_duration assert inline_query_result_gif.thumbnail_url == self.thumbnail_url assert inline_query_result_gif.thumbnail_mime_type == self.thumbnail_mime_type assert inline_query_result_gif.title == self.title assert inline_query_result_gif.caption == self.caption assert inline_query_result_gif.parse_mode == self.parse_mode assert inline_query_result_gif.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_gif.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_gif.reply_markup.to_dict() == self.reply_markup.to_dict() def test_to_dict(self, inline_query_result_gif): inline_query_result_gif_dict = inline_query_result_gif.to_dict() assert isinstance(inline_query_result_gif_dict, dict) assert inline_query_result_gif_dict["type"] == inline_query_result_gif.type assert inline_query_result_gif_dict["id"] == inline_query_result_gif.id assert inline_query_result_gif_dict["gif_url"] == inline_query_result_gif.gif_url assert inline_query_result_gif_dict["gif_width"] == inline_query_result_gif.gif_width assert inline_query_result_gif_dict["gif_height"] == inline_query_result_gif.gif_height assert inline_query_result_gif_dict["gif_duration"] == inline_query_result_gif.gif_duration assert ( inline_query_result_gif_dict["thumbnail_url"] == inline_query_result_gif.thumbnail_url ) assert ( inline_query_result_gif_dict["thumbnail_mime_type"] == inline_query_result_gif.thumbnail_mime_type ) assert inline_query_result_gif_dict["title"] == inline_query_result_gif.title assert inline_query_result_gif_dict["caption"] == inline_query_result_gif.caption assert inline_query_result_gif_dict["parse_mode"] == inline_query_result_gif.parse_mode assert inline_query_result_gif_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_gif.caption_entities ] assert ( inline_query_result_gif_dict["input_message_content"] == inline_query_result_gif.input_message_content.to_dict() ) assert ( inline_query_result_gif_dict["reply_markup"] == inline_query_result_gif.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultGif(self.id_, self.gif_url, self.thumbnail_url) b = InlineQueryResultGif(self.id_, self.gif_url, self.thumbnail_url) c = InlineQueryResultGif(self.id_, "", self.thumbnail_url) d = InlineQueryResultGif("", self.gif_url, self.thumbnail_url) e = InlineQueryResultVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultlocation.py000066400000000000000000000154151460724040100272250ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultLocation, InlineQueryResultVoice, InputTextMessageContent, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_location(): return InlineQueryResultLocation( TestInlineQueryResultLocationBase.id_, TestInlineQueryResultLocationBase.latitude, TestInlineQueryResultLocationBase.longitude, TestInlineQueryResultLocationBase.title, live_period=TestInlineQueryResultLocationBase.live_period, thumbnail_url=TestInlineQueryResultLocationBase.thumbnail_url, thumbnail_width=TestInlineQueryResultLocationBase.thumbnail_width, thumbnail_height=TestInlineQueryResultLocationBase.thumbnail_height, input_message_content=TestInlineQueryResultLocationBase.input_message_content, reply_markup=TestInlineQueryResultLocationBase.reply_markup, horizontal_accuracy=TestInlineQueryResultLocationBase.horizontal_accuracy, heading=TestInlineQueryResultLocationBase.heading, proximity_alert_radius=TestInlineQueryResultLocationBase.proximity_alert_radius, ) class TestInlineQueryResultLocationBase: id_ = "id" type_ = "location" latitude = 0.0 longitude = 1.0 title = "title" horizontal_accuracy = 999 live_period = 70 heading = 90 proximity_alert_radius = 1000 thumbnail_url = "thumb url" thumbnail_width = 10 thumbnail_height = 15 input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultLocationWithoutRequest(TestInlineQueryResultLocationBase): def test_slot_behaviour(self, inline_query_result_location): inst = inline_query_result_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_expected_values(self, inline_query_result_location): assert inline_query_result_location.id == self.id_ assert inline_query_result_location.type == self.type_ assert inline_query_result_location.latitude == self.latitude assert inline_query_result_location.longitude == self.longitude assert inline_query_result_location.title == self.title assert inline_query_result_location.live_period == self.live_period assert inline_query_result_location.thumbnail_url == self.thumbnail_url assert inline_query_result_location.thumbnail_width == self.thumbnail_width assert inline_query_result_location.thumbnail_height == self.thumbnail_height assert ( inline_query_result_location.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_location.reply_markup.to_dict() == self.reply_markup.to_dict() assert inline_query_result_location.heading == self.heading assert inline_query_result_location.horizontal_accuracy == self.horizontal_accuracy assert inline_query_result_location.proximity_alert_radius == self.proximity_alert_radius def test_to_dict(self, inline_query_result_location): inline_query_result_location_dict = inline_query_result_location.to_dict() assert isinstance(inline_query_result_location_dict, dict) assert inline_query_result_location_dict["id"] == inline_query_result_location.id assert inline_query_result_location_dict["type"] == inline_query_result_location.type assert ( inline_query_result_location_dict["latitude"] == inline_query_result_location.latitude ) assert ( inline_query_result_location_dict["longitude"] == inline_query_result_location.longitude ) assert inline_query_result_location_dict["title"] == inline_query_result_location.title assert ( inline_query_result_location_dict["live_period"] == inline_query_result_location.live_period ) assert ( inline_query_result_location_dict["thumbnail_url"] == inline_query_result_location.thumbnail_url ) assert ( inline_query_result_location_dict["thumbnail_width"] == inline_query_result_location.thumbnail_width ) assert ( inline_query_result_location_dict["thumbnail_height"] == inline_query_result_location.thumbnail_height ) assert ( inline_query_result_location_dict["input_message_content"] == inline_query_result_location.input_message_content.to_dict() ) assert ( inline_query_result_location_dict["reply_markup"] == inline_query_result_location.reply_markup.to_dict() ) assert ( inline_query_result_location_dict["horizontal_accuracy"] == inline_query_result_location.horizontal_accuracy ) assert inline_query_result_location_dict["heading"] == inline_query_result_location.heading assert ( inline_query_result_location_dict["proximity_alert_radius"] == inline_query_result_location.proximity_alert_radius ) def test_equality(self): a = InlineQueryResultLocation(self.id_, self.longitude, self.latitude, self.title) b = InlineQueryResultLocation(self.id_, self.longitude, self.latitude, self.title) c = InlineQueryResultLocation(self.id_, 0, self.latitude, self.title) d = InlineQueryResultLocation("", self.longitude, self.latitude, self.title) e = InlineQueryResultVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultmpeg4gif.py000066400000000000000000000160561460724040100271210ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultMpeg4Gif, InlineQueryResultVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_mpeg4_gif(): return InlineQueryResultMpeg4Gif( TestInlineQueryResultMpeg4GifBase.id_, TestInlineQueryResultMpeg4GifBase.mpeg4_url, TestInlineQueryResultMpeg4GifBase.thumbnail_url, mpeg4_width=TestInlineQueryResultMpeg4GifBase.mpeg4_width, mpeg4_height=TestInlineQueryResultMpeg4GifBase.mpeg4_height, mpeg4_duration=TestInlineQueryResultMpeg4GifBase.mpeg4_duration, title=TestInlineQueryResultMpeg4GifBase.title, caption=TestInlineQueryResultMpeg4GifBase.caption, parse_mode=TestInlineQueryResultMpeg4GifBase.parse_mode, caption_entities=TestInlineQueryResultMpeg4GifBase.caption_entities, input_message_content=TestInlineQueryResultMpeg4GifBase.input_message_content, reply_markup=TestInlineQueryResultMpeg4GifBase.reply_markup, thumbnail_mime_type=TestInlineQueryResultMpeg4GifBase.thumbnail_mime_type, ) class TestInlineQueryResultMpeg4GifBase: id_ = "id" type_ = "mpeg4_gif" mpeg4_url = "mpeg4 url" mpeg4_width = 10 mpeg4_height = 15 mpeg4_duration = 1 thumbnail_url = "thumb url" thumbnail_mime_type = "image/jpeg" title = "title" caption = "caption" parse_mode = "Markdown" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultMpeg4GifWithoutRequest(TestInlineQueryResultMpeg4GifBase): def test_slot_behaviour(self, inline_query_result_mpeg4_gif): inst = inline_query_result_mpeg4_gif 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, inline_query_result_mpeg4_gif): assert inline_query_result_mpeg4_gif.type == self.type_ assert inline_query_result_mpeg4_gif.id == self.id_ assert inline_query_result_mpeg4_gif.mpeg4_url == self.mpeg4_url assert inline_query_result_mpeg4_gif.mpeg4_width == self.mpeg4_width assert inline_query_result_mpeg4_gif.mpeg4_height == self.mpeg4_height assert inline_query_result_mpeg4_gif.mpeg4_duration == self.mpeg4_duration assert inline_query_result_mpeg4_gif.thumbnail_url == self.thumbnail_url assert inline_query_result_mpeg4_gif.thumbnail_mime_type == self.thumbnail_mime_type assert inline_query_result_mpeg4_gif.title == self.title assert inline_query_result_mpeg4_gif.caption == self.caption assert inline_query_result_mpeg4_gif.parse_mode == self.parse_mode assert inline_query_result_mpeg4_gif.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_mpeg4_gif.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_mpeg4_gif.reply_markup.to_dict() == self.reply_markup.to_dict() def test_caption_entities_always_tuple(self): result = InlineQueryResultMpeg4Gif(self.id_, self.mpeg4_url, self.thumbnail_url) assert result.caption_entities == () def test_to_dict(self, inline_query_result_mpeg4_gif): inline_query_result_mpeg4_gif_dict = inline_query_result_mpeg4_gif.to_dict() assert isinstance(inline_query_result_mpeg4_gif_dict, dict) assert inline_query_result_mpeg4_gif_dict["type"] == inline_query_result_mpeg4_gif.type assert inline_query_result_mpeg4_gif_dict["id"] == inline_query_result_mpeg4_gif.id assert ( inline_query_result_mpeg4_gif_dict["mpeg4_url"] == inline_query_result_mpeg4_gif.mpeg4_url ) assert ( inline_query_result_mpeg4_gif_dict["mpeg4_width"] == inline_query_result_mpeg4_gif.mpeg4_width ) assert ( inline_query_result_mpeg4_gif_dict["mpeg4_height"] == inline_query_result_mpeg4_gif.mpeg4_height ) assert ( inline_query_result_mpeg4_gif_dict["mpeg4_duration"] == inline_query_result_mpeg4_gif.mpeg4_duration ) assert ( inline_query_result_mpeg4_gif_dict["thumbnail_url"] == inline_query_result_mpeg4_gif.thumbnail_url ) assert ( inline_query_result_mpeg4_gif_dict["thumbnail_mime_type"] == inline_query_result_mpeg4_gif.thumbnail_mime_type ) assert inline_query_result_mpeg4_gif_dict["title"] == inline_query_result_mpeg4_gif.title assert ( inline_query_result_mpeg4_gif_dict["caption"] == inline_query_result_mpeg4_gif.caption ) assert ( inline_query_result_mpeg4_gif_dict["parse_mode"] == inline_query_result_mpeg4_gif.parse_mode ) assert inline_query_result_mpeg4_gif_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_mpeg4_gif.caption_entities ] assert ( inline_query_result_mpeg4_gif_dict["input_message_content"] == inline_query_result_mpeg4_gif.input_message_content.to_dict() ) assert ( inline_query_result_mpeg4_gif_dict["reply_markup"] == inline_query_result_mpeg4_gif.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultMpeg4Gif(self.id_, self.mpeg4_url, self.thumbnail_url) b = InlineQueryResultMpeg4Gif(self.id_, self.mpeg4_url, self.thumbnail_url) c = InlineQueryResultMpeg4Gif(self.id_, "", self.thumbnail_url) d = InlineQueryResultMpeg4Gif("", self.mpeg4_url, self.thumbnail_url) e = InlineQueryResultVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultphoto.py000066400000000000000000000144571460724040100265530ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultPhoto, InlineQueryResultVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_photo(): return InlineQueryResultPhoto( TestInlineQueryResultPhotoBase.id_, TestInlineQueryResultPhotoBase.photo_url, TestInlineQueryResultPhotoBase.thumbnail_url, photo_width=TestInlineQueryResultPhotoBase.photo_width, photo_height=TestInlineQueryResultPhotoBase.photo_height, title=TestInlineQueryResultPhotoBase.title, description=TestInlineQueryResultPhotoBase.description, caption=TestInlineQueryResultPhotoBase.caption, parse_mode=TestInlineQueryResultPhotoBase.parse_mode, caption_entities=TestInlineQueryResultPhotoBase.caption_entities, input_message_content=TestInlineQueryResultPhotoBase.input_message_content, reply_markup=TestInlineQueryResultPhotoBase.reply_markup, ) class TestInlineQueryResultPhotoBase: id_ = "id" type_ = "photo" photo_url = "photo url" photo_width = 10 photo_height = 15 thumbnail_url = "thumb url" title = "title" description = "description" caption = "caption" parse_mode = "HTML" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultPhotoWithoutRequest(TestInlineQueryResultPhotoBase): def test_slot_behaviour(self, inline_query_result_photo): inst = inline_query_result_photo 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, inline_query_result_photo): assert inline_query_result_photo.type == self.type_ assert inline_query_result_photo.id == self.id_ assert inline_query_result_photo.photo_url == self.photo_url assert inline_query_result_photo.photo_width == self.photo_width assert inline_query_result_photo.photo_height == self.photo_height assert inline_query_result_photo.thumbnail_url == self.thumbnail_url assert inline_query_result_photo.title == self.title assert inline_query_result_photo.description == self.description assert inline_query_result_photo.caption == self.caption assert inline_query_result_photo.parse_mode == self.parse_mode assert inline_query_result_photo.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_photo.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_photo.reply_markup.to_dict() == self.reply_markup.to_dict() def test_caption_entities_always_tuple(self): result = InlineQueryResultPhoto(self.id_, self.photo_url, self.thumbnail_url) assert result.caption_entities == () def test_to_dict(self, inline_query_result_photo): inline_query_result_photo_dict = inline_query_result_photo.to_dict() assert isinstance(inline_query_result_photo_dict, dict) assert inline_query_result_photo_dict["type"] == inline_query_result_photo.type assert inline_query_result_photo_dict["id"] == inline_query_result_photo.id assert inline_query_result_photo_dict["photo_url"] == inline_query_result_photo.photo_url assert ( inline_query_result_photo_dict["photo_width"] == inline_query_result_photo.photo_width ) assert ( inline_query_result_photo_dict["photo_height"] == inline_query_result_photo.photo_height ) assert ( inline_query_result_photo_dict["thumbnail_url"] == inline_query_result_photo.thumbnail_url ) assert inline_query_result_photo_dict["title"] == inline_query_result_photo.title assert ( inline_query_result_photo_dict["description"] == inline_query_result_photo.description ) assert inline_query_result_photo_dict["caption"] == inline_query_result_photo.caption assert inline_query_result_photo_dict["parse_mode"] == inline_query_result_photo.parse_mode assert inline_query_result_photo_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_photo.caption_entities ] assert ( inline_query_result_photo_dict["input_message_content"] == inline_query_result_photo.input_message_content.to_dict() ) assert ( inline_query_result_photo_dict["reply_markup"] == inline_query_result_photo.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultPhoto(self.id_, self.photo_url, self.thumbnail_url) b = InlineQueryResultPhoto(self.id_, self.photo_url, self.thumbnail_url) c = InlineQueryResultPhoto(self.id_, "", self.thumbnail_url) d = InlineQueryResultPhoto("", self.photo_url, self.thumbnail_url) e = InlineQueryResultVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultvenue.py000066400000000000000000000156651460724040100265460ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultVenue, InlineQueryResultVoice, InputTextMessageContent, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_venue(): return InlineQueryResultVenue( TestInlineQueryResultVenueBase.id_, TestInlineQueryResultVenueBase.latitude, TestInlineQueryResultVenueBase.longitude, TestInlineQueryResultVenueBase.title, TestInlineQueryResultVenueBase.address, foursquare_id=TestInlineQueryResultVenueBase.foursquare_id, foursquare_type=TestInlineQueryResultVenueBase.foursquare_type, thumbnail_url=TestInlineQueryResultVenueBase.thumbnail_url, thumbnail_width=TestInlineQueryResultVenueBase.thumbnail_width, thumbnail_height=TestInlineQueryResultVenueBase.thumbnail_height, input_message_content=TestInlineQueryResultVenueBase.input_message_content, reply_markup=TestInlineQueryResultVenueBase.reply_markup, google_place_id=TestInlineQueryResultVenueBase.google_place_id, google_place_type=TestInlineQueryResultVenueBase.google_place_type, ) class TestInlineQueryResultVenueBase: id_ = "id" type_ = "venue" latitude = "latitude" longitude = "longitude" title = "title" address = "address" foursquare_id = "foursquare id" foursquare_type = "foursquare type" google_place_id = "google place id" google_place_type = "google place type" thumbnail_url = "thumb url" thumbnail_width = 10 thumbnail_height = 15 input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultVenueWithoutRequest(TestInlineQueryResultVenueBase): def test_slot_behaviour(self, inline_query_result_venue): inst = inline_query_result_venue 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, inline_query_result_venue): assert inline_query_result_venue.id == self.id_ assert inline_query_result_venue.type == self.type_ assert inline_query_result_venue.latitude == self.latitude assert inline_query_result_venue.longitude == self.longitude assert inline_query_result_venue.title == self.title assert inline_query_result_venue.address == self.address assert inline_query_result_venue.foursquare_id == self.foursquare_id assert inline_query_result_venue.foursquare_type == self.foursquare_type assert inline_query_result_venue.google_place_id == self.google_place_id assert inline_query_result_venue.google_place_type == self.google_place_type assert inline_query_result_venue.thumbnail_url == self.thumbnail_url assert inline_query_result_venue.thumbnail_width == self.thumbnail_width assert inline_query_result_venue.thumbnail_height == self.thumbnail_height assert ( inline_query_result_venue.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_venue.reply_markup.to_dict() == self.reply_markup.to_dict() def test_to_dict(self, inline_query_result_venue): inline_query_result_venue_dict = inline_query_result_venue.to_dict() assert isinstance(inline_query_result_venue_dict, dict) assert inline_query_result_venue_dict["id"] == inline_query_result_venue.id assert inline_query_result_venue_dict["type"] == inline_query_result_venue.type assert inline_query_result_venue_dict["latitude"] == inline_query_result_venue.latitude assert inline_query_result_venue_dict["longitude"] == inline_query_result_venue.longitude assert inline_query_result_venue_dict["title"] == inline_query_result_venue.title assert inline_query_result_venue_dict["address"] == inline_query_result_venue.address assert ( inline_query_result_venue_dict["foursquare_id"] == inline_query_result_venue.foursquare_id ) assert ( inline_query_result_venue_dict["foursquare_type"] == inline_query_result_venue.foursquare_type ) assert ( inline_query_result_venue_dict["google_place_id"] == inline_query_result_venue.google_place_id ) assert ( inline_query_result_venue_dict["google_place_type"] == inline_query_result_venue.google_place_type ) assert ( inline_query_result_venue_dict["thumbnail_url"] == inline_query_result_venue.thumbnail_url ) assert ( inline_query_result_venue_dict["thumbnail_width"] == inline_query_result_venue.thumbnail_width ) assert ( inline_query_result_venue_dict["thumbnail_height"] == inline_query_result_venue.thumbnail_height ) assert ( inline_query_result_venue_dict["input_message_content"] == inline_query_result_venue.input_message_content.to_dict() ) assert ( inline_query_result_venue_dict["reply_markup"] == inline_query_result_venue.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultVenue( self.id_, self.longitude, self.latitude, self.title, self.address ) b = InlineQueryResultVenue( self.id_, self.longitude, self.latitude, self.title, self.address ) c = InlineQueryResultVenue(self.id_, "", self.latitude, self.title, self.address) d = InlineQueryResultVenue("", self.longitude, self.latitude, self.title, self.address) e = InlineQueryResultVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultvideo.py000066400000000000000000000161041460724040100265170ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultVideo, InlineQueryResultVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_video(): return InlineQueryResultVideo( TestInlineQueryResultVideoBase.id_, TestInlineQueryResultVideoBase.video_url, TestInlineQueryResultVideoBase.mime_type, TestInlineQueryResultVideoBase.thumbnail_url, TestInlineQueryResultVideoBase.title, video_width=TestInlineQueryResultVideoBase.video_width, video_height=TestInlineQueryResultVideoBase.video_height, video_duration=TestInlineQueryResultVideoBase.video_duration, caption=TestInlineQueryResultVideoBase.caption, parse_mode=TestInlineQueryResultVideoBase.parse_mode, caption_entities=TestInlineQueryResultVideoBase.caption_entities, description=TestInlineQueryResultVideoBase.description, input_message_content=TestInlineQueryResultVideoBase.input_message_content, reply_markup=TestInlineQueryResultVideoBase.reply_markup, ) class TestInlineQueryResultVideoBase: id_ = "id" type_ = "video" video_url = "video url" mime_type = "mime type" video_width = 10 video_height = 15 video_duration = 15 thumbnail_url = "thumbnail url" title = "title" caption = "caption" parse_mode = "Markdown" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] description = "description" input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultVideoWithoutRequest(TestInlineQueryResultVideoBase): def test_slot_behaviour(self, inline_query_result_video): inst = inline_query_result_video 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, inline_query_result_video): assert inline_query_result_video.type == self.type_ assert inline_query_result_video.id == self.id_ assert inline_query_result_video.video_url == self.video_url assert inline_query_result_video.mime_type == self.mime_type assert inline_query_result_video.video_width == self.video_width assert inline_query_result_video.video_height == self.video_height assert inline_query_result_video.video_duration == self.video_duration assert inline_query_result_video.thumbnail_url == self.thumbnail_url assert inline_query_result_video.title == self.title assert inline_query_result_video.description == self.description assert inline_query_result_video.caption == self.caption assert inline_query_result_video.parse_mode == self.parse_mode assert inline_query_result_video.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_video.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_video.reply_markup.to_dict() == self.reply_markup.to_dict() def test_caption_entities_always_tuple(self): video = InlineQueryResultVideo( self.id_, self.video_url, self.mime_type, self.thumbnail_url, self.title ) assert video.caption_entities == () def test_to_dict(self, inline_query_result_video): inline_query_result_video_dict = inline_query_result_video.to_dict() assert isinstance(inline_query_result_video_dict, dict) assert inline_query_result_video_dict["type"] == inline_query_result_video.type assert inline_query_result_video_dict["id"] == inline_query_result_video.id assert inline_query_result_video_dict["video_url"] == inline_query_result_video.video_url assert inline_query_result_video_dict["mime_type"] == inline_query_result_video.mime_type assert ( inline_query_result_video_dict["video_width"] == inline_query_result_video.video_width ) assert ( inline_query_result_video_dict["video_height"] == inline_query_result_video.video_height ) assert ( inline_query_result_video_dict["video_duration"] == inline_query_result_video.video_duration ) assert ( inline_query_result_video_dict["thumbnail_url"] == inline_query_result_video.thumbnail_url ) assert inline_query_result_video_dict["title"] == inline_query_result_video.title assert ( inline_query_result_video_dict["description"] == inline_query_result_video.description ) assert inline_query_result_video_dict["caption"] == inline_query_result_video.caption assert inline_query_result_video_dict["parse_mode"] == inline_query_result_video.parse_mode assert inline_query_result_video_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_video.caption_entities ] assert ( inline_query_result_video_dict["input_message_content"] == inline_query_result_video.input_message_content.to_dict() ) assert ( inline_query_result_video_dict["reply_markup"] == inline_query_result_video.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultVideo( self.id_, self.video_url, self.mime_type, self.thumbnail_url, self.title ) b = InlineQueryResultVideo( self.id_, self.video_url, self.mime_type, self.thumbnail_url, self.title ) c = InlineQueryResultVideo(self.id_, "", self.mime_type, self.thumbnail_url, self.title) d = InlineQueryResultVideo( "", self.video_url, self.mime_type, self.thumbnail_url, self.title ) e = InlineQueryResultVoice(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) python-telegram-bot-21.1.1/tests/_inline/test_inlinequeryresultvoice.py000066400000000000000000000127531460724040100265240ustar00rootroot00000000000000#!/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, InlineKeyboardMarkup, InlineQueryResultAudio, InlineQueryResultVoice, InputTextMessageContent, MessageEntity, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inline_query_result_voice(): return InlineQueryResultVoice( id=TestInlineQueryResultVoiceBase.id_, voice_url=TestInlineQueryResultVoiceBase.voice_url, title=TestInlineQueryResultVoiceBase.title, voice_duration=TestInlineQueryResultVoiceBase.voice_duration, caption=TestInlineQueryResultVoiceBase.caption, parse_mode=TestInlineQueryResultVoiceBase.parse_mode, caption_entities=TestInlineQueryResultVoiceBase.caption_entities, input_message_content=TestInlineQueryResultVoiceBase.input_message_content, reply_markup=TestInlineQueryResultVoiceBase.reply_markup, ) class TestInlineQueryResultVoiceBase: id_ = "id" type_ = "voice" voice_url = "voice url" title = "title" voice_duration = "voice_duration" caption = "caption" parse_mode = "HTML" caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] input_message_content = InputTextMessageContent("input_message_content") reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]]) class TestInlineQueryResultVoiceWithoutRequest(TestInlineQueryResultVoiceBase): def test_slot_behaviour(self, inline_query_result_voice): inst = inline_query_result_voice 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, inline_query_result_voice): assert inline_query_result_voice.type == self.type_ assert inline_query_result_voice.id == self.id_ assert inline_query_result_voice.voice_url == self.voice_url assert inline_query_result_voice.title == self.title assert inline_query_result_voice.voice_duration == self.voice_duration assert inline_query_result_voice.caption == self.caption assert inline_query_result_voice.parse_mode == self.parse_mode assert inline_query_result_voice.caption_entities == tuple(self.caption_entities) assert ( inline_query_result_voice.input_message_content.to_dict() == self.input_message_content.to_dict() ) assert inline_query_result_voice.reply_markup.to_dict() == self.reply_markup.to_dict() def test_caption_entities_always_tuple(self): result = InlineQueryResultVoice( self.id_, self.voice_url, self.title, ) assert result.caption_entities == () def test_to_dict(self, inline_query_result_voice): inline_query_result_voice_dict = inline_query_result_voice.to_dict() assert isinstance(inline_query_result_voice_dict, dict) assert inline_query_result_voice_dict["type"] == inline_query_result_voice.type assert inline_query_result_voice_dict["id"] == inline_query_result_voice.id assert inline_query_result_voice_dict["voice_url"] == inline_query_result_voice.voice_url assert inline_query_result_voice_dict["title"] == inline_query_result_voice.title assert ( inline_query_result_voice_dict["voice_duration"] == inline_query_result_voice.voice_duration ) assert inline_query_result_voice_dict["caption"] == inline_query_result_voice.caption assert inline_query_result_voice_dict["parse_mode"] == inline_query_result_voice.parse_mode assert inline_query_result_voice_dict["caption_entities"] == [ ce.to_dict() for ce in inline_query_result_voice.caption_entities ] assert ( inline_query_result_voice_dict["input_message_content"] == inline_query_result_voice.input_message_content.to_dict() ) assert ( inline_query_result_voice_dict["reply_markup"] == inline_query_result_voice.reply_markup.to_dict() ) def test_equality(self): a = InlineQueryResultVoice(self.id_, self.voice_url, self.title) b = InlineQueryResultVoice(self.id_, self.voice_url, self.title) c = InlineQueryResultVoice(self.id_, "", self.title) d = InlineQueryResultVoice("", self.voice_url, self.title) e = InlineQueryResultAudio(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) python-telegram-bot-21.1.1/tests/_inline/test_inputcontactmessagecontent.py000066400000000000000000000061201460724040100273350ustar00rootroot00000000000000#!/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 InputContactMessageContent, User from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def input_contact_message_content(): return InputContactMessageContent( TestInputContactMessageContentBase.phone_number, TestInputContactMessageContentBase.first_name, TestInputContactMessageContentBase.last_name, ) class TestInputContactMessageContentBase: phone_number = "phone number" first_name = "first name" last_name = "last name" class TestInputContactMessageContentWithoutRequest(TestInputContactMessageContentBase): def test_slot_behaviour(self, input_contact_message_content): inst = input_contact_message_content 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, input_contact_message_content): assert input_contact_message_content.first_name == self.first_name assert input_contact_message_content.phone_number == self.phone_number assert input_contact_message_content.last_name == self.last_name def test_to_dict(self, input_contact_message_content): input_contact_message_content_dict = input_contact_message_content.to_dict() assert isinstance(input_contact_message_content_dict, dict) assert ( input_contact_message_content_dict["phone_number"] == input_contact_message_content.phone_number ) assert ( input_contact_message_content_dict["first_name"] == input_contact_message_content.first_name ) assert ( input_contact_message_content_dict["last_name"] == input_contact_message_content.last_name ) def test_equality(self): a = InputContactMessageContent("phone", "first", last_name="last") b = InputContactMessageContent("phone", "first_name", vcard="vcard") c = InputContactMessageContent("phone_number", "first", vcard="vcard") d = User(123, "first", 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) python-telegram-bot-21.1.1/tests/_inline/test_inputinvoicemessagecontent.py000066400000000000000000000325531460724040100273470ustar00rootroot00000000000000#!/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 InputInvoiceMessageContent, InputTextMessageContent, LabeledPrice from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def input_invoice_message_content(): return InputInvoiceMessageContent( title=TestInputInvoiceMessageContentBase.title, description=TestInputInvoiceMessageContentBase.description, payload=TestInputInvoiceMessageContentBase.payload, provider_token=TestInputInvoiceMessageContentBase.provider_token, currency=TestInputInvoiceMessageContentBase.currency, prices=TestInputInvoiceMessageContentBase.prices, max_tip_amount=TestInputInvoiceMessageContentBase.max_tip_amount, suggested_tip_amounts=TestInputInvoiceMessageContentBase.suggested_tip_amounts, provider_data=TestInputInvoiceMessageContentBase.provider_data, photo_url=TestInputInvoiceMessageContentBase.photo_url, photo_size=TestInputInvoiceMessageContentBase.photo_size, photo_width=TestInputInvoiceMessageContentBase.photo_width, photo_height=TestInputInvoiceMessageContentBase.photo_height, need_name=TestInputInvoiceMessageContentBase.need_name, need_phone_number=TestInputInvoiceMessageContentBase.need_phone_number, need_email=TestInputInvoiceMessageContentBase.need_email, need_shipping_address=TestInputInvoiceMessageContentBase.need_shipping_address, send_phone_number_to_provider=( TestInputInvoiceMessageContentBase.send_phone_number_to_provider ), send_email_to_provider=TestInputInvoiceMessageContentBase.send_email_to_provider, is_flexible=TestInputInvoiceMessageContentBase.is_flexible, ) class TestInputInvoiceMessageContentBase: title = "invoice title" description = "invoice description" payload = "invoice payload" provider_token = "provider token" currency = "PTBCoin" prices = [LabeledPrice("label1", 42), LabeledPrice("label2", 314)] max_tip_amount = 420 suggested_tip_amounts = [314, 256] provider_data = "provider data" photo_url = "photo_url" photo_size = 314 photo_width = 420 photo_height = 256 need_name = True need_phone_number = True need_email = True need_shipping_address = True send_phone_number_to_provider = True send_email_to_provider = True is_flexible = True class TestInputInvoiceMessageContentWithoutRequest(TestInputInvoiceMessageContentBase): def test_slot_behaviour(self, input_invoice_message_content): inst = input_invoice_message_content 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, input_invoice_message_content): assert input_invoice_message_content.title == self.title assert input_invoice_message_content.description == self.description assert input_invoice_message_content.payload == self.payload assert input_invoice_message_content.provider_token == self.provider_token assert input_invoice_message_content.currency == self.currency assert input_invoice_message_content.prices == tuple(self.prices) assert input_invoice_message_content.max_tip_amount == self.max_tip_amount assert input_invoice_message_content.suggested_tip_amounts == tuple( int(amount) for amount in self.suggested_tip_amounts ) assert input_invoice_message_content.provider_data == self.provider_data assert input_invoice_message_content.photo_url == self.photo_url assert input_invoice_message_content.photo_size == int(self.photo_size) assert input_invoice_message_content.photo_width == int(self.photo_width) assert input_invoice_message_content.photo_height == int(self.photo_height) assert input_invoice_message_content.need_name == self.need_name assert input_invoice_message_content.need_phone_number == self.need_phone_number assert input_invoice_message_content.need_email == self.need_email assert input_invoice_message_content.need_shipping_address == self.need_shipping_address assert ( input_invoice_message_content.send_phone_number_to_provider == self.send_phone_number_to_provider ) assert input_invoice_message_content.send_email_to_provider == self.send_email_to_provider assert input_invoice_message_content.is_flexible == self.is_flexible def test_suggested_tip_amonuts_always_tuple(self, input_invoice_message_content): assert isinstance(input_invoice_message_content.suggested_tip_amounts, tuple) assert input_invoice_message_content.suggested_tip_amounts == tuple( int(amount) for amount in self.suggested_tip_amounts ) input_invoice_message_content = InputInvoiceMessageContent( title=self.title, description=self.description, payload=self.payload, provider_token=self.provider_token, currency=self.currency, prices=self.prices, ) assert input_invoice_message_content.suggested_tip_amounts == () def test_to_dict(self, input_invoice_message_content): input_invoice_message_content_dict = input_invoice_message_content.to_dict() assert isinstance(input_invoice_message_content_dict, dict) assert input_invoice_message_content_dict["title"] == input_invoice_message_content.title assert ( input_invoice_message_content_dict["description"] == input_invoice_message_content.description ) assert ( input_invoice_message_content_dict["payload"] == input_invoice_message_content.payload ) assert ( input_invoice_message_content_dict["provider_token"] == input_invoice_message_content.provider_token ) assert ( input_invoice_message_content_dict["currency"] == input_invoice_message_content.currency ) assert input_invoice_message_content_dict["prices"] == [ price.to_dict() for price in input_invoice_message_content.prices ] assert ( input_invoice_message_content_dict["max_tip_amount"] == input_invoice_message_content.max_tip_amount ) assert input_invoice_message_content_dict["suggested_tip_amounts"] == list( input_invoice_message_content.suggested_tip_amounts ) assert ( input_invoice_message_content_dict["provider_data"] == input_invoice_message_content.provider_data ) assert ( input_invoice_message_content_dict["photo_url"] == input_invoice_message_content.photo_url ) assert ( input_invoice_message_content_dict["photo_size"] == input_invoice_message_content.photo_size ) assert ( input_invoice_message_content_dict["photo_width"] == input_invoice_message_content.photo_width ) assert ( input_invoice_message_content_dict["photo_height"] == input_invoice_message_content.photo_height ) assert ( input_invoice_message_content_dict["need_name"] == input_invoice_message_content.need_name ) assert ( input_invoice_message_content_dict["need_phone_number"] == input_invoice_message_content.need_phone_number ) assert ( input_invoice_message_content_dict["need_email"] == input_invoice_message_content.need_email ) assert ( input_invoice_message_content_dict["need_shipping_address"] == input_invoice_message_content.need_shipping_address ) assert ( input_invoice_message_content_dict["send_phone_number_to_provider"] == input_invoice_message_content.send_phone_number_to_provider ) assert ( input_invoice_message_content_dict["send_email_to_provider"] == input_invoice_message_content.send_email_to_provider ) assert ( input_invoice_message_content_dict["is_flexible"] == input_invoice_message_content.is_flexible ) def test_de_json(self, bot): assert InputInvoiceMessageContent.de_json({}, bot=bot) is None json_dict = { "title": self.title, "description": self.description, "payload": self.payload, "provider_token": self.provider_token, "currency": self.currency, "prices": [price.to_dict() for price in self.prices], "max_tip_amount": self.max_tip_amount, "suggested_tip_amounts": self.suggested_tip_amounts, "provider_data": self.provider_data, "photo_url": self.photo_url, "photo_size": self.photo_size, "photo_width": self.photo_width, "photo_height": self.photo_height, "need_name": self.need_name, "need_phone_number": self.need_phone_number, "need_email": self.need_email, "need_shipping_address": self.need_shipping_address, "send_phone_number_to_provider": self.send_phone_number_to_provider, "send_email_to_provider": self.send_email_to_provider, "is_flexible": self.is_flexible, } input_invoice_message_content = InputInvoiceMessageContent.de_json(json_dict, bot=bot) assert input_invoice_message_content.api_kwargs == {} assert input_invoice_message_content.title == self.title assert input_invoice_message_content.description == self.description assert input_invoice_message_content.payload == self.payload assert input_invoice_message_content.provider_token == self.provider_token assert input_invoice_message_content.currency == self.currency assert input_invoice_message_content.prices == tuple(self.prices) assert input_invoice_message_content.max_tip_amount == self.max_tip_amount assert input_invoice_message_content.suggested_tip_amounts == tuple( int(amount) for amount in self.suggested_tip_amounts ) assert input_invoice_message_content.provider_data == self.provider_data assert input_invoice_message_content.photo_url == self.photo_url assert input_invoice_message_content.photo_size == int(self.photo_size) assert input_invoice_message_content.photo_width == int(self.photo_width) assert input_invoice_message_content.photo_height == int(self.photo_height) assert input_invoice_message_content.need_name == self.need_name assert input_invoice_message_content.need_phone_number == self.need_phone_number assert input_invoice_message_content.need_email == self.need_email assert input_invoice_message_content.need_shipping_address == self.need_shipping_address assert ( input_invoice_message_content.send_phone_number_to_provider == self.send_phone_number_to_provider ) assert input_invoice_message_content.send_email_to_provider == self.send_email_to_provider assert input_invoice_message_content.is_flexible == self.is_flexible def test_equality(self): a = InputInvoiceMessageContent( self.title, self.description, self.payload, self.provider_token, self.currency, self.prices, ) b = InputInvoiceMessageContent( self.title, self.description, self.payload, self.provider_token, self.currency, self.prices, max_tip_amount=100, provider_data="foobar", ) c = InputInvoiceMessageContent( self.title, self.description, self.payload, self.provider_token, self.currency, # the first prices amount & the second lebal changed [LabeledPrice("label1", 24), LabeledPrice("label22", 314)], ) d = InputInvoiceMessageContent( self.title, self.description, "different_payload", self.provider_token, self.currency, self.prices, ) e = InputTextMessageContent("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) assert a != e assert hash(a) != hash(e) python-telegram-bot-21.1.1/tests/_inline/test_inputlocationmessagecontent.py000066400000000000000000000100541460724040100275130ustar00rootroot00000000000000#!/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 InputLocationMessageContent, Location from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def input_location_message_content(): return InputLocationMessageContent( TestInputLocationMessageContentBase.latitude, TestInputLocationMessageContentBase.longitude, live_period=TestInputLocationMessageContentBase.live_period, horizontal_accuracy=TestInputLocationMessageContentBase.horizontal_accuracy, heading=TestInputLocationMessageContentBase.heading, proximity_alert_radius=TestInputLocationMessageContentBase.proximity_alert_radius, ) class TestInputLocationMessageContentBase: latitude = -23.691288 longitude = -46.788279 live_period = 80 horizontal_accuracy = 50.5 heading = 90 proximity_alert_radius = 999 class TestInputLocationMessageContentWithoutRequest(TestInputLocationMessageContentBase): def test_slot_behaviour(self, input_location_message_content): inst = input_location_message_content 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, input_location_message_content): assert input_location_message_content.longitude == self.longitude assert input_location_message_content.latitude == self.latitude assert input_location_message_content.live_period == self.live_period assert input_location_message_content.horizontal_accuracy == self.horizontal_accuracy assert input_location_message_content.heading == self.heading assert input_location_message_content.proximity_alert_radius == self.proximity_alert_radius def test_to_dict(self, input_location_message_content): input_location_message_content_dict = input_location_message_content.to_dict() assert isinstance(input_location_message_content_dict, dict) assert ( input_location_message_content_dict["latitude"] == input_location_message_content.latitude ) assert ( input_location_message_content_dict["longitude"] == input_location_message_content.longitude ) assert ( input_location_message_content_dict["live_period"] == input_location_message_content.live_period ) assert ( input_location_message_content_dict["horizontal_accuracy"] == input_location_message_content.horizontal_accuracy ) assert ( input_location_message_content_dict["heading"] == input_location_message_content.heading ) assert ( input_location_message_content_dict["proximity_alert_radius"] == input_location_message_content.proximity_alert_radius ) def test_equality(self): a = InputLocationMessageContent(123, 456, 70) b = InputLocationMessageContent(123, 456, 90) c = InputLocationMessageContent(123, 457, 70) d = Location(123, 456) assert a == b assert hash(a) == hash(b) assert a != c assert hash(a) != hash(c) assert a != d assert hash(a) != hash(d) python-telegram-bot-21.1.1/tests/_inline/test_inputtextmessagecontent.py000066400000000000000000000102261460724040100266700ustar00rootroot00000000000000#!/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 InputTextMessageContent, LinkPreviewOptions, MessageEntity from telegram.constants import ParseMode from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def input_text_message_content(): return InputTextMessageContent( TestInputTextMessageContentBase.message_text, parse_mode=TestInputTextMessageContentBase.parse_mode, entities=TestInputTextMessageContentBase.entities, link_preview_options=TestInputTextMessageContentBase.link_preview_options, ) class TestInputTextMessageContentBase: message_text = "*message text*" parse_mode = ParseMode.MARKDOWN entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] disable_web_page_preview = False link_preview_options = LinkPreviewOptions(False, url="https://python-telegram-bot.org") class TestInputTextMessageContentWithoutRequest(TestInputTextMessageContentBase): def test_slot_behaviour(self, input_text_message_content): inst = input_text_message_content 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, input_text_message_content): assert input_text_message_content.parse_mode == self.parse_mode assert input_text_message_content.message_text == self.message_text assert input_text_message_content.entities == tuple(self.entities) assert input_text_message_content.link_preview_options == self.link_preview_options def test_entities_always_tuple(self): input_text_message_content = InputTextMessageContent("text") assert input_text_message_content.entities == () def test_to_dict(self, input_text_message_content): input_text_message_content_dict = input_text_message_content.to_dict() assert isinstance(input_text_message_content_dict, dict) assert ( input_text_message_content_dict["message_text"] == input_text_message_content.message_text ) assert ( input_text_message_content_dict["parse_mode"] == input_text_message_content.parse_mode ) assert input_text_message_content_dict["entities"] == [ ce.to_dict() for ce in input_text_message_content.entities ] assert ( input_text_message_content_dict["link_preview_options"] == input_text_message_content.link_preview_options.to_dict() ) def test_equality(self): a = InputTextMessageContent("text") b = InputTextMessageContent("text", parse_mode=ParseMode.HTML) c = InputTextMessageContent("label") d = ParseMode.HTML 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_mutually_exclusive(self): with pytest.raises(ValueError, match="`link_preview_options` are mutually exclusive"): InputTextMessageContent( "text", disable_web_page_preview=True, link_preview_options=LinkPreviewOptions() ) def test_disable_web_page_preview_deprecation(self): itmc = InputTextMessageContent("text", disable_web_page_preview=True) assert itmc.link_preview_options.is_disabled is True python-telegram-bot-21.1.1/tests/_inline/test_inputvenuemessagecontent.py000066400000000000000000000111211460724040100270210ustar00rootroot00000000000000#!/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 InputVenueMessageContent, Location from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def input_venue_message_content(): return InputVenueMessageContent( TestInputVenueMessageContentBase.latitude, TestInputVenueMessageContentBase.longitude, TestInputVenueMessageContentBase.title, TestInputVenueMessageContentBase.address, foursquare_id=TestInputVenueMessageContentBase.foursquare_id, foursquare_type=TestInputVenueMessageContentBase.foursquare_type, google_place_id=TestInputVenueMessageContentBase.google_place_id, google_place_type=TestInputVenueMessageContentBase.google_place_type, ) class TestInputVenueMessageContentBase: latitude = 1.0 longitude = 2.0 title = "title" address = "address" foursquare_id = "foursquare id" foursquare_type = "foursquare type" google_place_id = "google place id" google_place_type = "google place type" class TestInputVenueMessageContentWithoutRequest(TestInputVenueMessageContentBase): def test_slot_behaviour(self, input_venue_message_content): inst = input_venue_message_content 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, input_venue_message_content): assert input_venue_message_content.longitude == self.longitude assert input_venue_message_content.latitude == self.latitude assert input_venue_message_content.title == self.title assert input_venue_message_content.address == self.address assert input_venue_message_content.foursquare_id == self.foursquare_id assert input_venue_message_content.foursquare_type == self.foursquare_type assert input_venue_message_content.google_place_id == self.google_place_id assert input_venue_message_content.google_place_type == self.google_place_type def test_to_dict(self, input_venue_message_content): input_venue_message_content_dict = input_venue_message_content.to_dict() assert isinstance(input_venue_message_content_dict, dict) assert input_venue_message_content_dict["latitude"] == input_venue_message_content.latitude assert ( input_venue_message_content_dict["longitude"] == input_venue_message_content.longitude ) assert input_venue_message_content_dict["title"] == input_venue_message_content.title assert input_venue_message_content_dict["address"] == input_venue_message_content.address assert ( input_venue_message_content_dict["foursquare_id"] == input_venue_message_content.foursquare_id ) assert ( input_venue_message_content_dict["foursquare_type"] == input_venue_message_content.foursquare_type ) assert ( input_venue_message_content_dict["google_place_id"] == input_venue_message_content.google_place_id ) assert ( input_venue_message_content_dict["google_place_type"] == input_venue_message_content.google_place_type ) def test_equality(self): a = InputVenueMessageContent(123, 456, "title", "address") b = InputVenueMessageContent(123, 456, "title", "") c = InputVenueMessageContent(123, 456, "title", "address", foursquare_id=123) d = InputVenueMessageContent(456, 123, "title", "address", foursquare_id=123) e = Location(123, 456) 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) python-telegram-bot-21.1.1/tests/_passport/000077500000000000000000000000001460724040100206475ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/_passport/__init__.py000066400000000000000000000014661460724040100227670ustar00rootroot00000000000000#!/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/]. python-telegram-bot-21.1.1/tests/_passport/test_encryptedcredentials.py000066400000000000000000000056511460724040100265020ustar00rootroot00000000000000#!/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 EncryptedCredentials, PassportElementError from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def encrypted_credentials(): return EncryptedCredentials( TestEncryptedCredentialsBase.data, TestEncryptedCredentialsBase.hash, TestEncryptedCredentialsBase.secret, ) class TestEncryptedCredentialsBase: data = "data" hash = "hash" secret = "secret" class TestEncryptedCredentialsWithoutRequest(TestEncryptedCredentialsBase): def test_slot_behaviour(self, encrypted_credentials): inst = encrypted_credentials 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, encrypted_credentials): assert encrypted_credentials.data == self.data assert encrypted_credentials.hash == self.hash assert encrypted_credentials.secret == self.secret def test_to_dict(self, encrypted_credentials): encrypted_credentials_dict = encrypted_credentials.to_dict() assert isinstance(encrypted_credentials_dict, dict) assert encrypted_credentials_dict["data"] == encrypted_credentials.data assert encrypted_credentials_dict["hash"] == encrypted_credentials.hash assert encrypted_credentials_dict["secret"] == encrypted_credentials.secret def test_equality(self): a = EncryptedCredentials(self.data, self.hash, self.secret) b = EncryptedCredentials(self.data, self.hash, self.secret) c = EncryptedCredentials(self.data, "", "") d = EncryptedCredentials("", self.hash, "") e = EncryptedCredentials("", "", self.secret) f = PassportElementError("source", "type", "message") 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) python-telegram-bot-21.1.1/tests/_passport/test_encryptedpassportelement.py000066400000000000000000000112541460724040100274260ustar00rootroot00000000000000#!/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 EncryptedPassportElement, PassportElementError, PassportFile from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def encrypted_passport_element(): return EncryptedPassportElement( TestEncryptedPassportElementBase.type_, "this is a hash", data=TestEncryptedPassportElementBase.data, phone_number=TestEncryptedPassportElementBase.phone_number, email=TestEncryptedPassportElementBase.email, files=TestEncryptedPassportElementBase.files, front_side=TestEncryptedPassportElementBase.front_side, reverse_side=TestEncryptedPassportElementBase.reverse_side, selfie=TestEncryptedPassportElementBase.selfie, ) class TestEncryptedPassportElementBase: type_ = "type" hash = "this is a hash" data = "data" phone_number = "phone_number" email = "email" files = [PassportFile("file_id", 50, 0, 25)] front_side = PassportFile("file_id", 50, 0, 25) reverse_side = PassportFile("file_id", 50, 0, 25) selfie = PassportFile("file_id", 50, 0, 25) class TestEncryptedPassportElementWithoutRequest(TestEncryptedPassportElementBase): def test_slot_behaviour(self, encrypted_passport_element): inst = encrypted_passport_element 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, encrypted_passport_element): assert encrypted_passport_element.type == self.type_ assert encrypted_passport_element.hash == self.hash assert encrypted_passport_element.data == self.data assert encrypted_passport_element.phone_number == self.phone_number assert encrypted_passport_element.email == self.email assert encrypted_passport_element.files == tuple(self.files) assert encrypted_passport_element.front_side == self.front_side assert encrypted_passport_element.reverse_side == self.reverse_side assert encrypted_passport_element.selfie == self.selfie def test_to_dict(self, encrypted_passport_element): encrypted_passport_element_dict = encrypted_passport_element.to_dict() assert isinstance(encrypted_passport_element_dict, dict) assert encrypted_passport_element_dict["type"] == encrypted_passport_element.type assert encrypted_passport_element_dict["data"] == encrypted_passport_element.data assert ( encrypted_passport_element_dict["phone_number"] == encrypted_passport_element.phone_number ) assert encrypted_passport_element_dict["email"] == encrypted_passport_element.email assert isinstance(encrypted_passport_element_dict["files"], list) assert ( encrypted_passport_element_dict["front_side"] == encrypted_passport_element.front_side.to_dict() ) assert ( encrypted_passport_element_dict["reverse_side"] == encrypted_passport_element.reverse_side.to_dict() ) assert ( encrypted_passport_element_dict["selfie"] == encrypted_passport_element.selfie.to_dict() ) def test_attributes_always_tuple(self): element = EncryptedPassportElement(self.type_, self.hash) assert element.files == () assert element.translation == () def test_equality(self): a = EncryptedPassportElement(self.type_, self.hash, data=self.data) b = EncryptedPassportElement(self.type_, self.hash, data=self.data) c = EncryptedPassportElement(self.data, "") d = PassportElementError("source", "type", "message") 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) python-telegram-bot-21.1.1/tests/_passport/test_no_passport.py000066400000000000000000000037201460724040100246310ustar00rootroot00000000000000#!/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 file tests the case that PTB was installed *without* the optional dependency `passport`. Currently this only means that cryptography is not installed. Because imports in pytest are intricate, we just run pytest -k test_no_passport.py with the TEST_WITH_OPT_DEPS environment variable set to False in addition to the regular test suite """ import pytest from telegram import _bot as bot from telegram._passport import credentials from tests.auxil.envvars import TEST_WITH_OPT_DEPS @pytest.mark.skipif( TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is not installed" ) class TestNoPassportWithoutRequest: def test_bot_init(self, bot_info): with pytest.raises(RuntimeError, match="passport"): bot.Bot(bot_info["token"], private_key=1, private_key_password=2) def test_credentials_decrypt(self): with pytest.raises(RuntimeError, match="passport"): credentials.decrypt(1, 1, 1) def test_encrypted_credentials_decrypted_secret(self): ec = credentials.EncryptedCredentials("data", "hash", "secret") with pytest.raises(RuntimeError, match="passport"): ec.decrypted_secret python-telegram-bot-21.1.1/tests/_passport/test_passport.py000066400000000000000000000643631460724040100241470ustar00rootroot00000000000000#!/usr/bin/env python # flake8: noqa: E501 # 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 ( Bot, Credentials, File, PassportData, PassportElementErrorDataField, PassportElementErrorSelfie, PassportFile, ) from telegram.error import PassportDecryptionError # Note: All classes in telegram.credentials (except EncryptedCredentials) aren't directly tested # here, although they are implicitly tested. Testing for those classes was too much work and not # worth it. from telegram.request import RequestData from tests.auxil.pytest_classes import make_bot from tests.auxil.slots import mro_slots RAW_PASSPORT_DATA = { "credentials": { "hash": "qB4hz2LMcXYhglwz6EvXMMyI3PURisWLXl/iCmCXcSk=", "secret": "O6x3X2JrLO1lUIhw48os1gaenDuZLhesoZMKXehZwtM3vsxOdtxHKWQyLNwtbyy4snYpARXDwf8f1QHNmQ/M1PwBQvk1ozrZBXb4a6k/iYj+P4v8Xw2M++CRHqZv0LaxFtyHOXnNYZ6dXpNeF0ZvYYTmm0FsYvK+/3/F6VDB3Oe6xWlXFLwaCCP/jA9i2+dKp6iq8NLOo4VnxenPKWWYz20RZ50MdAbS3UR+NCx4AHM2P5DEGrHNW0tMXJ+FG3jpVrit5BuCbB/eRgKxNGRWNxEGV5hun5MChdxKCEHCimnUA97h7MZdoTgYxkrfvDSZ/V89BnFXLdr87t/NLvVE0Q==", "data": "MjHCHQT277BgJMxq5PfkPUl9p9h/5GbWtR0lcEi9MvtmQ9ONW8DZ3OmddaaVDdEHwh6Lfcr/0mxyMKhttm9QyACA1+oGBdw/KHRzLKS4a0P+rMyCcgctO6Q/+P9o6xs66hPFJAsN+sOUU4d431zaQN/RuHYuGM2s14A1K4YNRvNlp5/0JiS7RrV6SH6LC/97CvgGUnBOhLISmJXiMqwyVgg+wfS5SnOy2hQ5Zt/XdzxFyuehE3W4mHyY5W09I+MB/IafM4HcEvaqfFkWPmXNTkgBk9C2EJU9Lqc0PLmrXZn4LKeHVjuY7iloes/JecYKPMWmNjXwZQg/duIXvWL5icUaNrfjEcT5oljwZsrAc6NyyZwIp4w/+cb98jFwFAJ5uF81lRkZbeC3iw84mdpSVVYEzJSWSkSRs6JydfRCOYki0BNX9RnjgGqPYT+hNtUpEix2vHvJTIyvceflLF5vu+ol/axusirRiBVgNjKMfhs+x5bwBj5nDEE1XtEVrKtRq8/Ss96p0Tlds8eKulCDtPv/YujHVIErEhgUxDCGhr7OShokAFs/RwLmj6IBYQwnVbo0zIsq5qmCn/+1ogxJK+e934cDcwJAs8pnpgp7JPeFN9wBdmXSTpkO3KZt5Lgl3V86Rv5qv8oExQoJIUH5pKoXM+H2GB3QdfHLc/KpCeedG8RjateuIXKL2EtVe3JDMGBeI56eP9bTlW8+G1zVcpUuw/YEV14q4yiPlIRuWzrxXMvC1EtSzfGeY899trZBMCI00aeSpJyanf1f7B7nlQu6UbtMyN/9/GXbnjQjdP15CCQnmUK3PEWGtGV4XmK4iXIjBJELDD3T86RJyX/JAhJbT6funMt05w0bTyKFUDXdOcMyw2upj+wCsWTVMRNkw9yM63xL5TEfOc24aNi4pc4/LARSvwaNI/iBStqZEpG3KkYBQ2KutA022jRWzQ+xHIIz3mgA8z4PmXhcAU2RrTDGjGZUfbcX9LysZ/HvCHo/EB5njRISn3Yr1Ewu1pLX+Z4mERs+PCBXbpqBrZjY6dSWJ1QhggVJTPpWHya4CTGhkpyeFIc+D35t4kgt3U5ib61IaO9ABq0fUnB6dsvTGiN/i7KM8Ie1RUvPFBoVbz9x5YU9IT/ai8ln+1kfFfhiy8Ku4MnczthOUIjdr8nGUo4r3y0iEd5JEmqEcEsNx+/ZVMb7NEhpqXG8GPUxmwFTaHekldENxqTylv6qIxodhch6SLs/+iMat86DeCk1/+0u2fGmqZpxEEd9B89iD0+Av3UZC/C1rHn5FhC+o89RQAFWnH245rOHSbrTXyAtVBu2s1R0eIGadtAZYOI8xjULkbp52XyznZKCKaMKmr3UYah4P4VnUmhddBy+Mp/Bvxh8N3Ma8VSltO1n+3lyQWeUwdlCjt/3q3UpjAmilIKwEfeXMVhyjRlae1YGi/k+vgn+9LbFogh3Pl+N/kuyNqsTqPlzei2RXgpkX2qqHdF8WfkwQJpjXRurQN5LYaBfalygrUT+fCCpyaNkByxdDljKIPq6EibqtFA5jprAVDWGTTtFCKsPDJKFf9vc2lFy+7zyJxe8kMP1Wru8GrzF5z+pbfNp1tB80NqOrqJUbRnPB2I9Fb47ab76L8RBu2MROUNGcKJ62imQtfPH2I0f8zpbqqTZQwr3AmZ+PS9df2hHp2dYR9gFpMyR9u+bJ7HbpiKbYhh7mEFYeB/pQHsQRqM2OU5Bxk8XzxrwsdnzYO6tVcn8xr3Q4P9kZNXA6X5H0vJPpzClWoCPEr3ZGGWGl5DOhfsAmmst47vdAA1Cbl5k3pUW7/T3LWnMNwRnP8OdDOnsm06/v1nxIDjH08YlzLj4GTeXphSnsXSRNKFmz+M7vsOZPhWB8Y/WQmpJpOIj6IRstLxJk0h47TfYC7/RHBr4y7HQ8MLHODoPz/FM+nZtm2MMpB+u0qFNBvZG+Tjvlia7ZhX0n0OtivLWhnqygx3jZX7Ffwt5Es03wDP39ru4IccVZ9Jly/YUriHZURS6oDGycH3+DKUn5gRAxgOyjAwxGRqJh/YKfPt14d4iur0H3VUuLwFCbwj5hSvHVIv5cNapitgINU+0qzIlhyeE0HfRKstO7nOQ9A+nclqbOikYgurYIe0z70WZyJ3qSiHbOMMqQqcoKOJ6M9v2hDdJo9MDQ13dF6bl4+BfX4mcF0m7nVUBkzCRiSOQWWFUMgLX7CxSdmotT+eawKLjrCpSPmq9sicWyrFtVlq/NYLDGhT0jUUau6Mb5ksT+/OBVeMzqoezUcly29L1/gaeWAc8zOApVEjAMT48U63NXK5o8GrANeqqAt3TB36S5yeIjMf194nXAAzsJZ+s/tXprLn2M5mA1Iag4RbVPTarEsMp10JYag==", }, "data": [ { "data": "QRfzWcCN4WncvRO3lASG+d+c5gzqXtoCinQ1PgtYiZMKXCksx9eB9Ic1bOt8C/un9/XaX220PjJSO7Kuba+nXXC51qTsjqP9rnLKygnEIWjKrfiDdklzgcukpRzFSjiOAvhy86xFJZ1PfPSrFATy/Gp1RydLzbrBd2ZWxZqXrxcMoA0Q2UTTFXDoCYerEAiZoD69i79tB/6nkLBcUUvN5d52gKd/GowvxWqAAmdO6l1N7jlo6aWjdYQNBAK1KHbJdbRZMJLxC1MqMuZXAYrPoYBRKr5xAnxDTmPn/LEZKLc3gwwZyEgR5x7e9jp5heM6IEMmsv3O/6SUeEQs7P0iVuRSPLMJLfDdwns8Tl3fF2M4IxKVovjCaOVW+yHKsADDAYQPzzH2RcrWVD0TP5I64mzpK64BbTOq3qm3Hn51SV9uA/+LvdGbCp7VnzHx4EdUizHsVyilJULOBwvklsrDRvXMiWmh34ZSR6zilh051tMEcRf0I+Oe7pIxVJd/KKfYA2Z/eWVQTCn5gMuAInQNXFSqDIeIqBX+wca6kvOCUOXB7J2uRjTpLaC4DM9s/sNjSBvFixcGAngt+9oap6Y45rQc8ZJaNN/ALqEJAmkphW8=", "type": "personal_details", "hash": "What to put here?", }, { "reverse_side": { "file_size": 32424112, "file_date": 1534074942, "file_id": "DgADBAADNQQAAtoagFPf4wwmFZdmyQI", "file_unique_id": "adc3145fd2e84d95b64d68eaa22aa33e", }, "translation": [ { "file_size": 28640, "file_date": 1535630933, "file_id": "DgADBAADswMAAisqQVAmooP-kVgLgAI", "file_unique_id": "52a90d53d6064bb58feb582acdc3a324", }, { "file_size": 28672, "file_date": 1535630933, "file_id": "DgADBAAD1QMAAnrpQFBMZsT3HysjwwI", "file_unique_id": "7285f864d168441ba1f7d02146250432", }, ], "front_side": { "file_size": 28624, "file_date": 1534074942, "file_id": "DgADBAADxwMAApnQgVPK2-ckL2eXVAI", "file_unique_id": "d9d52a700cbb4a189a80104aa5978133", }, "type": "driver_license", "selfie": { "file_size": 28592, "file_date": 1534074942, "file_id": "DgADBAADEQQAAkopgFNr6oi-wISRtAI", "file_unique_id": "d4e390cca57b4da5a65322b304762a12", }, "data": "eJUOFuY53QKmGqmBgVWlLBAQCUQJ79n405SX6M5aGFIIodOPQqnLYvMNqTwTrXGDlW+mVLZcbu+y8luLVO8WsJB/0SB7q5WaXn/IMt1G9lz5G/KMLIZG/x9zlnimsaQLg7u8srG6L4KZzv+xkbbHjZdETrxU8j0N/DoS4HvLMRSJAgeFUrY6v2YW9vSRg+fSxIqQy1jR2VKpzAT8OhOz7A==", "hash": "We seriously need to improve this mess! took so long to debug!", }, { "translation": [ { "file_size": 28480, "file_date": 1535630939, "file_id": "DgADBAADyQUAAqyqQVC_eoX_KwNjJwI", "file_unique_id": "38b2877b443542cbaf520c6e36a33ac4", }, { "file_size": 28528, "file_date": 1535630939, "file_id": "DgADBAADsQQAAubTQVDRO_FN3lOwWwI", "file_unique_id": "f008ca48c44b4a47895ddbcd2f76741e", }, ], "files": [ { "file_size": 28640, "file_date": 1534074988, "file_id": "DgADBAADLAMAAhwfgVMyfGa5Nr0LvAI", "file_unique_id": "b170748794834644baaa3ec57ee4ce7a", }, { "file_size": 28480, "file_date": 1534074988, "file_id": "DgADBAADaQQAAsFxgVNVfLZuT-_3ZQI", "file_unique_id": "19a12ae34dca424b85e0308f706cee75", }, ], "type": "utility_bill", "hash": "Wow over 30 minutes spent debugging passport stuff.", }, { "data": "j9SksVkSj128DBtZA+3aNjSFNirzv+R97guZaMgae4Gi0oDVNAF7twPR7j9VSmPedfJrEwL3O889Ei+a5F1xyLLyEI/qEBljvL70GFIhYGitS0JmNabHPHSZrjOl8b4s/0Z0Px2GpLO5siusTLQonimdUvu4UPjKquYISmlKEKhtmGATy+h+JDjNCYuOkhakeNw0Rk0BHgj0C3fCb7WZNQSyVb+2GTu6caR6eXf/AFwFp0TV3sRz3h0WIVPW8bna", "type": "address", "hash": "at least I get the pattern now", }, {"email": "fb3e3i47zt@dispostable.com", "type": "email", "hash": "this should be it."}, ], } @pytest.fixture(scope="module") def all_passport_data(): return [ { "type": "personal_details", "data": RAW_PASSPORT_DATA["data"][0]["data"], "hash": "what to put here?", }, { "type": "passport", "data": RAW_PASSPORT_DATA["data"][1]["data"], "front_side": RAW_PASSPORT_DATA["data"][1]["front_side"], "selfie": RAW_PASSPORT_DATA["data"][1]["selfie"], "translation": RAW_PASSPORT_DATA["data"][1]["translation"], "hash": "more data arghh", }, { "type": "internal_passport", "data": RAW_PASSPORT_DATA["data"][1]["data"], "front_side": RAW_PASSPORT_DATA["data"][1]["front_side"], "selfie": RAW_PASSPORT_DATA["data"][1]["selfie"], "translation": RAW_PASSPORT_DATA["data"][1]["translation"], "hash": "more data arghh", }, { "type": "driver_license", "data": RAW_PASSPORT_DATA["data"][1]["data"], "front_side": RAW_PASSPORT_DATA["data"][1]["front_side"], "reverse_side": RAW_PASSPORT_DATA["data"][1]["reverse_side"], "selfie": RAW_PASSPORT_DATA["data"][1]["selfie"], "translation": RAW_PASSPORT_DATA["data"][1]["translation"], "hash": "more data arghh", }, { "type": "identity_card", "data": RAW_PASSPORT_DATA["data"][1]["data"], "front_side": RAW_PASSPORT_DATA["data"][1]["front_side"], "reverse_side": RAW_PASSPORT_DATA["data"][1]["reverse_side"], "selfie": RAW_PASSPORT_DATA["data"][1]["selfie"], "translation": RAW_PASSPORT_DATA["data"][1]["translation"], "hash": "more data arghh", }, { "type": "utility_bill", "files": RAW_PASSPORT_DATA["data"][2]["files"], "translation": RAW_PASSPORT_DATA["data"][2]["translation"], "hash": "more data arghh", }, { "type": "bank_statement", "files": RAW_PASSPORT_DATA["data"][2]["files"], "translation": RAW_PASSPORT_DATA["data"][2]["translation"], "hash": "more data arghh", }, { "type": "rental_agreement", "files": RAW_PASSPORT_DATA["data"][2]["files"], "translation": RAW_PASSPORT_DATA["data"][2]["translation"], "hash": "more data arghh", }, { "type": "passport_registration", "files": RAW_PASSPORT_DATA["data"][2]["files"], "translation": RAW_PASSPORT_DATA["data"][2]["translation"], "hash": "more data arghh", }, { "type": "temporary_registration", "files": RAW_PASSPORT_DATA["data"][2]["files"], "translation": RAW_PASSPORT_DATA["data"][2]["translation"], "hash": "more data arghh", }, { "type": "address", "data": RAW_PASSPORT_DATA["data"][3]["data"], "hash": "more data arghh", }, {"type": "email", "email": "fb3e3i47zt@dispostable.com", "hash": "more data arghh"}, { "type": "phone_number", "phone_number": "fb3e3i47zt@dispostable.com", "hash": "more data arghh", }, ] @pytest.fixture(scope="module") def passport_data(bot): return PassportData.de_json(RAW_PASSPORT_DATA, bot=bot) class TestPassportBase: driver_license_selfie_file_id = "DgADBAADEQQAAkopgFNr6oi-wISRtAI" driver_license_selfie_file_unique_id = "d4e390cca57b4da5a65322b304762a12" driver_license_front_side_file_id = "DgADBAADxwMAApnQgVPK2-ckL2eXVAI" driver_license_front_side_file_unique_id = "d9d52a700cbb4a189a80104aa5978133" driver_license_reverse_side_file_id = "DgADBAADNQQAAtoagFPf4wwmFZdmyQI" driver_license_reverse_side_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" driver_license_translation_1_file_id = "DgADBAADswMAAisqQVAmooP-kVgLgAI" driver_license_translation_1_file_unique_id = "52a90d53d6064bb58feb582acdc3a324" driver_license_translation_2_file_id = "DgADBAAD1QMAAnrpQFBMZsT3HysjwwI" driver_license_translation_2_file_unique_id = "7285f864d168441ba1f7d02146250432" utility_bill_1_file_id = "DgADBAADLAMAAhwfgVMyfGa5Nr0LvAI" utility_bill_1_file_unique_id = "b170748794834644baaa3ec57ee4ce7a" utility_bill_2_file_id = "DgADBAADaQQAAsFxgVNVfLZuT-_3ZQI" utility_bill_2_file_unique_id = "19a12ae34dca424b85e0308f706cee75" utility_bill_translation_1_file_id = "DgADBAADyQUAAqyqQVC_eoX_KwNjJwI" utility_bill_translation_1_file_unique_id = "38b2877b443542cbaf520c6e36a33ac4" utility_bill_translation_2_file_id = "DgADBAADsQQAAubTQVDRO_FN3lOwWwI" utility_bill_translation_2_file_unique_id = "f008ca48c44b4a47895ddbcd2f76741e" driver_license_selfie_credentials_file_hash = "Cila/qLXSBH7DpZFbb5bRZIRxeFW2uv/ulL0u0JNsYI=" driver_license_selfie_credentials_secret = "tivdId6RNYNsvXYPppdzrbxOBuBOr9wXRPDcCvnXU7E=" class TestPassportWithoutRequest(TestPassportBase): def test_slot_behaviour(self, passport_data): inst = passport_data 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_creation(self, passport_data): assert isinstance(passport_data, PassportData) def test_expected_encrypted_values(self, passport_data): personal_details, driver_license, utility_bill, address, email = passport_data.data assert personal_details.type == "personal_details" assert personal_details.data == RAW_PASSPORT_DATA["data"][0]["data"] assert driver_license.type == "driver_license" assert driver_license.data == RAW_PASSPORT_DATA["data"][1]["data"] assert isinstance(driver_license.selfie, PassportFile) assert driver_license.selfie.file_id == self.driver_license_selfie_file_id assert driver_license.selfie.file_unique_id == self.driver_license_selfie_file_unique_id assert isinstance(driver_license.front_side, PassportFile) assert driver_license.front_side.file_id == self.driver_license_front_side_file_id assert ( driver_license.front_side.file_unique_id == self.driver_license_front_side_file_unique_id ) assert isinstance(driver_license.reverse_side, PassportFile) assert driver_license.reverse_side.file_id == self.driver_license_reverse_side_file_id assert ( driver_license.reverse_side.file_unique_id == self.driver_license_reverse_side_file_unique_id ) assert isinstance(driver_license.translation[0], PassportFile) assert driver_license.translation[0].file_id == self.driver_license_translation_1_file_id assert ( driver_license.translation[0].file_unique_id == self.driver_license_translation_1_file_unique_id ) assert isinstance(driver_license.translation[1], PassportFile) assert driver_license.translation[1].file_id == self.driver_license_translation_2_file_id assert ( driver_license.translation[1].file_unique_id == self.driver_license_translation_2_file_unique_id ) assert utility_bill.type == "utility_bill" assert isinstance(utility_bill.files[0], PassportFile) assert utility_bill.files[0].file_id == self.utility_bill_1_file_id assert utility_bill.files[0].file_unique_id == self.utility_bill_1_file_unique_id assert isinstance(utility_bill.files[1], PassportFile) assert utility_bill.files[1].file_id == self.utility_bill_2_file_id assert utility_bill.files[1].file_unique_id == self.utility_bill_2_file_unique_id assert isinstance(utility_bill.translation[0], PassportFile) assert utility_bill.translation[0].file_id == self.utility_bill_translation_1_file_id assert ( utility_bill.translation[0].file_unique_id == self.utility_bill_translation_1_file_unique_id ) assert isinstance(utility_bill.translation[1], PassportFile) assert utility_bill.translation[1].file_id == self.utility_bill_translation_2_file_id assert ( utility_bill.translation[1].file_unique_id == self.utility_bill_translation_2_file_unique_id ) assert address.type == "address" assert address.data == RAW_PASSPORT_DATA["data"][3]["data"] assert email.type == "email" assert email.email == "fb3e3i47zt@dispostable.com" def test_expected_decrypted_values(self, passport_data): ( personal_details, driver_license, utility_bill, address, email, ) = passport_data.decrypted_data assert personal_details.type == "personal_details" assert personal_details.data.to_dict() == { "first_name": "FIRSTNAME", "middle_name": "MIDDLENAME", "first_name_native": "FIRSTNAMENATIVE", "residence_country_code": "DK", "birth_date": "01.01.2001", "last_name_native": "LASTNAMENATIVE", "gender": "female", "middle_name_native": "MIDDLENAMENATIVE", "country_code": "DK", "last_name": "LASTNAME", } assert driver_license.type == "driver_license" assert driver_license.data.to_dict() == { "expiry_date": "01.01.2001", "document_no": "DOCUMENT_NO", } assert isinstance(driver_license.selfie, PassportFile) assert driver_license.selfie.file_id == self.driver_license_selfie_file_id assert driver_license.selfie.file_unique_id == self.driver_license_selfie_file_unique_id assert isinstance(driver_license.front_side, PassportFile) assert driver_license.front_side.file_id == self.driver_license_front_side_file_id assert ( driver_license.front_side.file_unique_id == self.driver_license_front_side_file_unique_id ) assert isinstance(driver_license.reverse_side, PassportFile) assert driver_license.reverse_side.file_id == self.driver_license_reverse_side_file_id assert ( driver_license.reverse_side.file_unique_id == self.driver_license_reverse_side_file_unique_id ) assert address.type == "address" assert address.data.to_dict() == { "city": "CITY", "street_line2": "STREET_LINE2", "state": "STATE", "post_code": "POSTCODE", "country_code": "DK", "street_line1": "STREET_LINE1", } assert utility_bill.type == "utility_bill" assert isinstance(utility_bill.files[0], PassportFile) assert utility_bill.files[0].file_id == self.utility_bill_1_file_id assert utility_bill.files[0].file_unique_id == self.utility_bill_1_file_unique_id assert isinstance(utility_bill.files[1], PassportFile) assert utility_bill.files[1].file_id == self.utility_bill_2_file_id assert utility_bill.files[1].file_unique_id == self.utility_bill_2_file_unique_id assert email.type == "email" assert email.email == "fb3e3i47zt@dispostable.com" def test_de_json_and_to_dict(self, bot): passport_data = PassportData.de_json(RAW_PASSPORT_DATA, bot) assert passport_data.api_kwargs == {} assert passport_data.to_dict() == RAW_PASSPORT_DATA assert passport_data.decrypted_data assert passport_data.to_dict() == RAW_PASSPORT_DATA def test_equality(self, passport_data): a = PassportData(passport_data.data, passport_data.credentials) b = PassportData(passport_data.data, passport_data.credentials) assert a == b assert hash(a) == hash(b) assert a is not b new_pp_data = deepcopy(passport_data) new_pp_data.credentials._unfreeze() new_pp_data.credentials.hash = "NOTAPROPERHASH" c = PassportData(new_pp_data.data, new_pp_data.credentials) assert a != c assert hash(a) != hash(c) def test_bot_init_invalid_key(self, bot): with pytest.raises(TypeError): Bot(bot.token, private_key="Invalid key!") with pytest.raises(ValueError, match="Could not deserialize key data"): Bot(bot.token, private_key=b"Invalid key!") def test_all_types(self, passport_data, bot, all_passport_data): credentials = passport_data.decrypted_credentials.to_dict() # Copy credentials from other types to all types so we can decrypt everything sd = credentials["secure_data"] credentials["secure_data"] = { "personal_details": sd["personal_details"].copy(), "passport": sd["driver_license"].copy(), "internal_passport": sd["driver_license"].copy(), "driver_license": sd["driver_license"].copy(), "identity_card": sd["driver_license"].copy(), "address": sd["address"].copy(), "utility_bill": sd["utility_bill"].copy(), "bank_statement": sd["utility_bill"].copy(), "rental_agreement": sd["utility_bill"].copy(), "passport_registration": sd["utility_bill"].copy(), "temporary_registration": sd["utility_bill"].copy(), } new = PassportData.de_json( { "data": all_passport_data, # Replaced below "credentials": {"data": "data", "hash": "hash", "secret": "secret"}, }, bot=bot, ) assert new.api_kwargs == {} new.credentials._decrypted_data = Credentials.de_json(credentials, bot) assert new.credentials.api_kwargs == {} assert isinstance(new, PassportData) assert new.decrypted_data async def test_passport_data_okay_with_non_crypto_bot(self, bot): async with make_bot(token=bot.token) as b: assert PassportData.de_json(RAW_PASSPORT_DATA, bot=b) def test_wrong_hash(self, bot): data = deepcopy(RAW_PASSPORT_DATA) data["credentials"]["hash"] = "bm90Y29ycmVjdGhhc2g=" # Not correct hash passport_data = PassportData.de_json(data, bot=bot) with pytest.raises(PassportDecryptionError): assert passport_data.decrypted_data async def test_wrong_key(self, bot): short_key = ( b"-----BEGIN RSA PRIVATE" b" KEY-----\r\nMIIBOQIBAAJBAKU+OZ2jJm7sCA/ec4gngNZhXYPu+DZ/TAwSMl0W7vAPXAsLplBk\r\nO8l6IBHx8N0ZC4Bc65mO3b2G8YAzqndyqH8CAwEAAQJAWOx3jQFzeVXDsOaBPdAk\r\nYTncXVeIc6tlfUl9mOLyinSbRNCy1XicOiOZFgH1rRKOGIC1235QmqxFvdecySoY\r\nwQIhAOFeGgeX9CrEPuSsd9+kqUcA2avCwqdQgSdy2qggRFyJAiEAu7QHT8JQSkHU\r\nDELfzrzc24AhjyG0z1DpGZArM8COascCIDK42SboXj3Z2UXiQ0CEcMzYNiVgOisq\r\nBUd5pBi+2mPxAiAM5Z7G/Sv1HjbKrOGh29o0/sXPhtpckEuj5QMC6E0gywIgFY6S\r\nNjwrAA+cMmsgY0O2fAzEKkDc5YiFsiXaGaSS4eA=\r\n-----END" b" RSA PRIVATE KEY-----" ) async with make_bot(token=bot.token, private_key=short_key) as b: passport_data = PassportData.de_json(RAW_PASSPORT_DATA, bot=b) with pytest.raises(PassportDecryptionError): assert passport_data.decrypted_data async with make_bot(token=bot.token, private_key=short_key) as b: passport_data = PassportData.de_json(RAW_PASSPORT_DATA, bot=b) with pytest.raises(PassportDecryptionError): assert passport_data.decrypted_data async def test_mocked_download_passport_file(self, passport_data, monkeypatch): # The files are not coming from our test bot, therefore the file id is invalid/wrong # when coming from this bot, so we monkeypatch the call, to make sure that Bot.get_file # at least gets called # TODO: Actually download a passport file in a test selfie = passport_data.decrypted_data[1].selfie # NOTE: file_unique_id is not used in the get_file method, so it is passed directly async def get_file(*_, **kwargs): return File(kwargs["file_id"], selfie.file_unique_id) monkeypatch.setattr(passport_data.get_bot(), "get_file", get_file) file = await selfie.get_file() assert file.file_id == selfie.file_id assert file.file_unique_id == selfie.file_unique_id assert file._credentials.file_hash == self.driver_license_selfie_credentials_file_hash assert file._credentials.secret == self.driver_license_selfie_credentials_secret async def test_mocked_set_passport_data_errors(self, monkeypatch, bot, chat_id, passport_data): async def make_assertion(url, request_data: RequestData, *args, **kwargs): data = request_data.parameters return ( data["user_id"] == str(chat_id) and data["errors"][0]["file_hash"] == ( passport_data.decrypted_credentials.secure_data.driver_license.selfie.file_hash ) and data["errors"][1]["data_hash"] == passport_data.decrypted_credentials.secure_data.driver_license.data.data_hash ) monkeypatch.setattr(bot.request, "post", make_assertion) message = await bot.set_passport_data_errors( chat_id, [ PassportElementErrorSelfie( "driver_license", ( passport_data.decrypted_credentials.secure_data.driver_license.selfie.file_hash ), "You're not handsome enough to use this app!", ), PassportElementErrorDataField( "driver_license", "expiry_date", ( passport_data.decrypted_credentials.secure_data.driver_license.data.data_hash ), "Your driver license is expired!", ), ], ) assert message python-telegram-bot-21.1.1/tests/_passport/test_passportelementerrordatafield.py000066400000000000000000000102631460724040100304170ustar00rootroot00000000000000#!/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 PassportElementErrorDataField, PassportElementErrorSelfie from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def passport_element_error_data_field(): return PassportElementErrorDataField( TestPassportElementErrorDataFieldBase.type_, TestPassportElementErrorDataFieldBase.field_name, TestPassportElementErrorDataFieldBase.data_hash, TestPassportElementErrorDataFieldBase.message, ) class TestPassportElementErrorDataFieldBase: source = "data" type_ = "test_type" field_name = "test_field" data_hash = "data_hash" message = "Error message" class TestPassportElementErrorDataFieldWithoutRequest(TestPassportElementErrorDataFieldBase): def test_slot_behaviour(self, passport_element_error_data_field): inst = passport_element_error_data_field 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, passport_element_error_data_field): assert passport_element_error_data_field.source == self.source assert passport_element_error_data_field.type == self.type_ assert passport_element_error_data_field.field_name == self.field_name assert passport_element_error_data_field.data_hash == self.data_hash assert passport_element_error_data_field.message == self.message def test_to_dict(self, passport_element_error_data_field): passport_element_error_data_field_dict = passport_element_error_data_field.to_dict() assert isinstance(passport_element_error_data_field_dict, dict) assert ( passport_element_error_data_field_dict["source"] == passport_element_error_data_field.source ) assert ( passport_element_error_data_field_dict["type"] == passport_element_error_data_field.type ) assert ( passport_element_error_data_field_dict["field_name"] == passport_element_error_data_field.field_name ) assert ( passport_element_error_data_field_dict["data_hash"] == passport_element_error_data_field.data_hash ) assert ( passport_element_error_data_field_dict["message"] == passport_element_error_data_field.message ) def test_equality(self): a = PassportElementErrorDataField( self.type_, self.field_name, self.data_hash, self.message ) b = PassportElementErrorDataField( self.type_, self.field_name, self.data_hash, self.message ) c = PassportElementErrorDataField(self.type_, "", "", "") d = PassportElementErrorDataField("", self.field_name, "", "") e = PassportElementErrorDataField("", "", self.data_hash, "") f = PassportElementErrorDataField("", "", "", self.message) g = PassportElementErrorSelfie(self.type_, "", self.message) 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) assert a != g assert hash(a) != hash(g) python-telegram-bot-21.1.1/tests/_passport/test_passportelementerrorfile.py000066400000000000000000000065651460724040100274330ustar00rootroot00000000000000#!/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 PassportElementErrorFile, PassportElementErrorSelfie from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def passport_element_error_file(): return PassportElementErrorFile( TestPassportElementErrorFileBase.type_, TestPassportElementErrorFileBase.file_hash, TestPassportElementErrorFileBase.message, ) class TestPassportElementErrorFileBase: source = "file" type_ = "test_type" file_hash = "file_hash" message = "Error message" class TestPassportElementErrorFileWithoutRequest(TestPassportElementErrorFileBase): def test_slot_behaviour(self, passport_element_error_file): inst = passport_element_error_file 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, passport_element_error_file): assert passport_element_error_file.source == self.source assert passport_element_error_file.type == self.type_ assert passport_element_error_file.file_hash == self.file_hash assert passport_element_error_file.message == self.message def test_to_dict(self, passport_element_error_file): passport_element_error_file_dict = passport_element_error_file.to_dict() assert isinstance(passport_element_error_file_dict, dict) assert passport_element_error_file_dict["source"] == passport_element_error_file.source assert passport_element_error_file_dict["type"] == passport_element_error_file.type assert ( passport_element_error_file_dict["file_hash"] == passport_element_error_file.file_hash ) assert passport_element_error_file_dict["message"] == passport_element_error_file.message def test_equality(self): a = PassportElementErrorFile(self.type_, self.file_hash, self.message) b = PassportElementErrorFile(self.type_, self.file_hash, self.message) c = PassportElementErrorFile(self.type_, "", "") d = PassportElementErrorFile("", self.file_hash, "") e = PassportElementErrorFile("", "", self.message) f = PassportElementErrorSelfie(self.type_, self.file_hash, self.message) 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) python-telegram-bot-21.1.1/tests/_passport/test_passportelementerrorfiles.py000066400000000000000000000077551460724040100276200ustar00rootroot00000000000000#!/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 PassportElementErrorFiles, PassportElementErrorSelfie from telegram.warnings import PTBDeprecationWarning from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def passport_element_error_files(): return PassportElementErrorFiles( TestPassportElementErrorFilesBase.type_, TestPassportElementErrorFilesBase.file_hashes, TestPassportElementErrorFilesBase.message, ) class TestPassportElementErrorFilesBase: source = "files" type_ = "test_type" file_hashes = ["hash1", "hash2"] message = "Error message" class TestPassportElementErrorFilesWithoutRequest(TestPassportElementErrorFilesBase): def test_slot_behaviour(self, passport_element_error_files): inst = passport_element_error_files 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, passport_element_error_files): assert passport_element_error_files.source == self.source assert passport_element_error_files.type == self.type_ assert isinstance(passport_element_error_files.file_hashes, list) assert passport_element_error_files.file_hashes == self.file_hashes assert passport_element_error_files.message == self.message def test_to_dict(self, passport_element_error_files): passport_element_error_files_dict = passport_element_error_files.to_dict() assert isinstance(passport_element_error_files_dict, dict) assert passport_element_error_files_dict["source"] == passport_element_error_files.source assert passport_element_error_files_dict["type"] == passport_element_error_files.type assert passport_element_error_files_dict["message"] == passport_element_error_files.message assert ( passport_element_error_files_dict["file_hashes"] == passport_element_error_files.file_hashes ) def test_equality(self): a = PassportElementErrorFiles(self.type_, self.file_hashes, self.message) b = PassportElementErrorFiles(self.type_, self.file_hashes, self.message) c = PassportElementErrorFiles(self.type_, "", "") d = PassportElementErrorFiles("", self.file_hashes, "") e = PassportElementErrorFiles("", "", self.message) f = PassportElementErrorSelfie(self.type_, "", self.message) 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) def test_file_hashes_deprecated(self, passport_element_error_files, recwarn): passport_element_error_files.file_hashes assert len(recwarn) == 1 assert ( "The attribute `file_hashes` will return a tuple instead of a list in future major" " versions." in str(recwarn[0].message) ) assert recwarn[0].category is PTBDeprecationWarning assert recwarn[0].filename == __file__ python-telegram-bot-21.1.1/tests/_passport/test_passportelementerrorfrontside.py000066400000000000000000000072541460724040100305050ustar00rootroot00000000000000#!/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 PassportElementErrorFrontSide, PassportElementErrorSelfie from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def passport_element_error_front_side(): return PassportElementErrorFrontSide( TestPassportElementErrorFrontSideBase.type_, TestPassportElementErrorFrontSideBase.file_hash, TestPassportElementErrorFrontSideBase.message, ) class TestPassportElementErrorFrontSideBase: source = "front_side" type_ = "test_type" file_hash = "file_hash" message = "Error message" class TestPassportElementErrorFrontSideWithoutRequest(TestPassportElementErrorFrontSideBase): def test_slot_behaviour(self, passport_element_error_front_side): inst = passport_element_error_front_side 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, passport_element_error_front_side): assert passport_element_error_front_side.source == self.source assert passport_element_error_front_side.type == self.type_ assert passport_element_error_front_side.file_hash == self.file_hash assert passport_element_error_front_side.message == self.message def test_to_dict(self, passport_element_error_front_side): passport_element_error_front_side_dict = passport_element_error_front_side.to_dict() assert isinstance(passport_element_error_front_side_dict, dict) assert ( passport_element_error_front_side_dict["source"] == passport_element_error_front_side.source ) assert ( passport_element_error_front_side_dict["type"] == passport_element_error_front_side.type ) assert ( passport_element_error_front_side_dict["file_hash"] == passport_element_error_front_side.file_hash ) assert ( passport_element_error_front_side_dict["message"] == passport_element_error_front_side.message ) def test_equality(self): a = PassportElementErrorFrontSide(self.type_, self.file_hash, self.message) b = PassportElementErrorFrontSide(self.type_, self.file_hash, self.message) c = PassportElementErrorFrontSide(self.type_, "", "") d = PassportElementErrorFrontSide("", self.file_hash, "") e = PassportElementErrorFrontSide("", "", self.message) f = PassportElementErrorSelfie(self.type_, self.file_hash, self.message) 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) python-telegram-bot-21.1.1/tests/_passport/test_passportelementerrorreverseside.py000066400000000000000000000073601460724040100310260ustar00rootroot00000000000000#!/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 PassportElementErrorReverseSide, PassportElementErrorSelfie from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def passport_element_error_reverse_side(): return PassportElementErrorReverseSide( TestPassportElementErrorReverseSideBase.type_, TestPassportElementErrorReverseSideBase.file_hash, TestPassportElementErrorReverseSideBase.message, ) class TestPassportElementErrorReverseSideBase: source = "reverse_side" type_ = "test_type" file_hash = "file_hash" message = "Error message" class TestPassportElementErrorReverseSideWithoutRequest(TestPassportElementErrorReverseSideBase): def test_slot_behaviour(self, passport_element_error_reverse_side): inst = passport_element_error_reverse_side 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, passport_element_error_reverse_side): assert passport_element_error_reverse_side.source == self.source assert passport_element_error_reverse_side.type == self.type_ assert passport_element_error_reverse_side.file_hash == self.file_hash assert passport_element_error_reverse_side.message == self.message def test_to_dict(self, passport_element_error_reverse_side): passport_element_error_reverse_side_dict = passport_element_error_reverse_side.to_dict() assert isinstance(passport_element_error_reverse_side_dict, dict) assert ( passport_element_error_reverse_side_dict["source"] == passport_element_error_reverse_side.source ) assert ( passport_element_error_reverse_side_dict["type"] == passport_element_error_reverse_side.type ) assert ( passport_element_error_reverse_side_dict["file_hash"] == passport_element_error_reverse_side.file_hash ) assert ( passport_element_error_reverse_side_dict["message"] == passport_element_error_reverse_side.message ) def test_equality(self): a = PassportElementErrorReverseSide(self.type_, self.file_hash, self.message) b = PassportElementErrorReverseSide(self.type_, self.file_hash, self.message) c = PassportElementErrorReverseSide(self.type_, "", "") d = PassportElementErrorReverseSide("", self.file_hash, "") e = PassportElementErrorReverseSide("", "", self.message) f = PassportElementErrorSelfie(self.type_, self.file_hash, self.message) 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) python-telegram-bot-21.1.1/tests/_passport/test_passportelementerrorselfie.py000066400000000000000000000067331460724040100277600ustar00rootroot00000000000000#!/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 PassportElementErrorDataField, PassportElementErrorSelfie from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def passport_element_error_selfie(): return PassportElementErrorSelfie( TestPassportElementErrorSelfieBase.type_, TestPassportElementErrorSelfieBase.file_hash, TestPassportElementErrorSelfieBase.message, ) class TestPassportElementErrorSelfieBase: source = "selfie" type_ = "test_type" file_hash = "file_hash" message = "Error message" class TestPassportElementErrorSelfieWithoutRequest(TestPassportElementErrorSelfieBase): def test_slot_behaviour(self, passport_element_error_selfie): inst = passport_element_error_selfie 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, passport_element_error_selfie): assert passport_element_error_selfie.source == self.source assert passport_element_error_selfie.type == self.type_ assert passport_element_error_selfie.file_hash == self.file_hash assert passport_element_error_selfie.message == self.message def test_to_dict(self, passport_element_error_selfie): passport_element_error_selfie_dict = passport_element_error_selfie.to_dict() assert isinstance(passport_element_error_selfie_dict, dict) assert passport_element_error_selfie_dict["source"] == passport_element_error_selfie.source assert passport_element_error_selfie_dict["type"] == passport_element_error_selfie.type assert ( passport_element_error_selfie_dict["file_hash"] == passport_element_error_selfie.file_hash ) assert ( passport_element_error_selfie_dict["message"] == passport_element_error_selfie.message ) def test_equality(self): a = PassportElementErrorSelfie(self.type_, self.file_hash, self.message) b = PassportElementErrorSelfie(self.type_, self.file_hash, self.message) c = PassportElementErrorSelfie(self.type_, "", "") d = PassportElementErrorSelfie("", self.file_hash, "") e = PassportElementErrorSelfie("", "", self.message) f = PassportElementErrorDataField(self.type_, "", "", self.message) 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) python-telegram-bot-21.1.1/tests/_passport/test_passportelementerrortranslationfile.py000066400000000000000000000076241460724040100317070ustar00rootroot00000000000000#!/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 PassportElementErrorDataField, PassportElementErrorTranslationFile from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def passport_element_error_translation_file(): return PassportElementErrorTranslationFile( TestPassportElementErrorTranslationFileBase.type_, TestPassportElementErrorTranslationFileBase.file_hash, TestPassportElementErrorTranslationFileBase.message, ) class TestPassportElementErrorTranslationFileBase: source = "translation_file" type_ = "test_type" file_hash = "file_hash" message = "Error message" class TestPassportElementErrorTranslationFileWithoutRequest( TestPassportElementErrorTranslationFileBase ): def test_slot_behaviour(self, passport_element_error_translation_file): inst = passport_element_error_translation_file 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, passport_element_error_translation_file): assert passport_element_error_translation_file.source == self.source assert passport_element_error_translation_file.type == self.type_ assert passport_element_error_translation_file.file_hash == self.file_hash assert passport_element_error_translation_file.message == self.message def test_to_dict(self, passport_element_error_translation_file): passport_element_error_translation_file_dict = ( passport_element_error_translation_file.to_dict() ) assert isinstance(passport_element_error_translation_file_dict, dict) assert ( passport_element_error_translation_file_dict["source"] == passport_element_error_translation_file.source ) assert ( passport_element_error_translation_file_dict["type"] == passport_element_error_translation_file.type ) assert ( passport_element_error_translation_file_dict["file_hash"] == passport_element_error_translation_file.file_hash ) assert ( passport_element_error_translation_file_dict["message"] == passport_element_error_translation_file.message ) def test_equality(self): a = PassportElementErrorTranslationFile(self.type_, self.file_hash, self.message) b = PassportElementErrorTranslationFile(self.type_, self.file_hash, self.message) c = PassportElementErrorTranslationFile(self.type_, "", "") d = PassportElementErrorTranslationFile("", self.file_hash, "") e = PassportElementErrorTranslationFile("", "", self.message) f = PassportElementErrorDataField(self.type_, "", "", self.message) 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) python-telegram-bot-21.1.1/tests/_passport/test_passportelementerrortranslationfiles.py000066400000000000000000000110461460724040100320630ustar00rootroot00000000000000#!/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 PassportElementErrorSelfie, PassportElementErrorTranslationFiles from telegram.warnings import PTBDeprecationWarning from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def passport_element_error_translation_files(): return PassportElementErrorTranslationFiles( TestPassportElementErrorTranslationFilesBase.type_, TestPassportElementErrorTranslationFilesBase.file_hashes, TestPassportElementErrorTranslationFilesBase.message, ) class TestPassportElementErrorTranslationFilesBase: source = "translation_files" type_ = "test_type" file_hashes = ["hash1", "hash2"] message = "Error message" class TestPassportElementErrorTranslationFilesWithoutRequest( TestPassportElementErrorTranslationFilesBase ): def test_slot_behaviour(self, passport_element_error_translation_files): inst = passport_element_error_translation_files 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, passport_element_error_translation_files): assert passport_element_error_translation_files.source == self.source assert passport_element_error_translation_files.type == self.type_ assert isinstance(passport_element_error_translation_files.file_hashes, list) assert passport_element_error_translation_files.file_hashes == self.file_hashes assert passport_element_error_translation_files.message == self.message def test_to_dict(self, passport_element_error_translation_files): passport_element_error_translation_files_dict = ( passport_element_error_translation_files.to_dict() ) assert isinstance(passport_element_error_translation_files_dict, dict) assert ( passport_element_error_translation_files_dict["source"] == passport_element_error_translation_files.source ) assert ( passport_element_error_translation_files_dict["type"] == passport_element_error_translation_files.type ) assert ( passport_element_error_translation_files_dict["message"] == passport_element_error_translation_files.message ) assert ( passport_element_error_translation_files_dict["file_hashes"] == passport_element_error_translation_files.file_hashes ) def test_equality(self): a = PassportElementErrorTranslationFiles(self.type_, self.file_hashes, self.message) b = PassportElementErrorTranslationFiles(self.type_, self.file_hashes, self.message) c = PassportElementErrorTranslationFiles(self.type_, "", "") d = PassportElementErrorTranslationFiles("", self.file_hashes, "") e = PassportElementErrorTranslationFiles("", "", self.message) f = PassportElementErrorSelfie(self.type_, "", self.message) 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) def test_file_hashes_deprecated(self, passport_element_error_translation_files, recwarn): passport_element_error_translation_files.file_hashes assert len(recwarn) == 1 assert ( "The attribute `file_hashes` will return a tuple instead of a list in future major" " versions." in str(recwarn[0].message) ) assert recwarn[0].category is PTBDeprecationWarning assert recwarn[0].filename == __file__ python-telegram-bot-21.1.1/tests/_passport/test_passportelementerrorunspecified.py000066400000000000000000000073671460724040100310130ustar00rootroot00000000000000#!/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 PassportElementErrorDataField, PassportElementErrorUnspecified from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def passport_element_error_unspecified(): return PassportElementErrorUnspecified( TestPassportElementErrorUnspecifiedBase.type_, TestPassportElementErrorUnspecifiedBase.element_hash, TestPassportElementErrorUnspecifiedBase.message, ) class TestPassportElementErrorUnspecifiedBase: source = "unspecified" type_ = "test_type" element_hash = "element_hash" message = "Error message" class TestPassportElementErrorUnspecifiedWithoutRequest(TestPassportElementErrorUnspecifiedBase): def test_slot_behaviour(self, passport_element_error_unspecified): inst = passport_element_error_unspecified 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, passport_element_error_unspecified): assert passport_element_error_unspecified.source == self.source assert passport_element_error_unspecified.type == self.type_ assert passport_element_error_unspecified.element_hash == self.element_hash assert passport_element_error_unspecified.message == self.message def test_to_dict(self, passport_element_error_unspecified): passport_element_error_unspecified_dict = passport_element_error_unspecified.to_dict() assert isinstance(passport_element_error_unspecified_dict, dict) assert ( passport_element_error_unspecified_dict["source"] == passport_element_error_unspecified.source ) assert ( passport_element_error_unspecified_dict["type"] == passport_element_error_unspecified.type ) assert ( passport_element_error_unspecified_dict["element_hash"] == passport_element_error_unspecified.element_hash ) assert ( passport_element_error_unspecified_dict["message"] == passport_element_error_unspecified.message ) def test_equality(self): a = PassportElementErrorUnspecified(self.type_, self.element_hash, self.message) b = PassportElementErrorUnspecified(self.type_, self.element_hash, self.message) c = PassportElementErrorUnspecified(self.type_, "", "") d = PassportElementErrorUnspecified("", self.element_hash, "") e = PassportElementErrorUnspecified("", "", self.message) f = PassportElementErrorDataField(self.type_, "", "", self.message) 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) python-telegram-bot-21.1.1/tests/_passport/test_passportfile.py000066400000000000000000000107161460724040100250000ustar00rootroot00000000000000#!/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, File, PassportElementError, PassportFile from telegram.warnings import PTBDeprecationWarning 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="class") def passport_file(bot): pf = PassportFile( file_id=TestPassportFileBase.file_id, file_unique_id=TestPassportFileBase.file_unique_id, file_size=TestPassportFileBase.file_size, file_date=TestPassportFileBase.file_date, ) pf.set_bot(bot) return pf class TestPassportFileBase: file_id = "data" file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" file_size = 50 file_date = 1532879128 class TestPassportFileWithoutRequest(TestPassportFileBase): def test_slot_behaviour(self, passport_file): inst = passport_file 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, passport_file): assert passport_file.file_id == self.file_id assert passport_file.file_unique_id == self.file_unique_id assert passport_file.file_size == self.file_size assert passport_file.file_date == self.file_date def test_to_dict(self, passport_file): passport_file_dict = passport_file.to_dict() assert isinstance(passport_file_dict, dict) assert passport_file_dict["file_id"] == passport_file.file_id assert passport_file_dict["file_unique_id"] == passport_file.file_unique_id assert passport_file_dict["file_size"] == passport_file.file_size assert passport_file_dict["file_date"] == passport_file.file_date def test_equality(self): a = PassportFile(self.file_id, self.file_unique_id, self.file_size, self.file_date) b = PassportFile("", self.file_unique_id, self.file_size, self.file_date) c = PassportFile(self.file_id, self.file_unique_id, "", "") d = PassportFile("", "", self.file_size, self.file_date) e = PassportElementError("source", "type", "message") 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_file_date_deprecated(self, passport_file, recwarn): passport_file.file_date assert len(recwarn) == 1 assert ( "The attribute `file_date` will return a datetime instead of an integer in future" " major versions." in str(recwarn[0].message) ) assert recwarn[0].category is PTBDeprecationWarning assert recwarn[0].filename == __file__ async def test_get_file_instance_method(self, monkeypatch, passport_file): async def make_assertion(*_, **kwargs): result = kwargs["file_id"] == passport_file.file_id # we need to be a bit hacky here, b/c PF.get_file needs Bot.get_file to return a File return File(file_id=result, file_unique_id=result) assert check_shortcut_signature(PassportFile.get_file, Bot.get_file, ["file_id"], []) assert await check_shortcut_call( passport_file.get_file, passport_file.get_bot(), "get_file" ) assert await check_defaults_handling(passport_file.get_file, passport_file.get_bot()) monkeypatch.setattr(passport_file.get_bot(), "get_file", make_assertion) assert (await passport_file.get_file()).file_id == "True" python-telegram-bot-21.1.1/tests/_payment/000077500000000000000000000000001460724040100204515ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/_payment/__init__.py000066400000000000000000000014661460724040100225710ustar00rootroot00000000000000#!/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/]. python-telegram-bot-21.1.1/tests/_payment/test_invoice.py000066400000000000000000000320051460724040100235160ustar00rootroot00000000000000#!/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 pytest from telegram import Invoice, LabeledPrice, ReplyParameters from telegram.constants import ParseMode from telegram.error import BadRequest from telegram.request import RequestData from tests.auxil.build_messages import make_message from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def invoice(): return Invoice( TestInvoiceBase.title, TestInvoiceBase.description, TestInvoiceBase.start_parameter, TestInvoiceBase.currency, TestInvoiceBase.total_amount, ) class TestInvoiceBase: payload = "payload" prices = [LabeledPrice("Fish", 100), LabeledPrice("Fish Tax", 1000)] provider_data = """{"test":"test"}""" title = "title" description = "description" start_parameter = "start_parameter" currency = "EUR" total_amount = sum(p.amount for p in prices) max_tip_amount = 42 suggested_tip_amounts = [13, 42] class TestInvoiceWithoutRequest(TestInvoiceBase): def test_slot_behaviour(self, invoice): for attr in invoice.__slots__: assert getattr(invoice, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(invoice)) == len(set(mro_slots(invoice))), "duplicate slot" def test_de_json(self, bot): invoice_json = Invoice.de_json( { "title": self.title, "description": self.description, "start_parameter": self.start_parameter, "currency": self.currency, "total_amount": self.total_amount, }, bot, ) assert invoice_json.api_kwargs == {} assert invoice_json.title == self.title assert invoice_json.description == self.description assert invoice_json.start_parameter == self.start_parameter assert invoice_json.currency == self.currency assert invoice_json.total_amount == self.total_amount def test_to_dict(self, invoice): invoice_dict = invoice.to_dict() assert isinstance(invoice_dict, dict) assert invoice_dict["title"] == invoice.title assert invoice_dict["description"] == invoice.description assert invoice_dict["start_parameter"] == invoice.start_parameter assert invoice_dict["currency"] == invoice.currency assert invoice_dict["total_amount"] == invoice.total_amount async def test_send_invoice_all_args_mock(self, bot, monkeypatch): # We do this one as safety guard to make sure that we pass all of the optional # parameters correctly because #2526 went unnoticed for 3 years … async def make_assertion(*args, **_): kwargs = args[1] return all(kwargs[key] == key for key in kwargs) monkeypatch.setattr(bot, "_send_message", make_assertion) assert await bot.send_invoice( 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", send_phone_number_to_provider="send_phone_number_to_provider", send_email_to_provider="send_email_to_provider", is_flexible="is_flexible", disable_notification=True, protect_content=True, ) async def test_send_all_args_create_invoice_link(self, bot, monkeypatch): async def make_assertion(*args, **_): kwargs = args[1] return all(kwargs[i] == i for i in kwargs) monkeypatch.setattr(bot, "_post", make_assertion) assert await bot.create_invoice_link( 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", send_phone_number_to_provider="send_phone_number_to_provider", send_email_to_provider="send_email_to_provider", is_flexible="is_flexible", ) async def test_send_object_as_provider_data(self, monkeypatch, bot, chat_id, provider_token): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.json_parameters["provider_data"] == '{"test_data": 123456789}' monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_invoice( chat_id, self.title, self.description, self.payload, provider_token, self.currency, self.prices, provider_data={"test_data": 123456789}, start_parameter=self.start_parameter, ) @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_invoice_default_quote_parse_mode( self, default_bot, chat_id, invoice, custom, monkeypatch, provider_token ): 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_invoice( chat_id, self.title, self.description, self.payload, provider_token, self.currency, self.prices, reply_parameters=ReplyParameters(**kwargs), ) def test_equality(self): a = Invoice("invoice", "desc", "start", "EUR", 7) b = Invoice("invoice", "desc", "start", "EUR", 7) c = Invoice("invoices", "description", "stop", "USD", 8) d = LabeledPrice("label", 5) 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 TestInvoiceWithRequest(TestInvoiceBase): async def test_send_required_args_only(self, bot, chat_id, provider_token): message = await bot.send_invoice( chat_id=chat_id, title=self.title, description=self.description, payload=self.payload, provider_token=provider_token, currency=self.currency, prices=self.prices, ) assert message.invoice.currency == self.currency assert not message.invoice.start_parameter assert message.invoice.description == self.description assert message.invoice.title == self.title assert message.invoice.total_amount == self.total_amount link = await bot.create_invoice_link( title=self.title, description=self.description, payload=self.payload, provider_token=provider_token, currency=self.currency, prices=self.prices, ) assert isinstance(link, str) assert link @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) async def test_send_invoice_default_protect_content( self, chat_id, default_bot, provider_token ): tasks = asyncio.gather( *( default_bot.send_invoice( chat_id, self.title, self.description, self.payload, provider_token, self.currency, self.prices, **kwargs, ) for kwargs in ({}, {"protect_content": False}) ) ) protected, unprotected = await tasks assert protected.has_protected_content assert not unprotected.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_invoice_default_allow_sending_without_reply( self, default_bot, chat_id, custom, provider_token ): 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_invoice( chat_id, self.title, self.description, self.payload, provider_token, self.currency, self.prices, 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_invoice( chat_id, self.title, self.description, self.payload, provider_token, self.currency, self.prices, reply_to_message_id=reply_to_message.message_id, ) assert message.reply_to_message is None else: with pytest.raises(BadRequest, match="Message to reply not found"): await default_bot.send_invoice( chat_id, self.title, self.description, self.payload, provider_token, self.currency, self.prices, reply_to_message_id=reply_to_message.message_id, ) async def test_send_all_args_send_invoice(self, bot, chat_id, provider_token): message = await bot.send_invoice( chat_id, self.title, self.description, self.payload, provider_token, self.currency, self.prices, max_tip_amount=self.max_tip_amount, suggested_tip_amounts=self.suggested_tip_amounts, start_parameter=self.start_parameter, provider_data=self.provider_data, photo_url=( "https://raw.githubusercontent.com/" "python-telegram-bot/logos/master/logo/png/ptb-logo_240.png" ), photo_size=240, photo_width=240, photo_height=240, need_name=True, need_phone_number=True, need_email=True, need_shipping_address=True, send_phone_number_to_provider=True, send_email_to_provider=True, is_flexible=True, disable_notification=True, protect_content=True, ) for attr in message.invoice.__slots__: assert getattr(message.invoice, attr) == getattr(self, attr) assert message.has_protected_content python-telegram-bot-21.1.1/tests/_payment/test_labeledprice.py000066400000000000000000000042771460724040100245070ustar00rootroot00000000000000#!/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 LabeledPrice, Location from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def labeled_price(): return LabeledPrice(TestLabeledPriceBase.label, TestLabeledPriceBase.amount) class TestLabeledPriceBase: label = "label" amount = 100 class TestLabeledPriceWithoutRequest(TestLabeledPriceBase): def test_slot_behaviour(self, labeled_price): inst = labeled_price 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, labeled_price): assert labeled_price.label == self.label assert labeled_price.amount == self.amount def test_to_dict(self, labeled_price): labeled_price_dict = labeled_price.to_dict() assert isinstance(labeled_price_dict, dict) assert labeled_price_dict["label"] == labeled_price.label assert labeled_price_dict["amount"] == labeled_price.amount def test_equality(self): a = LabeledPrice("label", 100) b = LabeledPrice("label", 100) c = LabeledPrice("Label", 101) d = Location(123, 456) assert a == b assert hash(a) == hash(b) assert a != c assert hash(a) != hash(c) assert a != d assert hash(a) != hash(d) python-telegram-bot-21.1.1/tests/_payment/test_orderinfo.py000066400000000000000000000071531460724040100240570ustar00rootroot00000000000000#!/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 OrderInfo, ShippingAddress from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def order_info(): return OrderInfo( TestOrderInfoBase.name, TestOrderInfoBase.phone_number, TestOrderInfoBase.email, TestOrderInfoBase.shipping_address, ) class TestOrderInfoBase: name = "name" phone_number = "phone_number" email = "email" shipping_address = ShippingAddress("GB", "", "London", "12 Grimmauld Place", "", "WC1") class TestOrderInfoWithoutRequest(TestOrderInfoBase): def test_slot_behaviour(self, order_info): for attr in order_info.__slots__: assert getattr(order_info, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(order_info)) == len(set(mro_slots(order_info))), "duplicate slot" def test_de_json(self, bot): json_dict = { "name": self.name, "phone_number": self.phone_number, "email": self.email, "shipping_address": self.shipping_address.to_dict(), } order_info = OrderInfo.de_json(json_dict, bot) assert order_info.api_kwargs == {} assert order_info.name == self.name assert order_info.phone_number == self.phone_number assert order_info.email == self.email assert order_info.shipping_address == self.shipping_address def test_to_dict(self, order_info): order_info_dict = order_info.to_dict() assert isinstance(order_info_dict, dict) assert order_info_dict["name"] == order_info.name assert order_info_dict["phone_number"] == order_info.phone_number assert order_info_dict["email"] == order_info.email assert order_info_dict["shipping_address"] == order_info.shipping_address.to_dict() def test_equality(self): a = OrderInfo( "name", "number", "mail", ShippingAddress("GB", "", "London", "12 Grimmauld Place", "", "WC1"), ) b = OrderInfo( "name", "number", "mail", ShippingAddress("GB", "", "London", "12 Grimmauld Place", "", "WC1"), ) c = OrderInfo( "name", "number", "mail", ShippingAddress("GB", "", "London", "13 Grimmauld Place", "", "WC1"), ) d = OrderInfo( "name", "number", "e-mail", ShippingAddress("GB", "", "London", "12 Grimmauld Place", "", "WC1"), ) e = ShippingAddress("GB", "", "London", "12 Grimmauld Place", "", "WC1") 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) python-telegram-bot-21.1.1/tests/_payment/test_precheckoutquery.py000066400000000000000000000124431460724040100254700ustar00rootroot00000000000000#!/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, OrderInfo, PreCheckoutQuery, Update, 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(scope="module") def pre_checkout_query(bot): pcq = PreCheckoutQuery( TestPreCheckoutQueryBase.id_, TestPreCheckoutQueryBase.from_user, TestPreCheckoutQueryBase.currency, TestPreCheckoutQueryBase.total_amount, TestPreCheckoutQueryBase.invoice_payload, shipping_option_id=TestPreCheckoutQueryBase.shipping_option_id, order_info=TestPreCheckoutQueryBase.order_info, ) pcq.set_bot(bot) return pcq class TestPreCheckoutQueryBase: id_ = 5 invoice_payload = "invoice_payload" shipping_option_id = "shipping_option_id" currency = "EUR" total_amount = 100 from_user = User(0, "", False) order_info = OrderInfo() class TestPreCheckoutQueryWithoutRequest(TestPreCheckoutQueryBase): def test_slot_behaviour(self, pre_checkout_query): inst = pre_checkout_query 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_, "invoice_payload": self.invoice_payload, "shipping_option_id": self.shipping_option_id, "currency": self.currency, "total_amount": self.total_amount, "from": self.from_user.to_dict(), "order_info": self.order_info.to_dict(), } pre_checkout_query = PreCheckoutQuery.de_json(json_dict, bot) assert pre_checkout_query.api_kwargs == {} assert pre_checkout_query.get_bot() is bot assert pre_checkout_query.id == self.id_ assert pre_checkout_query.invoice_payload == self.invoice_payload assert pre_checkout_query.shipping_option_id == self.shipping_option_id assert pre_checkout_query.currency == self.currency assert pre_checkout_query.from_user == self.from_user assert pre_checkout_query.order_info == self.order_info def test_to_dict(self, pre_checkout_query): pre_checkout_query_dict = pre_checkout_query.to_dict() assert isinstance(pre_checkout_query_dict, dict) assert pre_checkout_query_dict["id"] == pre_checkout_query.id assert pre_checkout_query_dict["invoice_payload"] == pre_checkout_query.invoice_payload assert ( pre_checkout_query_dict["shipping_option_id"] == pre_checkout_query.shipping_option_id ) assert pre_checkout_query_dict["currency"] == pre_checkout_query.currency assert pre_checkout_query_dict["from"] == pre_checkout_query.from_user.to_dict() assert pre_checkout_query_dict["order_info"] == pre_checkout_query.order_info.to_dict() def test_equality(self): a = PreCheckoutQuery( self.id_, self.from_user, self.currency, self.total_amount, self.invoice_payload ) b = PreCheckoutQuery( self.id_, self.from_user, self.currency, self.total_amount, self.invoice_payload ) c = PreCheckoutQuery(self.id_, None, "", 0, "") d = PreCheckoutQuery( 0, self.from_user, self.currency, self.total_amount, self.invoice_payload ) 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) async def test_answer(self, monkeypatch, pre_checkout_query): async def make_assertion(*_, **kwargs): return kwargs["pre_checkout_query_id"] == pre_checkout_query.id assert check_shortcut_signature( PreCheckoutQuery.answer, Bot.answer_pre_checkout_query, ["pre_checkout_query_id"], [] ) assert await check_shortcut_call( pre_checkout_query.answer, pre_checkout_query.get_bot(), "answer_pre_checkout_query", ) assert await check_defaults_handling( pre_checkout_query.answer, pre_checkout_query.get_bot() ) monkeypatch.setattr( pre_checkout_query.get_bot(), "answer_pre_checkout_query", make_assertion ) assert await pre_checkout_query.answer(ok=True) python-telegram-bot-21.1.1/tests/_payment/test_shippingaddress.py000066400000000000000000000116601460724040100252550ustar00rootroot00000000000000#!/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 ShippingAddress from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def shipping_address(): return ShippingAddress( TestShippingAddressBase.country_code, TestShippingAddressBase.state, TestShippingAddressBase.city, TestShippingAddressBase.street_line1, TestShippingAddressBase.street_line2, TestShippingAddressBase.post_code, ) class TestShippingAddressBase: country_code = "GB" state = "state" city = "London" street_line1 = "12 Grimmauld Place" street_line2 = "street_line2" post_code = "WC1" class TestShippingAddressWithoutRequest(TestShippingAddressBase): def test_slot_behaviour(self, shipping_address): inst = shipping_address 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 = { "country_code": self.country_code, "state": self.state, "city": self.city, "street_line1": self.street_line1, "street_line2": self.street_line2, "post_code": self.post_code, } shipping_address = ShippingAddress.de_json(json_dict, bot) assert shipping_address.api_kwargs == {} assert shipping_address.country_code == self.country_code assert shipping_address.state == self.state assert shipping_address.city == self.city assert shipping_address.street_line1 == self.street_line1 assert shipping_address.street_line2 == self.street_line2 assert shipping_address.post_code == self.post_code def test_to_dict(self, shipping_address): shipping_address_dict = shipping_address.to_dict() assert isinstance(shipping_address_dict, dict) assert shipping_address_dict["country_code"] == shipping_address.country_code assert shipping_address_dict["state"] == shipping_address.state assert shipping_address_dict["city"] == shipping_address.city assert shipping_address_dict["street_line1"] == shipping_address.street_line1 assert shipping_address_dict["street_line2"] == shipping_address.street_line2 assert shipping_address_dict["post_code"] == shipping_address.post_code def test_equality(self): a = ShippingAddress( self.country_code, self.state, self.city, self.street_line1, self.street_line2, self.post_code, ) b = ShippingAddress( self.country_code, self.state, self.city, self.street_line1, self.street_line2, self.post_code, ) d = ShippingAddress( "", self.state, self.city, self.street_line1, self.street_line2, self.post_code ) d2 = ShippingAddress( self.country_code, "", self.city, self.street_line1, self.street_line2, self.post_code, ) d3 = ShippingAddress( self.country_code, self.state, "", self.street_line1, self.street_line2, self.post_code, ) d4 = ShippingAddress( self.country_code, self.state, self.city, "", self.street_line2, self.post_code ) d5 = ShippingAddress( self.country_code, self.state, self.city, self.street_line1, "", self.post_code ) d6 = ShippingAddress( self.country_code, self.state, self.city, self.street_line1, self.street_line2, "" ) assert a == b assert hash(a) == hash(b) assert a is not b assert a != d assert hash(a) != hash(d) assert a != d2 assert hash(a) != hash(d2) assert a != d3 assert hash(a) != hash(d3) assert a != d4 assert hash(a) != hash(d4) assert a != d5 assert hash(a) != hash(d5) assert a != d6 assert hash(6) != hash(d6) python-telegram-bot-21.1.1/tests/_payment/test_shippingoption.py000066400000000000000000000054421460724040100251410ustar00rootroot00000000000000#!/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 LabeledPrice, ShippingOption, Voice from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def shipping_option(): return ShippingOption( TestShippingOptionBase.id_, TestShippingOptionBase.title, TestShippingOptionBase.prices ) class TestShippingOptionBase: id_ = "id" title = "title" prices = [LabeledPrice("Fish Container", 100), LabeledPrice("Premium Fish Container", 1000)] class TestShippingOptionWithoutRequest(TestShippingOptionBase): def test_slot_behaviour(self, shipping_option): inst = shipping_option 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, shipping_option): assert shipping_option.id == self.id_ assert shipping_option.title == self.title assert shipping_option.prices == tuple(self.prices) def test_to_dict(self, shipping_option): shipping_option_dict = shipping_option.to_dict() assert isinstance(shipping_option_dict, dict) assert shipping_option_dict["id"] == shipping_option.id assert shipping_option_dict["title"] == shipping_option.title assert shipping_option_dict["prices"][0] == shipping_option.prices[0].to_dict() assert shipping_option_dict["prices"][1] == shipping_option.prices[1].to_dict() def test_equality(self): a = ShippingOption(self.id_, self.title, self.prices) b = ShippingOption(self.id_, self.title, self.prices) c = ShippingOption(self.id_, "", []) d = ShippingOption(0, self.title, self.prices) e = Voice(self.id_, "someid", 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) python-telegram-bot-21.1.1/tests/_payment/test_shippingquery.py000066400000000000000000000104541460724040100247750ustar00rootroot00000000000000#!/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, ShippingAddress, ShippingQuery, Update, 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(scope="module") def shipping_query(bot): sq = ShippingQuery( TestShippingQueryBase.id_, TestShippingQueryBase.from_user, TestShippingQueryBase.invoice_payload, TestShippingQueryBase.shipping_address, ) sq.set_bot(bot) return sq class TestShippingQueryBase: id_ = "5" invoice_payload = "invoice_payload" from_user = User(0, "", False) shipping_address = ShippingAddress("GB", "", "London", "12 Grimmauld Place", "", "WC1") class TestShippingQueryWithoutRequest(TestShippingQueryBase): def test_slot_behaviour(self, shipping_query): inst = shipping_query 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_, "invoice_payload": self.invoice_payload, "from": self.from_user.to_dict(), "shipping_address": self.shipping_address.to_dict(), } shipping_query = ShippingQuery.de_json(json_dict, bot) assert shipping_query.api_kwargs == {} assert shipping_query.id == self.id_ assert shipping_query.invoice_payload == self.invoice_payload assert shipping_query.from_user == self.from_user assert shipping_query.shipping_address == self.shipping_address assert shipping_query.get_bot() is bot def test_to_dict(self, shipping_query): shipping_query_dict = shipping_query.to_dict() assert isinstance(shipping_query_dict, dict) assert shipping_query_dict["id"] == shipping_query.id assert shipping_query_dict["invoice_payload"] == shipping_query.invoice_payload assert shipping_query_dict["from"] == shipping_query.from_user.to_dict() assert shipping_query_dict["shipping_address"] == shipping_query.shipping_address.to_dict() def test_equality(self): a = ShippingQuery(self.id_, self.from_user, self.invoice_payload, self.shipping_address) b = ShippingQuery(self.id_, self.from_user, self.invoice_payload, self.shipping_address) c = ShippingQuery(self.id_, None, "", None) d = ShippingQuery(0, self.from_user, self.invoice_payload, self.shipping_address) 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) async def test_answer(self, monkeypatch, shipping_query): async def make_assertion(*_, **kwargs): return kwargs["shipping_query_id"] == shipping_query.id assert check_shortcut_signature( ShippingQuery.answer, Bot.answer_shipping_query, ["shipping_query_id"], [] ) assert await check_shortcut_call( shipping_query.answer, shipping_query._bot, "answer_shipping_query" ) assert await check_defaults_handling(shipping_query.answer, shipping_query._bot) monkeypatch.setattr(shipping_query._bot, "answer_shipping_query", make_assertion) assert await shipping_query.answer(ok=True) python-telegram-bot-21.1.1/tests/_payment/test_successfulpayment.py000066400000000000000000000116361460724040100256460ustar00rootroot00000000000000#!/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 OrderInfo, SuccessfulPayment from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def successful_payment(): return SuccessfulPayment( TestSuccessfulPaymentBase.currency, TestSuccessfulPaymentBase.total_amount, TestSuccessfulPaymentBase.invoice_payload, TestSuccessfulPaymentBase.telegram_payment_charge_id, TestSuccessfulPaymentBase.provider_payment_charge_id, shipping_option_id=TestSuccessfulPaymentBase.shipping_option_id, order_info=TestSuccessfulPaymentBase.order_info, ) class TestSuccessfulPaymentBase: invoice_payload = "invoice_payload" shipping_option_id = "shipping_option_id" currency = "EUR" total_amount = 100 order_info = OrderInfo() telegram_payment_charge_id = "telegram_payment_charge_id" provider_payment_charge_id = "provider_payment_charge_id" class TestSuccessfulPaymentWithoutRequest(TestSuccessfulPaymentBase): def test_slot_behaviour(self, successful_payment): inst = successful_payment 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 = { "invoice_payload": self.invoice_payload, "shipping_option_id": self.shipping_option_id, "currency": self.currency, "total_amount": self.total_amount, "order_info": self.order_info.to_dict(), "telegram_payment_charge_id": self.telegram_payment_charge_id, "provider_payment_charge_id": self.provider_payment_charge_id, } successful_payment = SuccessfulPayment.de_json(json_dict, bot) assert successful_payment.api_kwargs == {} assert successful_payment.invoice_payload == self.invoice_payload assert successful_payment.shipping_option_id == self.shipping_option_id assert successful_payment.currency == self.currency assert successful_payment.order_info == self.order_info assert successful_payment.telegram_payment_charge_id == self.telegram_payment_charge_id assert successful_payment.provider_payment_charge_id == self.provider_payment_charge_id def test_to_dict(self, successful_payment): successful_payment_dict = successful_payment.to_dict() assert isinstance(successful_payment_dict, dict) assert successful_payment_dict["invoice_payload"] == successful_payment.invoice_payload assert ( successful_payment_dict["shipping_option_id"] == successful_payment.shipping_option_id ) assert successful_payment_dict["currency"] == successful_payment.currency assert successful_payment_dict["order_info"] == successful_payment.order_info.to_dict() assert ( successful_payment_dict["telegram_payment_charge_id"] == successful_payment.telegram_payment_charge_id ) assert ( successful_payment_dict["provider_payment_charge_id"] == successful_payment.provider_payment_charge_id ) def test_equality(self): a = SuccessfulPayment( self.currency, self.total_amount, self.invoice_payload, self.telegram_payment_charge_id, self.provider_payment_charge_id, ) b = SuccessfulPayment( self.currency, self.total_amount, self.invoice_payload, self.telegram_payment_charge_id, self.provider_payment_charge_id, ) c = SuccessfulPayment( "", 0, "", self.telegram_payment_charge_id, self.provider_payment_charge_id ) d = SuccessfulPayment( self.currency, self.total_amount, self.invoice_payload, self.telegram_payment_charge_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) python-telegram-bot-21.1.1/tests/_utils/000077500000000000000000000000001460724040100201345ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/_utils/__init__.py000066400000000000000000000014661460724040100222540ustar00rootroot00000000000000#!/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/]. python-telegram-bot-21.1.1/tests/_utils/test_datetime.py000066400000000000000000000202141460724040100233400ustar00rootroot00000000000000#!/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 time import pytest from telegram._utils import datetime as tg_dtm from telegram.ext import Defaults # sample time specification values categorised into absolute / delta / time-of-day from tests.auxil.envvars import TEST_WITH_OPT_DEPS # We do not parametrize tests with these variables, since there's a tiny chance that there is an # error while collecting the tests (happens when time goes from HH:59:00 -> HH+1:00:00) when we # run the test suite with multiple workers DELTA_TIME_SPECS = [dtm.timedelta(hours=3, seconds=42, milliseconds=2), 30, 7.5] TIME_OF_DAY_TIME_SPECS = [ dtm.time(12, 42, tzinfo=dtm.timezone(dtm.timedelta(hours=-7))), dtm.time(12, 42), ] RELATIVE_TIME_SPECS = DELTA_TIME_SPECS + TIME_OF_DAY_TIME_SPECS ABSOLUTE_TIME_SPECS = [ dtm.datetime.now(tz=dtm.timezone(dtm.timedelta(hours=-7))), dtm.datetime.utcnow(), ] TIME_SPECS = ABSOLUTE_TIME_SPECS + RELATIVE_TIME_SPECS """ This part is here because pytz is just installed as dependency of the optional dependency APScheduler, so we don't always have pytz (unless the user installs it). Because imports in pytest are intricate, we just run pytest -k test_datetime.py with the TEST_WITH_OPT_DEPS=False environment variable in addition to the regular test suite. """ class TestDatetime: @staticmethod def localize(dt, tzinfo): if TEST_WITH_OPT_DEPS: return tzinfo.localize(dt) return dt.replace(tzinfo=tzinfo) def test_helpers_utc(self): # Here we just test, that we got the correct UTC variant if not TEST_WITH_OPT_DEPS: assert tg_dtm.UTC is tg_dtm.DTM_UTC else: assert tg_dtm.UTC is not tg_dtm.DTM_UTC def test_to_float_timestamp_absolute_naive(self): """Conversion from timezone-naive datetime to timestamp. Naive datetimes should be assumed to be in UTC. """ datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5) assert tg_dtm.to_float_timestamp(datetime) == 1573431976.1 def test_to_float_timestamp_absolute_naive_no_pytz(self, monkeypatch): """Conversion from timezone-naive datetime to timestamp. Naive datetimes should be assumed to be in UTC. """ monkeypatch.setattr(tg_dtm, "UTC", tg_dtm.DTM_UTC) datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5) assert tg_dtm.to_float_timestamp(datetime) == 1573431976.1 def test_to_float_timestamp_absolute_aware(self, timezone): """Conversion from timezone-aware datetime to timestamp""" # we're parametrizing this with two different UTC offsets to exclude the possibility # of an xpass when the test is run in a timezone with the same UTC offset test_datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5) datetime = self.localize(test_datetime, timezone) assert ( tg_dtm.to_float_timestamp(datetime) == 1573431976.1 - timezone.utcoffset(test_datetime).total_seconds() ) def test_to_float_timestamp_absolute_no_reference(self): """A reference timestamp is only relevant for relative time specifications""" with pytest.raises(ValueError, match="while reference_timestamp is not None"): tg_dtm.to_float_timestamp(dtm.datetime(2019, 11, 11), reference_timestamp=123) # see note on parametrization at the top of this file def test_to_float_timestamp_delta(self): """Conversion from a 'delta' time specification to timestamp""" reference_t = 0 for i in DELTA_TIME_SPECS: delta = i.total_seconds() if hasattr(i, "total_seconds") else i assert ( tg_dtm.to_float_timestamp(i, reference_t) == reference_t + delta ), f"failed for {i}" def test_to_float_timestamp_time_of_day(self): """Conversion from time-of-day specification to timestamp""" hour, hour_delta = 12, 1 ref_t = tg_dtm._datetime_to_float_timestamp(dtm.datetime(1970, 1, 1, hour=hour)) # test for a time of day that is still to come, and one in the past time_future, time_past = dtm.time(hour + hour_delta), dtm.time(hour - hour_delta) assert tg_dtm.to_float_timestamp(time_future, ref_t) == ref_t + 60 * 60 * hour_delta assert tg_dtm.to_float_timestamp(time_past, ref_t) == ref_t + 60 * 60 * (24 - hour_delta) def test_to_float_timestamp_time_of_day_timezone(self, timezone): """Conversion from timezone-aware time-of-day specification to timestamp""" # we're parametrizing this with two different UTC offsets to exclude the possibility # of an xpass when the test is run in a timezone with the same UTC offset ref_datetime = dtm.datetime(1970, 1, 1, 12) utc_offset = timezone.utcoffset(ref_datetime) ref_t, time_of_day = tg_dtm._datetime_to_float_timestamp(ref_datetime), ref_datetime.time() aware_time_of_day = self.localize(ref_datetime, timezone).timetz() # first test that naive time is assumed to be utc: assert tg_dtm.to_float_timestamp(time_of_day, ref_t) == pytest.approx(ref_t) # test that by setting the timezone the timestamp changes accordingly: assert tg_dtm.to_float_timestamp(aware_time_of_day, ref_t) == pytest.approx( ref_t + (-utc_offset.total_seconds() % (24 * 60 * 60)) ) # see note on parametrization at the top of this file def test_to_float_timestamp_default_reference(self): """The reference timestamp for relative time specifications should default to now""" for i in RELATIVE_TIME_SPECS: now = time.time() assert tg_dtm.to_float_timestamp(i) == pytest.approx( tg_dtm.to_float_timestamp(i, reference_timestamp=now) ), f"Failed for {i}" def test_to_float_timestamp_error(self): with pytest.raises(TypeError, match="Defaults"): tg_dtm.to_float_timestamp(Defaults()) # see note on parametrization at the top of this file def test_to_timestamp(self): # delegate tests to `to_float_timestamp` for i in TIME_SPECS: assert tg_dtm.to_timestamp(i) == int(tg_dtm.to_float_timestamp(i)), f"Failed for {i}" def test_to_timestamp_none(self): # this 'convenience' behaviour has been left left for backwards compatibility assert tg_dtm.to_timestamp(None) is None def test_from_timestamp_none(self): assert tg_dtm.from_timestamp(None) is None def test_from_timestamp_naive(self): datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, tzinfo=dtm.timezone.utc) assert tg_dtm.from_timestamp(1573431976, tzinfo=None) == datetime def test_from_timestamp_aware(self, timezone): # we're parametrizing this with two different UTC offsets to exclude the possibility # of an xpass when the test is run in a timezone with the same UTC offset test_datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5) datetime = self.localize(test_datetime, timezone) assert ( tg_dtm.from_timestamp(1573431976.1 - timezone.utcoffset(test_datetime).total_seconds()) == datetime ) def test_extract_tzinfo_from_defaults(self, tz_bot, bot, raw_bot): assert tg_dtm.extract_tzinfo_from_defaults(tz_bot) == tz_bot.defaults.tzinfo assert tg_dtm.extract_tzinfo_from_defaults(bot) is None assert tg_dtm.extract_tzinfo_from_defaults(raw_bot) is None python-telegram-bot-21.1.1/tests/_utils/test_defaultvalue.py000066400000000000000000000045371460724040100242370ustar00rootroot00000000000000#!/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 User from telegram._utils.defaultvalue import DefaultValue from tests.auxil.slots import mro_slots class TestDefaultValue: def test_slot_behaviour(self): inst = DefaultValue(1) 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_identity(self): df_1 = DefaultValue(1) df_2 = DefaultValue(2) assert df_1 is not df_2 assert df_1 != df_2 @pytest.mark.parametrize( ("value", "expected"), [ ({}, False), ({1: 2}, True), (None, False), (True, True), (1, True), (0, False), (False, False), ([], False), ([1], True), ], ) def test_truthiness(self, value, expected): assert bool(DefaultValue(value)) == expected @pytest.mark.parametrize( "value", ["string", 1, True, [1, 2, 3], {1: 3}, DefaultValue(1), User(1, "first", False)] ) def test_string_representations(self, value): df = DefaultValue(value) assert str(df) == f"DefaultValue({value})" assert repr(df) == repr(value) def test_as_function_argument(self): default_one = DefaultValue(1) def foo(arg=default_one): if arg is default_one: return 1 return 2 assert foo() == 1 assert foo(None) == 2 assert foo(1) == 2 python-telegram-bot-21.1.1/tests/_utils/test_files.py000066400000000000000000000142021460724040100226460ustar00rootroot00000000000000#!/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 contextlib import subprocess import sys from pathlib import Path import pytest import telegram._utils.datetime import telegram._utils.files from telegram import Animation, InputFile, MessageEntity from tests.auxil.files import TEST_DATA_PATH, data_file class TestFiles: @pytest.mark.parametrize( ("string", "expected"), [ (str(data_file("game.gif")), True), (str(TEST_DATA_PATH), False), (data_file("game.gif"), True), (TEST_DATA_PATH, False), ("https:/api.org/file/botTOKEN/document/file_3", False), (None, False), ], ) def test_is_local_file(self, string, expected): assert telegram._utils.files.is_local_file(string) == expected @pytest.mark.parametrize( ("string", "expected_local", "expected_non_local"), [ (data_file("game.gif"), data_file("game.gif").as_uri(), InputFile), (TEST_DATA_PATH, TEST_DATA_PATH, TEST_DATA_PATH), ("file://foobar", "file://foobar", ValueError), (str(data_file("game.gif")), data_file("game.gif").as_uri(), InputFile), (str(TEST_DATA_PATH), str(TEST_DATA_PATH), str(TEST_DATA_PATH)), ( "https:/api.org/file/botTOKEN/document/file_3", "https:/api.org/file/botTOKEN/document/file_3", "https:/api.org/file/botTOKEN/document/file_3", ), ], ids=[ "Path(local_file)", "Path(directory)", "file_uri", "str-path local file", "str-path directory", "URL", ], ) def test_parse_file_input_string(self, string, expected_local, expected_non_local): assert telegram._utils.files.parse_file_input(string, local_mode=True) == expected_local if expected_non_local is InputFile: assert isinstance( telegram._utils.files.parse_file_input(string, local_mode=False), InputFile ) elif expected_non_local is ValueError: with pytest.raises(ValueError, match="but local mode is not enabled."): telegram._utils.files.parse_file_input(string, local_mode=False) else: assert ( telegram._utils.files.parse_file_input(string, local_mode=False) == expected_non_local ) def test_parse_file_input_file_like(self): source_file = data_file("game.gif") with source_file.open("rb") as file: parsed = telegram._utils.files.parse_file_input(file) assert isinstance(parsed, InputFile) assert parsed.filename == "game.gif" with source_file.open("rb") as file: parsed = telegram._utils.files.parse_file_input(file, filename="test_file") assert isinstance(parsed, InputFile) assert parsed.filename == "test_file" def test_parse_file_input_bytes(self): source_file = data_file("text_file.txt") parsed = telegram._utils.files.parse_file_input(source_file.read_bytes()) assert isinstance(parsed, InputFile) assert parsed.filename == "application.octet-stream" parsed = telegram._utils.files.parse_file_input( source_file.read_bytes(), filename="test_file" ) assert isinstance(parsed, InputFile) assert parsed.filename == "test_file" def test_parse_file_input_tg_object(self): animation = Animation("file_id", "unique_id", 1, 1, 1) assert telegram._utils.files.parse_file_input(animation, Animation) == "file_id" assert telegram._utils.files.parse_file_input(animation, MessageEntity) is animation @pytest.mark.parametrize("obj", [{1: 2}, [1, 2], (1, 2)]) def test_parse_file_input_other(self, obj): assert telegram._utils.files.parse_file_input(obj) is obj @pytest.mark.parametrize("attach", [True, False]) def test_parse_file_input_attach(self, attach): source_file = data_file("text_file.txt") parsed = telegram._utils.files.parse_file_input(source_file.read_bytes(), attach=attach) assert isinstance(parsed, InputFile) assert bool(parsed.attach_name) is attach def test_load_file_none(self): assert telegram._utils.files.load_file(None) == (None, None) @pytest.mark.parametrize("arg", [b"bytes", "string", InputFile(b"content"), Path("file/path")]) def test_load_file_no_file(self, arg): out = telegram._utils.files.load_file(arg) assert out[0] is None assert out[1] is arg def test_load_file_file_handle(self): out = telegram._utils.files.load_file(data_file("telegram.gif").open("rb")) assert out[0] == "telegram.gif" assert out[1] == data_file("telegram.gif").read_bytes() def test_load_file_subprocess_pipe(self): png_file = data_file("telegram.png") cmd_str = "type" if sys.platform == "win32" else "cat" cmd = [cmd_str, str(png_file)] proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=(sys.platform == "win32")) out = telegram._utils.files.load_file(proc.stdout) assert out[0] is None assert out[1] == png_file.read_bytes() with contextlib.suppress(ProcessLookupError): proc.kill() # This exception may be thrown if the process has finished before we had the chance # to kill it. python-telegram-bot-21.1.1/tests/auxil/000077500000000000000000000000001460724040100177575ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/auxil/__init__.py000066400000000000000000000014661460724040100220770ustar00rootroot00000000000000#!/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/]. python-telegram-bot-21.1.1/tests/auxil/asyncio_helpers.py000066400000000000000000000035051460724040100235230ustar00rootroot00000000000000#!/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 from typing import Callable def call_after(function: Callable, after: Callable): """Run a callable after another has executed. Useful when trying to make sure that a function did actually run, but just monkeypatching it doesn't work because this would break some other functionality. Example usage: def test_stuff(self, bot, monkeypatch): def after(arg): # arg is the return value of `send_message` self.received = arg monkeypatch.setattr(bot, 'send_message', call_after(bot.send_message, after) """ if asyncio.iscoroutinefunction(function): async def wrapped(*args, **kwargs): out = await function(*args, **kwargs) if asyncio.iscoroutinefunction(after): await after(out) else: after(out) return out else: def wrapped(*args, **kwargs): out = function(*args, **kwargs) after(out) return out return wrapped python-telegram-bot-21.1.1/tests/auxil/bot_method_checks.py000066400000000000000000000677231460724040100240140ustar00rootroot00000000000000#!/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/]. """Provides functions to test both methods.""" import datetime import functools import inspect import re from typing import Any, Callable, Collection, Dict, Iterable, List, Optional, Tuple import pytest import telegram # for ForwardRef resolution from telegram import ( Bot, ChatPermissions, File, InlineQueryResultArticle, InlineQueryResultCachedPhoto, InputMediaPhoto, InputTextMessageContent, LinkPreviewOptions, ReplyParameters, TelegramObject, ) from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram.constants import InputMediaType from telegram.ext import Defaults, ExtBot from telegram.request import RequestData from tests.auxil.envvars import TEST_WITH_OPT_DEPS if TEST_WITH_OPT_DEPS: import pytz FORWARD_REF_PATTERN = re.compile(r"ForwardRef\('(?P\w+)'\)") """ A pattern to find a class name in a ForwardRef typing annotation. Class name (in a named group) is surrounded by parentheses and single quotes. """ def check_shortcut_signature( shortcut: Callable, bot_method: Callable, shortcut_kwargs: List[str], additional_kwargs: List[str], annotation_overrides: Optional[Dict[str, Tuple[Any, Any]]] = None, ) -> bool: """ Checks that the signature of a shortcut matches the signature of the underlying bot method. Args: shortcut: The shortcut, e.g. :meth:`telegram.Message.reply_text` bot_method: The bot method, e.g. :meth:`telegram.Bot.send_message` shortcut_kwargs: The kwargs passed by the shortcut directly, e.g. ``chat_id`` additional_kwargs: Additional kwargs of the shortcut that the bot method doesn't have, e.g. ``quote``. annotation_overrides: A dictionary of exceptions for the annotation comparison. The key is the name of the argument, the value is a tuple of the expected annotation and the default value. E.g. ``{'parse_mode': (str, 'None')}``. Returns: :obj:`bool`: Whether or not the signature matches. """ annotation_overrides = annotation_overrides or {} def resolve_class(class_name: str) -> Optional[type]: """Attempts to resolve a PTB class (telegram module only) from a ForwardRef. E.g. resolves from "StickerSet". Returns a class on success, :obj:`None` if nothing could be resolved. """ for module in telegram, telegram.request: cls = getattr(module, class_name, None) if cls is not None: return cls return None # for ruff shortcut_sig = inspect.signature(shortcut) effective_shortcut_args = set(shortcut_sig.parameters.keys()).difference(additional_kwargs) effective_shortcut_args.discard("self") bot_sig = inspect.signature(bot_method) expected_args = set(bot_sig.parameters.keys()).difference(shortcut_kwargs) expected_args.discard("self") len_expected = len(expected_args) len_effective = len(effective_shortcut_args) if len_expected > len_effective: raise Exception( f"Shortcut signature is missing {len_expected - len_effective} arguments " f"of the underlying Bot method: {expected_args - effective_shortcut_args}" ) if len_expected < len_effective: raise Exception( f"Shortcut signature has {len_effective - len_expected} additional arguments " f"that the Bot method doesn't have: {effective_shortcut_args - expected_args}" ) # TODO: Also check annotation of return type. Would currently be a hassle b/c typing doesn't # resolve `ForwardRef('Type')` to `Type`. For now we rely on MyPy, which probably allows the # shortcuts to return more specific types than the bot method, but it's only annotations after # all for kwarg in effective_shortcut_args: expected_kind = bot_sig.parameters[kwarg].kind if shortcut_sig.parameters[kwarg].kind != expected_kind: raise Exception(f"Argument {kwarg} must be of kind {expected_kind}.") if kwarg in annotation_overrides: if shortcut_sig.parameters[kwarg].annotation != annotation_overrides[kwarg][0]: raise Exception( f"For argument {kwarg} I expected {annotation_overrides[kwarg]}, " f"but got {shortcut_sig.parameters[kwarg].annotation}" ) continue if bot_sig.parameters[kwarg].annotation != shortcut_sig.parameters[kwarg].annotation: if FORWARD_REF_PATTERN.search(str(shortcut_sig.parameters[kwarg])): # If a shortcut signature contains a ForwardRef, the simple comparison of # annotations can fail. Try and resolve the .__args__, then compare them. for shortcut_arg, bot_arg in zip( shortcut_sig.parameters[kwarg].annotation.__args__, bot_sig.parameters[kwarg].annotation.__args__, ): shortcut_arg_to_check = shortcut_arg # for ruff match = FORWARD_REF_PATTERN.search(str(shortcut_arg)) if match: shortcut_arg_to_check = resolve_class(match.group("class_name")) if shortcut_arg_to_check != bot_arg: raise Exception( f"For argument {kwarg} I expected " f"{bot_sig.parameters[kwarg].annotation}, but " f"got {shortcut_sig.parameters[kwarg].annotation}." f"Comparison of {shortcut_arg} and {bot_arg} failed." ) elif isinstance(bot_sig.parameters[kwarg].annotation, type): if bot_sig.parameters[kwarg].annotation.__name__ != str( shortcut_sig.parameters[kwarg].annotation ): raise Exception( f"For argument {kwarg} I expected {bot_sig.parameters[kwarg].annotation}, " f"but got {shortcut_sig.parameters[kwarg].annotation}" ) else: raise Exception( f"For argument {kwarg} I expected {bot_sig.parameters[kwarg].annotation}," f"but got {shortcut_sig.parameters[kwarg].annotation}" ) bot_method_sig = inspect.signature(bot_method) shortcut_sig = inspect.signature(shortcut) for arg in expected_args: if arg in annotation_overrides: if shortcut_sig.parameters[arg].default == annotation_overrides[arg][1]: continue raise Exception( f"For argument {arg} I expected default {annotation_overrides[arg][1]}, " f"but got {shortcut_sig.parameters[arg].default}" ) if not shortcut_sig.parameters[arg].default == bot_method_sig.parameters[arg].default: raise Exception( f"Default for argument {arg} does not match the default of the Bot method." ) for kwarg in additional_kwargs: if kwarg == "reply_to_message_id": # special case for deprecated argument of Message.reply_* continue if not shortcut_sig.parameters[kwarg].kind == inspect.Parameter.KEYWORD_ONLY: raise Exception(f"Argument {kwarg} must be a positional-only argument!") return True async def check_shortcut_call( shortcut_method: Callable, bot: ExtBot, bot_method_name: str, skip_params: Optional[Iterable[str]] = None, shortcut_kwargs: Optional[Iterable[str]] = None, ) -> bool: """ Checks that a shortcut passes all the existing arguments to the underlying bot method. Use as:: assert await check_shortcut_call(message.reply_text, message.bot, 'send_message') Args: shortcut_method: The shortcut method, e.g. `message.reply_text` bot: The bot bot_method_name: The bot methods name, e.g. `'send_message'` skip_params: Parameters that are allowed to be missing, e.g. `['inline_message_id']` `rate_limit_args` will be skipped by default shortcut_kwargs: The kwargs passed by the shortcut directly, e.g. ``chat_id`` Returns: :obj:`bool` """ skip_params = set() if not skip_params else set(skip_params) skip_params.add("rate_limit_args") shortcut_kwargs = set() if not shortcut_kwargs else set(shortcut_kwargs) orig_bot_method = getattr(bot, bot_method_name) bot_signature = inspect.signature(orig_bot_method) expected_args = set(bot_signature.parameters.keys()) - {"self"} - set(skip_params) positional_args = { name for name, param in bot_signature.parameters.items() if param.default == param.empty } ignored_args = positional_args | set(shortcut_kwargs) shortcut_signature = inspect.signature(shortcut_method) # auto_pagination: Special casing for InlineQuery.answer # quote: Don't test deprecated "quote" parameter of Message.reply_* kwargs = { name: name for name in shortcut_signature.parameters if name not in ["auto_pagination", "quote"] } if "reply_parameters" in kwargs: kwargs["reply_parameters"] = ReplyParameters(message_id=1) # We tested this for a long time, but Bot API 7.0 deprecated it in favor of # reply_parameters. In the transition phase, both exist in a mutually exclusive # way. Testing both cases would require a lot of additional code, so we just # ignore this parameter here until it is removed. kwargs.pop("reply_to_message_id", None) expected_args.discard("reply_to_message_id") async def make_assertion(**kw): # name == value makes sure that # a) we receive non-None input for all parameters # b) we receive the correct input for each kwarg received_kwargs = { name for name, value in kw.items() if name in ignored_args or (value == name or (name == "reply_parameters" and value.message_id == 1)) } if not received_kwargs == expected_args: raise Exception( f"{orig_bot_method.__name__} did not receive correct value for the parameters " f"{expected_args - received_kwargs}" ) if bot_method_name == "get_file": # This is here mainly for PassportFile.get_file, which calls .set_credentials on the # return value return File(file_id="result", file_unique_id="result") return True setattr(bot, bot_method_name, make_assertion) try: await shortcut_method(**kwargs) except Exception as exc: raise exc finally: setattr(bot, bot_method_name, orig_bot_method) return True def build_kwargs( signature: inspect.Signature, default_kwargs, manually_passed_value: Any = DEFAULT_NONE ): kws = {} for name, param in signature.parameters.items(): # For required params we need to pass something if param.default is inspect.Parameter.empty: # Some special casing if name == "permissions": kws[name] = ChatPermissions() elif name in ["prices", "commands", "errors"]: kws[name] = [] elif name == "media": media = InputMediaPhoto("media", parse_mode=manually_passed_value) if "list" in str(param.annotation).lower(): kws[name] = [media] else: kws[name] = media elif name == "results": itmc = InputTextMessageContent( "text", parse_mode=manually_passed_value, link_preview_options=LinkPreviewOptions( is_disabled=manually_passed_value, url=manually_passed_value ), ) kws[name] = [ InlineQueryResultArticle("id", "title", input_message_content=itmc), InlineQueryResultCachedPhoto( "id", "photo_file_id", parse_mode=manually_passed_value, input_message_content=itmc, ), ] elif name == "ok": kws["ok"] = False kws["error_message"] = "error" else: kws[name] = True # pass values for params that can have defaults only if we don't want to use the # standard default elif name in default_kwargs: if manually_passed_value != DEFAULT_NONE: if name == "link_preview_options": kws[name] = LinkPreviewOptions( is_disabled=manually_passed_value, url=manually_passed_value ) else: kws[name] = manually_passed_value # Some special casing for methods that have "exactly one of the optionals" type args elif name in ["location", "contact", "venue", "inline_message_id"]: kws[name] = True elif name == "until_date": if manually_passed_value not in [None, DEFAULT_NONE]: # Europe/Berlin kws[name] = pytz.timezone("Europe/Berlin").localize( datetime.datetime(2000, 1, 1, 0) ) else: # naive UTC kws[name] = datetime.datetime(2000, 1, 1, 0) elif name == "reply_parameters": kws[name] = telegram.ReplyParameters( message_id=1, allow_sending_without_reply=manually_passed_value, quote_parse_mode=manually_passed_value, ) return kws def make_assertion_for_link_preview_options( expected_defaults_value, lpo, manual_value_expected, manually_passed_value ): if not lpo: return # if no_value_expected: # # We always expect a value for link_preview_options, because we don't test the # # case send_message(…, link_preview_options=None). Instead we focus on the more # # compicated case of send_message(…, link_preview_options=LinkPreviewOptions(arg=None)) if manual_value_expected: if lpo.get("is_disabled") != manually_passed_value: pytest.fail( f"Got value {lpo.get('is_disabled')} for link_preview_options.is_disabled, " f"expected it to be {manually_passed_value}" ) if lpo.get("url") != manually_passed_value: pytest.fail( f"Got value {lpo.get('url')} for link_preview_options.url, " f"expected it to be {manually_passed_value}" ) if expected_defaults_value: if lpo.get("show_above_text") != expected_defaults_value: pytest.fail( f"Got value {lpo.get('show_above_text')} for link_preview_options.show_above_text," f" expected it to be {expected_defaults_value}" ) if manually_passed_value is DEFAULT_NONE and lpo.get("url") != expected_defaults_value: pytest.fail( f"Got value {lpo.get('url')} for link_preview_options.url, " f"expected it to be {expected_defaults_value}" ) async def make_assertion( url, request_data: RequestData, method_name: str, kwargs_need_default: List[str], return_value, manually_passed_value: Any = DEFAULT_NONE, expected_defaults_value: Any = DEFAULT_NONE, *args, **kwargs, ): data = request_data.parameters no_value_expected = (manually_passed_value is None) or ( manually_passed_value is DEFAULT_NONE and expected_defaults_value is None ) manual_value_expected = (manually_passed_value is not DEFAULT_NONE) and not no_value_expected default_value_expected = (not manual_value_expected) and (not no_value_expected) # Check reply_parameters - needs special handling b/c we merge this with the default # value for `allow_sending_without_reply` reply_parameters = data.pop("reply_parameters", None) if reply_parameters: for param in ["allow_sending_without_reply", "quote_parse_mode"]: if no_value_expected and param in reply_parameters: pytest.fail(f"Got value for reply_parameters.{param}, expected it to be absent") param_value = reply_parameters.get(param) if manual_value_expected and param_value != manually_passed_value: pytest.fail( f"Got value {param_value} for reply_parameters.{param} " f"instead of {manually_passed_value}" ) elif default_value_expected and param_value != expected_defaults_value: pytest.fail( f"Got value {param_value} for reply_parameters.{param} " f"instead of {expected_defaults_value}" ) # Check link_preview_options - needs special handling b/c we merge this with the default # values specified in `Defaults.link_preview_options` make_assertion_for_link_preview_options( expected_defaults_value, data.get("link_preview_options", None), manual_value_expected, manually_passed_value, ) # Check regular arguments that need defaults for arg in kwargs_need_default: if arg == "link_preview_options": # already handled above continue # 'None' should not be passed along to Telegram if no_value_expected and arg in data: pytest.fail(f"Got value {data[arg]} for argument {arg}, expected it to be absent") value = data.get(arg, "`not passed at all`") if manual_value_expected and value != manually_passed_value: pytest.fail(f"Got value {value} for argument {arg} instead of {manually_passed_value}") elif default_value_expected and value != expected_defaults_value: pytest.fail( f"Got value {value} for argument {arg} instead of {expected_defaults_value}" ) # Check InputMedia (parse_mode can have a default) def check_input_media(m: Dict): parse_mode = m.get("parse_mode") if no_value_expected and parse_mode is not None: pytest.fail("InputMedia has non-None parse_mode, expected it to be absent") elif default_value_expected and parse_mode != expected_defaults_value: pytest.fail( f"Got value {parse_mode} for InputMedia.parse_mode instead " f"of {expected_defaults_value}" ) elif manual_value_expected and parse_mode != manually_passed_value: pytest.fail( f"Got value {parse_mode} for InputMedia.parse_mode instead " f"of {manually_passed_value}" ) media = data.pop("media", None) if media: if isinstance(media, dict) and isinstance(media.get("type", None), InputMediaType): check_input_media(media) else: for m in media: check_input_media(m) # Check InlineQueryResults results = data.pop("results", []) for result in results: if no_value_expected and "parse_mode" in result: pytest.fail("ILQR has a parse mode, expected it to be absent") # Here we explicitly use that we only pass ILQRPhoto and ILQRArticle for testing elif "photo" in result: parse_mode = result.get("parse_mode") if manually_passed_value and parse_mode != manually_passed_value: pytest.fail( f"Got value {parse_mode} for ILQR.parse_mode instead of " f"{manually_passed_value}" ) elif default_value_expected and parse_mode != expected_defaults_value: pytest.fail( f"Got value {parse_mode} for ILQR.parse_mode instead of " f"{expected_defaults_value}" ) # Here we explicitly use that we only pass InputTextMessageContent for testing # which has both parse_mode and link_preview_options imc = result.get("input_message_content") if not imc: continue if no_value_expected and "parse_mode" in imc: pytest.fail("ILQR.i_m_c has a parse_mode, expected it to be absent") parse_mode = imc.get("parse_mode") if manual_value_expected and parse_mode != manually_passed_value: pytest.fail( f"Got value {imc.parse_mode} for ILQR.i_m_c.parse_mode " f"instead of {manual_value_expected}" ) elif default_value_expected and parse_mode != expected_defaults_value: pytest.fail( f"Got value {imc.parse_mode} for ILQR.i_m_c.parse_mode " f"instead of {expected_defaults_value}" ) make_assertion_for_link_preview_options( expected_defaults_value, imc.get("link_preview_options", None), manual_value_expected, manually_passed_value, ) # Check datetime conversion until_date = data.pop("until_date", None) if until_date: if manual_value_expected and until_date != 946681200: pytest.fail("Non-naive until_date should have been interpreted as Europe/Berlin.") if not any((manually_passed_value, expected_defaults_value)) and until_date != 946684800: pytest.fail("Naive until_date should have been interpreted as UTC") if default_value_expected and until_date != 946702800: pytest.fail("Naive until_date should have been interpreted as America/New_York") if method_name in ["get_file", "get_small_file", "get_big_file"]: # This is here mainly for PassportFile.get_file, which calls .set_credentials on the # return value out = File(file_id="result", file_unique_id="result") return out.to_dict() # Otherwise return None by default, as TGObject.de_json/list(None) in [None, []] # That way we can check what gets passed to Request.post without having to actually # make a request # Some methods expect specific output, so we allow to customize that if isinstance(return_value, TelegramObject): return return_value.to_dict() return return_value async def check_defaults_handling( method: Callable, bot: Bot, return_value=None, no_default_kwargs: Collection[str] = frozenset(), ) -> bool: """ Checks that tg.ext.Defaults are handled correctly. Args: method: The shortcut/bot_method bot: The bot. May be a telegram.Bot or a telegram.ext.ExtBot. In the former case, all default values will be converted to None. return_value: Optional. The return value of Bot._post that the method expects. Defaults to None. get_file is automatically handled. If this is a `TelegramObject`, Bot._post will return the `to_dict` representation of it. no_default_kwargs: Optional. A collection of keyword arguments that should not have default values. Defaults to an empty frozenset. """ raw_bot = not isinstance(bot, ExtBot) get_updates = method.__name__.lower().replace("_", "") == "getupdates" shortcut_signature = inspect.signature(method) kwargs_need_default = { kwarg for kwarg, value in shortcut_signature.parameters.items() if isinstance(value.default, DefaultValue) and not kwarg.endswith("_timeout") and kwarg not in no_default_kwargs } # We tested this for a long time, but Bot API 7.0 deprecated it in favor of # reply_parameters. In the transition phase, both exist in a mutually exclusive # way. Testing both cases would require a lot of additional code, so we for now are content # with the explicit tests that we have inplace for allow_sending_without_reply kwargs_need_default.discard("allow_sending_without_reply") if method.__name__.endswith("_media_group"): # the parse_mode is applied to the first media item, and we test this elsewhere kwargs_need_default.remove("parse_mode") defaults_no_custom_defaults = Defaults() kwargs = {kwarg: "custom_default" for kwarg in inspect.signature(Defaults).parameters} kwargs["tzinfo"] = pytz.timezone("America/New_York") kwargs.pop("disable_web_page_preview") # mutually exclusive with link_preview_options kwargs.pop("quote") # mutually exclusive with do_quote kwargs["link_preview_options"] = LinkPreviewOptions( url="custom_default", show_above_text="custom_default" ) defaults_custom_defaults = Defaults(**kwargs) expected_return_values = [None, ()] if return_value is None else [return_value] if method.__name__ in ["get_file", "get_small_file", "get_big_file"]: expected_return_values = [File(file_id="result", file_unique_id="result")] request = bot._request[0] if get_updates else bot.request orig_post = request.post try: if raw_bot: combinations = [(None, None)] else: combinations = [ (None, defaults_no_custom_defaults), ("custom_default", defaults_custom_defaults), ] for expected_defaults_value, defaults in combinations: if not raw_bot: bot._defaults = defaults # 1: test that we get the correct default value, if we don't specify anything kwargs = build_kwargs(shortcut_signature, kwargs_need_default) assertion_callback = functools.partial( make_assertion, kwargs_need_default=kwargs_need_default, method_name=method.__name__, return_value=return_value, expected_defaults_value=expected_defaults_value, ) request.post = assertion_callback assert await method(**kwargs) in expected_return_values # 2: test that we get the manually passed non-None value kwargs = build_kwargs( shortcut_signature, kwargs_need_default, manually_passed_value="non-None-value" ) assertion_callback = functools.partial( make_assertion, manually_passed_value="non-None-value", kwargs_need_default=kwargs_need_default, method_name=method.__name__, return_value=return_value, expected_defaults_value=expected_defaults_value, ) request.post = assertion_callback assert await method(**kwargs) in expected_return_values # 3: test that we get the manually passed None value kwargs = build_kwargs( shortcut_signature, kwargs_need_default, manually_passed_value=None ) assertion_callback = functools.partial( make_assertion, manually_passed_value=None, kwargs_need_default=kwargs_need_default, method_name=method.__name__, return_value=return_value, expected_defaults_value=expected_defaults_value, ) request.post = assertion_callback assert await method(**kwargs) in expected_return_values except Exception as exc: raise exc finally: request.post = orig_post if not raw_bot: bot._defaults = None return True python-telegram-bot-21.1.1/tests/auxil/build_messages.py000066400000000000000000000075731460724040100233330ustar00rootroot00000000000000#!/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 re from telegram import Chat, Message, MessageEntity, Update, User from tests.auxil.ci_bots import BOT_INFO_PROVIDER from tests.auxil.pytest_classes import make_bot CMD_PATTERN = re.compile(r"/[\da-z_]{1,32}(?:@\w{1,32})?") DATE = datetime.datetime.now() def make_message(text, **kwargs): """ Testing utility factory to create a fake ``telegram.Message`` with reasonable defaults for mimicking a real message. :param text: (str) message text :return: a (fake) ``telegram.Message`` """ bot = kwargs.pop("bot", None) if bot is None: bot = make_bot(BOT_INFO_PROVIDER.get_info()) message = Message( message_id=1, from_user=kwargs.pop("user", User(id=1, first_name="", is_bot=False)), date=kwargs.pop("date", DATE), chat=kwargs.pop("chat", Chat(id=1, type="")), text=text, **kwargs, ) message.set_bot(bot) return message def make_command_message(text, **kwargs): """ Testing utility factory to create a message containing a single telegram command. Mimics the Telegram API in that it identifies commands within the message and tags the returned ``Message`` object with the appropriate ``MessageEntity`` tag (but it does this only for commands). :param text: (str) message text containing (or not) the command :return: a (fake) ``telegram.Message`` containing only the command """ match = re.search(CMD_PATTERN, text) entities = ( [ MessageEntity( type=MessageEntity.BOT_COMMAND, offset=match.start(0), length=len(match.group(0)) ) ] if match else [] ) return make_message(text, entities=entities, **kwargs) def make_message_update(message, message_factory=make_message, edited=False, **kwargs): """ Testing utility factory to create an update from a message, as either a ``telegram.Message`` or a string. In the latter case ``message_factory`` is used to convert ``message`` to a ``telegram.Message``. :param message: either a ``telegram.Message`` or a string with the message text :param message_factory: function to convert the message text into a ``telegram.Message`` :param edited: whether the message should be stored as ``edited_message`` (vs. ``message``) :return: ``telegram.Update`` with the given message """ if not isinstance(message, Message): message = message_factory(message, **kwargs) update_kwargs = {"message" if not edited else "edited_message": message} return Update(0, **update_kwargs) def make_command_update(message, edited=False, **kwargs): """ Testing utility factory to create an update from a message that potentially contains a command. See ``make_command_message`` for more details. :param message: message potentially containing a command :param edited: whether the message should be stored as ``edited_message`` (vs. ``message``) :return: ``telegram.Update`` with the given message """ return make_message_update(message, make_command_message, edited, **kwargs) python-telegram-bot-21.1.1/tests/auxil/ci_bots.py000066400000000000000000000061761460724040100217650ustar00rootroot00000000000000#!/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/]. """Provide a bot to tests""" import base64 import json import os import random # Provide some public fallbacks so it's easy for contributors to run tests on their local machine # These bots are only able to talk in our test chats, so they are quite useless for other # purposes than testing. FALLBACKS = ( "W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRnBLOHc2emtrVXJENHhTZVl3RjNNTzhlLTRHcm1jeTdjIiwgInBheW1lbnRfc" "HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2 lkIjogIjY3NTY2N" "jIyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTgzOD" "AwNDU3NyIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIi wgIm5hbWUiOiAiUFRCIHRlc3RzIG" "ZhbGxiYWNrIDEiLCAidXNlcm5hbWUiOiAiQHB0Yl9mYWxsYmFja18xX2JvdCJ9LCB7InRva2VuIjogIjU1ODE5NDA2Njp" "BQUZ3RFBJRmx6R1VsQ2FXSHRUT0VYNFJGclg4dTlETXFmbyIsIC JwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY" "4NTA2MzpURVNUOllqRXdPRFF3TVRGbU5EY3kiLCAiY2hhdF9pZCI6ICI2NzU2NjYyMjQiLCAic3VwZXJfZ3JvdXBfaWQi" "OiAiLTEwMDEyMjEyMTY4MzAiLCAiZm9ydW1fZ3 JvdXBfaWQiOiAiLTEwMDE4NTc4NDgzMTQiLCAiY2hhbm5lbF9pZCI6" "ICJAcHl0aG9udGVsZWdyYW1ib3R0ZXN0cyIsICJuYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgInVzZXJuYW1lI" "jogIkBwdGJfZmFsbGJhY2tfMl9ib3QifV0=" ) GITHUB_ACTION = os.getenv("GITHUB_ACTION", None) BOTS = os.getenv("BOTS", None) JOB_INDEX = os.getenv("JOB_INDEX", None) if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None: BOTS = json.loads(base64.b64decode(BOTS).decode("utf-8")) JOB_INDEX = int(JOB_INDEX) FALLBACKS = json.loads(base64.b64decode(FALLBACKS).decode("utf-8")) # type: list[dict[str, str]] class BotInfoProvider: def __init__(self): self._cached = {} @staticmethod def _get_value(key, fallback): # If we're running as a github action then fetch bots from the repo secrets if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None: try: return BOTS[JOB_INDEX][key] except (IndexError, KeyError): pass # Otherwise go with the fallback return fallback def get_info(self): if self._cached: return self._cached self._cached = {k: self._get_value(k, v) for k, v in random.choice(FALLBACKS).items()} return self._cached BOT_INFO_PROVIDER = BotInfoProvider() python-telegram-bot-21.1.1/tests/auxil/constants.py000066400000000000000000000051571460724040100223550ustar00rootroot00000000000000#!/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 KEY IS OBVIOUSLY COMPROMISED # DO NOT USE IN PRODUCTION! PRIVATE_KEY = b"-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEA0AvEbNaOnfIL3GjB8VI4M5IaWe+GcK8eSPHkLkXREIsaddum\r\nwPBm/+w8lFYdnY+O06OEJrsaDtwGdU//8cbGJ/H/9cJH3dh0tNbfszP7nTrQD+88\r\nydlcYHzClaG8G+oTe9uEZSVdDXj5IUqR0y6rDXXb9tC9l+oSz+ShYg6+C4grAb3E\r\nSTv5khZ9Zsi/JEPWStqNdpoNuRh7qEYc3t4B/a5BH7bsQENyJSc8AWrfv+drPAEe\r\njQ8xm1ygzWvJp8yZPwOIYuL+obtANcoVT2G2150Wy6qLC0bD88Bm40GqLbSazueC\r\nRHZRug0B9rMUKvKc4FhG4AlNzBCaKgIcCWEqKwIDAQABAoIBACcIjin9d3Sa3S7V\r\nWM32JyVF3DvTfN3XfU8iUzV7U+ZOswA53eeFM04A/Ly4C4ZsUNfUbg72O8Vd8rg/\r\n8j1ilfsYpHVvphwxaHQlfIMa1bKCPlc/A6C7b2GLBtccKTbzjARJA2YWxIaqk9Nz\r\nMjj1IJK98i80qt29xRnMQ5sqOO3gn2SxTErvNchtBiwOH8NirqERXig8VCY6fr3n\r\nz7ZImPU3G/4qpD0+9ULrt9x/VkjqVvNdK1l7CyAuve3D7ha3jPMfVHFtVH5gqbyp\r\nKotyIHAyD+Ex3FQ1JV+H7DkP0cPctQiss7OiO9Zd9C1G2OrfQz9el7ewAPqOmZtC\r\nKjB3hUECgYEA/4MfKa1cvaCqzd3yUprp1JhvssVkhM1HyucIxB5xmBcVLX2/Kdhn\r\nhiDApZXARK0O9IRpFF6QVeMEX7TzFwB6dfkyIePsGxputA5SPbtBlHOvjZa8omMl\r\nEYfNa8x/mJkvSEpzvkWPascuHJWv1cEypqphu/70DxubWB5UKo/8o6cCgYEA0HFy\r\ncgwPMB//nltHGrmaQZPFT7/Qgl9ErZT3G9S8teWY4o4CXnkdU75tBoKAaJnpSfX3\r\nq8VuRerF45AFhqCKhlG4l51oW7TUH50qE3GM+4ivaH5YZB3biwQ9Wqw+QyNLAh/Q\r\nnS4/Wwb8qC9QuyEgcCju5lsCaPEXZiZqtPVxZd0CgYEAshBG31yZjO0zG1TZUwfy\r\nfN3euc8mRgZpSdXIHiS5NSyg7Zr8ZcUSID8jAkJiQ3n3OiAsuq1MGQ6kNa582kLT\r\nFPQdI9Ea8ahyDbkNR0gAY9xbM2kg/Gnro1PorH9PTKE0ekSodKk1UUyNrg4DBAwn\r\nqE6E3ebHXt/2WmqIbUD653ECgYBQCC8EAQNX3AFegPd1GGxU33Lz4tchJ4kMCNU0\r\nN2NZh9VCr3nTYjdTbxsXU8YP44CCKFG2/zAO4kymyiaFAWEOn5P7irGF/JExrjt4\r\nibGy5lFLEq/HiPtBjhgsl1O0nXlwUFzd7OLghXc+8CPUJaz5w42unqT3PBJa40c3\r\nQcIPdQKBgBnSb7BcDAAQ/Qx9juo/RKpvhyeqlnp0GzPSQjvtWi9dQRIu9Pe7luHc\r\nm1Img1EO1OyE3dis/rLaDsAa2AKu1Yx6h85EmNjavBqP9wqmFa0NIQQH8fvzKY3/\r\nP8IHY6009aoamLqYaexvrkHVq7fFKiI6k8myMJ6qblVNFv14+KXU\r\n-----END RSA PRIVATE KEY-----" # noqa: E501 python-telegram-bot-21.1.1/tests/auxil/envvars.py000066400000000000000000000023141460724040100220150ustar00rootroot00000000000000#!/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 def env_var_2_bool(env_var: object) -> bool: if isinstance(env_var, bool): return env_var if not isinstance(env_var, str): return False return env_var.lower().strip() == "true" GITHUB_ACTION = os.getenv("GITHUB_ACTION", "") TEST_WITH_OPT_DEPS = env_var_2_bool(os.getenv("TEST_WITH_OPT_DEPS", "true")) RUN_TEST_OFFICIAL = env_var_2_bool(os.getenv("TEST_OFFICIAL")) python-telegram-bot-21.1.1/tests/auxil/files.py000066400000000000000000000020251460724040100214320ustar00rootroot00000000000000#!/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 pathlib import Path PROJECT_ROOT_PATH = Path(__file__).parent.parent.parent.resolve() TEST_DATA_PATH = PROJECT_ROOT_PATH / "tests" / "data" def data_file(filename: str) -> Path: return TEST_DATA_PATH / filename python-telegram-bot-21.1.1/tests/auxil/networking.py000066400000000000000000000076441460724040100225330ustar00rootroot00000000000000#!/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 pathlib import Path from typing import Optional import pytest from httpx import AsyncClient, AsyncHTTPTransport, Response from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ODVInput from telegram.error import BadRequest, RetryAfter, TimedOut from telegram.request import HTTPXRequest, RequestData class NonchalantHttpxRequest(HTTPXRequest): """This Request class is used in the tests to suppress errors that we don't care about in the test suite. """ async def _request_wrapper( self, method: str, url: str, request_data: Optional[RequestData] = None, read_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, ) -> bytes: try: return await super()._request_wrapper( method=method, url=url, request_data=request_data, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, ) except RetryAfter as e: pytest.xfail(f"Not waiting for flood control: {e}") except TimedOut as e: pytest.xfail(f"Ignoring TimedOut error: {e}") async def expect_bad_request(func, message, reason): """ Wrapper for testing bot functions expected to result in an :class:`telegram.error.BadRequest`. Makes it XFAIL, if the specified error message is present. Args: func: The awaitable to be executed. message: The expected message of the bad request error. If another message is present, the error will be reraised. reason: Explanation for the XFAIL. Returns: On success, returns the return value of :attr:`func` """ try: return await func() except BadRequest as e: if message in str(e): pytest.xfail(f"{reason}. {e}") else: raise e async def send_webhook_message( ip: str, port: int, payload_str: Optional[str], url_path: str = "", content_len: int = -1, content_type: str = "application/json", get_method: Optional[str] = None, secret_token: Optional[str] = None, unix: Optional[Path] = None, ) -> Response: headers = { "content-type": content_type, } if secret_token: headers["X-Telegram-Bot-Api-Secret-Token"] = secret_token if not payload_str: content_len = None payload = None else: payload = bytes(payload_str, encoding="utf-8") if content_len == -1: content_len = len(payload) if content_len is not None: headers["content-length"] = str(content_len) url = f"http://{ip}:{port}/{url_path}" transport = AsyncHTTPTransport(uds=unix) if unix else None async with AsyncClient(transport=transport) as client: return await client.request( url=url, method=get_method or "POST", data=payload, headers=headers ) python-telegram-bot-21.1.1/tests/auxil/pytest_classes.py000066400000000000000000000074561460724040100234120ustar00rootroot00000000000000#!/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 subclasses of classes from the python-telegram-bot library that modify behavior of the respective parent classes in order to make them easier to use in the pytest framework. A common change is to allow monkeypatching of the class members by not enforcing slots in the subclasses.""" from telegram import Bot, Message, User from telegram.ext import Application, ExtBot from tests.auxil.ci_bots import BOT_INFO_PROVIDER from tests.auxil.constants import PRIVATE_KEY from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.networking import NonchalantHttpxRequest def _get_bot_user(token: str) -> User: """Used to return a mock user in bot.get_me(). This saves API calls on every init.""" bot_info = BOT_INFO_PROVIDER.get_info() # We don't take token from bot_info, because we need to make a bot with a specific ID. So we # generate the correct user_id from the token (token from bot_info is random each test run). # This is important in e.g. bot equality tests. The other parameters like first_name don't # matter as much. In the future we may provide a way to get all the correct info from the token user_id = int(token.split(":")[0]) first_name = bot_info.get( "name", ) username = bot_info.get( "username", ).strip("@") return User( user_id, first_name, is_bot=True, username=username, can_join_groups=True, can_read_all_group_messages=False, supports_inline_queries=True, ) async def _mocked_get_me(bot: Bot): if bot._bot_user is None: bot._bot_user = _get_bot_user(bot.token) return bot._bot_user class PytestExtBot(ExtBot): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Makes it easier to work with the bot in tests self._unfreeze() # Here we override get_me for caching because we don't want to call the API repeatedly in tests async def get_me(self, *args, **kwargs): return await _mocked_get_me(self) class PytestBot(Bot): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Makes it easier to work with the bot in tests self._unfreeze() # Here we override get_me for caching because we don't want to call the API repeatedly in tests async def get_me(self, *args, **kwargs): return await _mocked_get_me(self) class PytestApplication(Application): pass class PytestMessage(Message): pass def make_bot(bot_info=None, **kwargs): """ Tests are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot """ token = kwargs.pop("token", (bot_info or {}).get("token")) private_key = kwargs.pop("private_key", PRIVATE_KEY) kwargs.pop("token", None) return PytestExtBot( token=token, private_key=private_key if TEST_WITH_OPT_DEPS else None, request=NonchalantHttpxRequest(8), get_updates_request=NonchalantHttpxRequest(1), **kwargs, ) python-telegram-bot-21.1.1/tests/auxil/slots.py000066400000000000000000000027061460724040100215020ustar00rootroot00000000000000#!/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 def mro_slots(obj, only_parents: bool = False): """Returns a list of all slots of a class and its parents. Args: obj (:obj:`type`): The class or class-instance to get the slots from. only_parents (:obj:`bool`, optional): If ``True``, only the slots of the parents are returned. Defaults to ``False``. """ cls = obj if inspect.isclass(obj) else obj.__class__ classes = cls.__mro__[1:] if only_parents else cls.__mro__ return [ attr for cls in classes if hasattr(cls, "__slots__") # The Exception class doesn't have slots for attr in cls.__slots__ ] python-telegram-bot-21.1.1/tests/auxil/string_manipulation.py000066400000000000000000000010171460724040100244160ustar00rootroot00000000000000import re def to_camel_case(snake_str): """https://stackoverflow.com/a/19053800""" components = snake_str.split("_") # We capitalize the first letter of each component except the first one # with the 'title' method and join them together. return components[0] + "".join(x.title() for x in components[1:]) def to_snake_case(camel_str): """https://stackoverflow.com/a/1176023""" name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_str) return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() python-telegram-bot-21.1.1/tests/auxil/timezones.py000066400000000000000000000021001460724040100223370ustar00rootroot00000000000000#!/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 class BasicTimezone(datetime.tzinfo): def __init__(self, offset, name): self.offset = offset self.name = name def utcoffset(self, dt): return self.offset def dst(self, dt): return datetime.timedelta(0) python-telegram-bot-21.1.1/tests/conftest.py000066400000000000000000000244101460724040100210350ustar00rootroot00000000000000#!/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 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 ApplicationBuilder, Defaults, Updater from telegram.ext.filters import MessageFilter, UpdateFilter 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 PytestApplication, 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() 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() async def app(bot_info): # We build a new bot each time so that we use `app` in a context manager without problems application = ( ApplicationBuilder().bot(make_bot(bot_info)).application_class(PytestApplication).build() ) yield application if application.running: await application.stop() await application.shutdown() @pytest.fixture() async def updater(bot_info): # We build a new bot each time so that we use `updater` in a context manager without problems up = Updater(bot=make_bot(bot_info), update_queue=asyncio.Queue()) yield up if up.running: await up.stop() await up.shutdown() @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 @pytest.fixture( scope="class", params=[{"class": MessageFilter}, {"class": UpdateFilter}], ids=["MessageFilter", "UpdateFilter"], ) def mock_filter(request): class MockFilter(request.param["class"]): def __init__(self): super().__init__() self.tested = False def filter(self, _): self.tested = True return MockFilter() 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): with tmp_path / uuid4().hex as file: yield file python-telegram-bot-21.1.1/tests/data/000077500000000000000000000000001460724040100175465ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/data/20a5_modified_chat.pickle000066400000000000000000000015671460724040100242560ustar00rootroot00000000000000l}( conversations/a known bot replaced by PTB's PicklePersistenceQ user_datahQ chat_data}Ktelegram.ext._picklepersistence_reconstruct_totelegram._chatChat}(biohQidKtypetelegram.constantsChatTypeprivateR last_namehQsticker_set_namehQslow_mode_delayhQlocationhQ first_namehQ permissionshQ invite_linkhQpinned_messagehQ descriptionhQcan_set_sticker_sethQusernamehQtitlehQphotohQlinked_chat_idhQmessage_auto_delete_timehQhas_protected_contenthQhas_private_forwardshQjoin_to_send_messageshQjoin_by_requesthQ'has_restricted_voice_and_video_messageshQactive_usernameshQemoji_status_custom_emoji_idhQall_members_are_administrators _id_attrsK_bothQ api_kwargs} somethingManually insertedsuRsbot_datahQ callback_datahQu.python-telegram-bot-21.1.1/tests/data/game.gif000066400000000000000000001072671460724040100211630ustar00rootroot00000000000000GIF89ahH4n:viu6pEd<3l9}ꗺ֩]7r>{@}z-pLKFp@~N5pBN5xNZK|8zBL ]RH0qX3p?)dL~0nD3lE&h5rMBx5tHJ}`8uCc2p쀭IAzS@G'gEF}FK;|D=w:t9{B~4l4q@|4rDC|AvOC|6q/k.hD>B~?y8tCDABF1jAGD9uFA~8tBCG9t4nG9t:uDEF8s@~CE7rC@7tJ:v?}9wF9pD@}>~@8u5o=y?s!(,+HJzp.8>^p A% &$JJG֠]w4#n, 90×`˜də* p#@cg3xrԳ)ldQi䢌J$ ـ .,t^)fdɚ㦊p B'vBg3ԊL5# 9{zQp@N1( !6۬ H e%nTp2~*flb묶ޚK/s9)DQG dH&d7&4&6T݂n[誩n.Z d0SÌ9^D[Lp!%qp@K6I@@2x`IqLcsr#'j++ PC/h(ZK DA#AםH 2Ӏ74PG=5U|u%[(=o´Lf439氭,ݨUD(0x1DkሓxY5G5_ٚy0#1Dw?WAb2SnŸuSW8w_a+9͛:7= "2!G;}x'?od߬?r\~!@E8* yŢ `8>"x0p Hs-ÛSRB 2.c8dQ, [y$",M\ (VD\epF p+q`#+X6P8zr*%D;vzc 1*.'"!Wx y_l$j=Pd%-iI`t`2bQ6X0%JQ\,,c^L$ c8CїD0/XeS(ih{"W9lj钥 Eq.R4|$:)et'1}`#Hyӟ(*ǂ/q.J.ե8QIV(1 /,a sRmA((5YkJU<M- 8Qs.+I(J4Aͥխv[aC)7ֱOzQ խqE ;ULZ*ؗfjRh!kN\沜$ Z'Nbխ,\OO@ IyڔJU+wWxXthB~fn( .0kc7Brˑ `UjJֺli Vy=x[Keo2 ͰELu AҫE$3Jt,; JB2j "y[sQiIlX7uC Q4 >/zk<8z2'h W]b*S"6Q"X<i^ 9(G 3; xJRڤsl<9yHQA6 nfo|h)$XA2h3C vf']8%tVvdA]q_ b 0i5E6 gu fgbϘmj:m]m Kp¶7t;.tvӇ=1Sԣ$0m-C{>4 tm4} ~smBTx6[Ͷߊo$/980mQ|5PiLnś8Vϙ֡z^v9ocKӑk" bϭ:r LCꔹ]ŇT^A`w:8n㐋|v{Wa%~aw c=  =8wPgQ9[Su^P8|̘9IIpTv3wL &iן* yqlHQ͈ũ٠& C`ELpF@ itIdS[v~ 1 <6 4AcLiYⰝna'iUqQz V WSuP<- r%phځ%V5up6+w 2{ڧ~,L+a'qHēbÈ߹9IʎINJ-j ʩX꩟(iA43FN~Ő;X6yXf'_Iyʫ|rnJ 'Ԫj7ڇ׊/ڠʭI s1]pyjgډl ҪZ\uꯋB {2) h&q{ pȬx&C!؛ùo *S K"{3Rd)pV _U[44 Ǘ@˯B$Pf\hOƨ^5+|{]ڵ ` bV*)Y#fMնHYɤ7 r9{yxz~ 8kwFPy@Wnl;NX|u{ ۹}4Q$ Hm Ds[|t;n5W0۝F3=7َW뤕{۳ڱڻ˽+ %+XP#@ Gbze難qJ|tu@1ۿZa 0 o®궰KVۤ[ȵzEkMW%` P/{*;{\= 7?Blj ˤ'(׊ګ;ܯ_+ƭ1 Р NB; Z3| jś}>viޠPvY,1<]6aՋ렚̻[ž<ɛ<6pՉ@ LGjJSLa{W]˿œLx{u`7drh\vLvəǽ,~\Ηq;3&3p0B3jh I"O;+5+\*M {΢p g{ `ZwP =<Ͱ hlP:yɻ)ҕIMw&! @FDV ̺D <ǤxJݬ&Q-MՓ"Fxn& PWPg̼oȏёLߌy|m/> &*#ږ0+KœLqMZ|*==գ|q&ӯ8r"뺐k-|H}K$9 @ܢ}ܖA ؙ~ڹJ̪.U Ծ-MޔmpqʴmyHrv,M =L^IhDʸ s˿ϸ{%ݠuڨ-#޲l`Q S^H$m֓.ҸO06Nȝ1>,m썡ꃧ쓎n+Gݠه 4iJ }IN׾>.-{aNPCEM.|g.OP>^/khnM\}>$4| *1XW!ڮ0|w F.JLNચxy6DZ)õ @n0~1!8j `>:h\y|bOZd_{&ԇ LoWH>JN?o}{#o‘q>/CM/;f<nZϱ/[^ k֊ߔ/2,XD"xA,dPb%VEd3VѣD$IIcɒKdf09B̧O`A&#*Q)a`SN]UUVڵ:<%Ye֭ 3ͥ[]y_&\aĉ/f fX2H 6𡠉+BL#j?~$:5)Ul 3&Ml.ˉΟ@#ңJ:Ԭ+$kXdǦMֻwr'_yկg.dɖ-cּ3ϡE.1BrM$d;i\|~#8Z{*鰪:N;nEsEcqFk&x=zq R+4H#-Qr%9(r(u]rK]/;s1Gs4;*0 5ܐ>.DH ;K,YtFB 5PDpQH#gIIRL34SY0SPCU-J5TTQcUV[u&bmlTjOF$>4PARtYfuYhjZlv[n[pX&FrpW;,XE6Yw_~}(wBҽp]N:z˲b3xc۠q8 bWaw?wO=bsyg{QB`[flYYjS d]3*m}mYk{n`5e>{޴N9mW|qc[dK69=Om: _KcqK7:oۭl6==tQ}wKz䢐;r_oâ]f?9tG}z%7j/?^seۡzW_"Z_>̇||۟>z2}o F.|?/`_Tr}}ɡ\7ik˟ ? P2 .BP3a mxCP;|17io{bՎp`4"FQ"uE,fQTHQQTJի+`pC]`GpiLhScJԣ$d! .%RYz@#HHBd%-iI/dR̤Ġ3B(2Rq\V>bT(zF@bjJ 4HG0[5[O41XrU+TZa~sk>ϼBT&Ҭe1pc#;YR0G,hַ&uMlzדBs@FmֈTX:},d͚[pdkQK̸dƵ+r:2׹4etMtv*< o7Δw? Iְ׮& ?GZa3( ^WE'm%`eX5p`(8 >p,R ɍJ1\Ubx>"MЛ>&vFbXʺ/O7{+LM3_Y(PwML)qUoۚezny${s1gشg[]xfl"\'z4ݪ,c8') :@^]KdB:ĮD2X,Kc:QtcP˳)L«f]N~H4XN7kp[ 2qf|cdUǧ -Z32BCH%6p\k%Y,oBڰNgo-'y-plf 沙-12/U[׵}l}}g#3f3bC8)w*^ _+B.#_{%y}ng>B'F~P ,_"کʷ"Ҭ抳٫ "J_~Gɍ}?ΰN7lG*CuKO@' x؛]GL;\.fiGsn (8r8,T@y^-K>~º|xV>W@` JG{K=[=m>d3/39Qdʽ{;z=7sp0ITA(PE`̵4ľAEi=[;ӱI6ws *GU(x0N6XA,7A3,7 =/⠙ï; @'60؄,1DJDaƳ'7T D) ŗkCr<a>˂2@AEpNIEHD`a|(Fa2 +CP,B1{8˛7Ԫ:͛EPh:ȀMh1p 0Z`st0$<`GLݚGGdRek Cʡjt@%HMŇ|ȈH;Eȯ1tǏXLPCI+]DG5$JDJ\ \sCt˱3?TIdiT<V@~ tH6nu$ʢ,úLحaeDL5#RdI H$%XpX9?BM;PJԆZM c,NDp*6(oTO 2H͌čLtzt<ēl}dMg\H0HEX@ PeAG3븯P JPܭ"QGt#lM@XЇ(20Q/*PnJDR"]S0ҍ={%MC<Ƭ$P\59tP/Rȶ=4MSdS"5RҔ8J. ,BM.->8dnR|THPIPJ L$ŕR); \B+LUf%PJC3Z[bJ)DM;U?D35P}.=fMU(PiOsTlMNmnK9O'E N3~ӥon% (HlPa,_) @Q%xIb[01>`2ͨm`vY]UX%@|D%^A5̕bcB0U Y/V0FFn~dLFM5YX.e,,>c0MUC:Ye[LY_5N=݇}Acen 8eT.8hiVUDNxe;S1Tdog/%ɳ]lzhH0ATCl<(Fe#2^fu ;ݤeY.hcF I&qaɡ? (BQhR66T>a+#]~ntșc_`!d@*,D´mMj.L0nV0gH\=>@viꬆǭ.6:id5Xc΀ܵNIpMj#*^ѻվeN. l:>d$dp Q=i*y-` `4jNmG^wlmeaKFܫQ5hd>Mf GUmn]FnFN*ghE|؇ ࿜<,a.aNFnUNd6aeesNXM`]:x-^ h'foid% Xǽ6qNq3Sx mÅ kZ5%+W<f}rf6'KFmfe'j/42|&1R& ?a8ȃ6g0`.Do:?q6uOPL:N߾/xm/_( AuU~u,4ģfG[uq?T(6E`o9gMb_cׂ댷kЂ}YelEqn_XaHgqtCO6uv7wGxyZþZ N6(xLx[T,pnGMx\_xqx&/xOrXBo%(6Y؄M0^.T΂\H0[txw=4S-h!suzV[Y/x?~Bz+Ue^rsO?{O{=[{>˨% ,6Ps/e=_izuob_ro|LJ\zo-XQCa}foGpO}_}6_oL'{ۿ}׾|=lvȎppvWF~g_}N؄[Rf Z 2D!Qh1ƌ1h#Ȑ"G,i$ʔ*Wl%̘2gҬi&N `hfOfB*jײn[bԁ)a0*֬ZrUcd*QڴjCf[-е.޼w7۷MOvZChX "8"m2̚7s3!wjl(ѣE%]ƴÊ%+6X-1ٵA3iLt2gy_%z!|ČGYrg9.o<׳_I4j}VZKD& `!nu5tбÎ?rء5]gPvmǝw'xw")"T|!eS+ц 1If8\0(rs3U!hz!V4"1aba9&ei1VkK #u)Yrv)}9N -p":Py|蘖[JFx^i&z)332榛KDO\u&c`Dp{eU:W(_&?Y|†Z9cZT*Z{myT}.:nV򛞶Z~+-v$)cQZi:F+-y|0 F^ш*V#֪.p`W+֛DpH-UP L5O09뼳N<&#~146+DamZX@.BHJ+r|r82sIso3m}-éq{*C}>k AGG.j5q*&"u s!,^66衋~GwvA 1^F@[Xd $haqZ$[CB-lX~=ڝ<:k@ݬ KH@|QR{& &.h%y#t<1;ѹ]o{| oҽ5}[1A`xa 0#҈Lj~ v3IǫW 8AJYVv@Vo DL( V7 j0(e'ʰWfYba\&tao QaX.5mG&3lf3Q T7؇ `˝wʳPclNu[Ҁ=~Ҵ u\&%*.R\ ڐl@C O 8PMq++O˒4JRKRQ)\)Q`)ɠ UF5jRc @7*>FO2ad$H8VƲE?ΓakLϵɵU]wӡ=R_Xp.rj&5 lfoYξ}?L )ljגl*łF QwPjBߔlKCs>N$nu[`lp-]635/3,>2ZIuo}+bG<JB i;]Z Abδ"BmU{a 3ɘ@ջ޼ֽ#QsY odl lo$0:ә<Q>~Bi0^~sK8cҶPjy\2 Wc^jxΑn~s.tΙ%%ծFgW*ʉVtQ]W oFDL/ɜi#|]:Y\szVms#8mdOi "Ny$x@UB=&Fh:1Y7hhSZj&$Vs2$ǝr+Hղ !4Β0͜>0u{ ~7[ < xCҵB+Osе1uhgIH@@jK^ 8(fQPvdV'uv ܂f/P)k;GO:±!X!ԡk{w^C}tEŎg"˕wpe]H;pe`G.:3>^H<[s?8~9|z>=BnO ou] $gRh~Zy*!#OKycOφ{ا?%7nWѩ]+4A0@<ɼl^-t\ߞi!ۉZ5Z]8Bٞ &@4BAY`ّv1B HJ _ڨ ~ ^=$Q   @8YE! C@] r !#" [:‹ہJA, "h]B-؂!ia(b5".&AbIb|:B lA &H44 +bg9b Y:H"b)\.#4$Jݱa=.$d3:c4Bc H@) 6Ym#7v7c:^$۰#{M ٣:x@ Au?Fc(΂?%Bz7ˍh ^["R-bQ _Q<#=dI[@'GdK BMށN*Z, P%-^E"_XQ$R!Gm K+ P1l4x3((<l쟺8jy魒-ݖngJj>%cƫj yAɢE᢬-,:2(1 B+PrjZ%P!rnnК.^FjN*xVTz>"؁ďᮁ Xvo:#Gm„ %BlZofRꦩ%~lh )1P/aB'xD p z.0/ I0ӺiʮzVr-t7/0aX3@'PE<Ҫ  K0"faNϮȁ2j(k:oF- Dt16N/ $^&TqUqA!(k of@ -?.*C G1ª%V##?r O/%q/m{ĩŧ2r*70 2aP5DA `%#0//sIsn2v2n7 @p3d47C6526A-MBzR/g:3G>3?hN02s#}j'@5aXC'lLB<Ef99n:Crw tH0X$1J[0 pĘKG2:*A[MB 7DQtE;hjFRsR6uB1WpU#5A`qXCX? B@:Ā$LpE'5]r^5_<3s`dAdvcc3dP-t'%@'"E0]ӵh$6#~3z;ҽ<_t8yx0G+GWz۹k9M=B{A~ O<~ =~7nK~3{k?#p{?|4$-3Δ5bDkUhVFk!JHH4Y +Y:fL$Hմ9̇;yCӅ?K6ujTSVzkV[vlXcZ-cIk&In\p{p4p!Æ$Jx2G= is([͛6u5#i;Uvlٳi׶}wnO `[q{or 7'l`…6~2F)攝U)ztDS7}n׷Q|.|:kS:N2̲#)9#%^i4JsϧP1 eaC s+@$r n1 !ː 5P Tb(b]x!Y1[QE5cd?<|]-sQ<~ OEa s^`d1!# @~)^7)y3 AvizkBXCp?|^ėI(2JB nEqPD'c{`#$QFN8tD$$?8XFFe%|P ,Lb/R3=4fHAm񅤹#N>>X6V1Z ER(A6I"=P2>az!Oߑd)SUցL+cdX6j q,]>eHV"¤!gdbM]IN@1F$4K01eԇ. 9PV;4TJ١ s18A ^!h EUi* N !-KcLizԄ'PBJ<#f-$ZғLa[ h jZUFʩGZF+rQ3ta\I @! Nh(qU5,Ӭ5m^$#`ES x8 P,b 9,WxQK*̫_d9YU_z]Tp zD2EP6tujfTTэjU%m"^@FÝ,ԉ\d2H|DmĠ ~#T*y[ Vn ; @m}{\jpln@,T6Gn)xU#&v,X5P6 }?AP zlMaVȁAd+_ 5:*^8@l9@*p)ظr1pYf;#u(!T Lp>-q;:Yp:;؁ p3;B$ aQDG U@iTWKB1Px'Ƞ5x'D8#tN4`D\@]?dA; "6rB;Yh]nsVnw]o{o ^p Wp?8V!,; H*\ȰC"q(FC3BIR䏓(S@2eˇ0cʜIběsr8O*U"arXͣHμQN  *KXjH'tG[ӪEï"۲ڻxm ʹtKp̽ [`Ð#TZrM$ąwBr"LnJP%bBaH*R;7jn >4x0YmKlscP0bʐ.I,].eԨw -'6v%@ U[Ȧqy1BܾKPh5*4` i GX&xc$(u&*p 1D9JI 5JMҕ!p7B1x8$Ɂ"@+LAu(!TР2-a F\˜-ߘ'|ʁLl-TЉHBg<97N`/T4vPEP[4*`/!R ݌BHv8! /AC@)h +]hJTc!0Dd(ȡ @C͡XjUJz\W!,5 H*\ȰC"F @ⳋ :ȱ Cdɓ&\ɲK+ZhУ͎#G")Ҥ@ mS&Ƌ5o)rgO;JJuDjTiH=}ZYQtM4ir>ʂF(Yz0*_˦=ݹۙpeŋ< k7v'. h$]|w}y~sőFmUVcBQ}]w-π>^KYW_v5 x !ڀ%Hd!k/4 xߑ7 $Ah@bqbFrd9VXYh쩙1m"`)q}_ Ĥs%l 4:cii"ݍ8ϤVziiɧ)+ t$+P8[饬:hQI'4г`Z[@hkNʚ *b߅z i.C{6 qZP-s"\euB] =0c:dƷ!@Y<򜉘!H(q3:Ld3A4M'ϛfCd! qюL7MNxbvX; #FN@_{eK@fKN pr+TJ7x v /oN TȜ аSW7O| *ٙI.a-ǒ@wpE~g={PE,}'3@ZFktW vY b"@ P{18A p|˗0cH"ƌ;ɒ)IQ4|0Ν!}D)pe>kWҤKX*[֚۷*;PÍ?$h2(W5JlزdFK댭m٥{4E0XfѪe 9dF/7k$T_nizlAՍ | 8A i[3{1 0ݠro t|K~/NOxv>0<1ͻDOx{eu]f1wGpag B l d@}ǠRht!^a T3#WbAQj8Rs@OXό(ᝊm_iw#B#Y"(z4 aITvj{9 !G gJ~IP(\2V#hLdcY˜^E@nRein6lz?TǠ#aEJk^2dʫAJh(zyEg #%-͂kP1gzH#`,zx?6 ofj/];(3lg}JO.G5Ht3U즄p;!b!\ d1CܲLP'xB; ?Ö|#Eɚg.C'b<Y-l;GFXiX*AvC?dO#1H&!ܑۓ(-L0b0?8C7Ʈg|+$1= C t <;mD/@&E\+35(׹f<1|bG "TJ!F DG`?p@I/}; 1Pp2uN `p! Cq  9x8z~G"@+pC 'J8DEC!Z8 Ļ 8-q!3 !0ďYp:;؁ 6*D $Q!H!q-I[ b; юp#x 蠉*5OB1Px0!DHڑ 3t%,g D[a8v%%(0px~( ''*y"0CQDtZb3aEQv"NH" * y)<Hi.vRCb,LJԙEMjT(uM5^@!,!-@PÀǰaCiҎIx,EgihGQIɓ(S\yR`A0D0:̣ۨO[!Y Jц. L8͛3n3ѫXEHDQwjdQYҦ}JpݻHݫG;rkC:7+;Rj۲M)W.ݻxo ;5|8'c7~|vdʕbƻyogς>0,je ;Y*璬C۷O0Zwib/VR[qZ`\$s lwp3i꽽v܍}uI噷sxC?Ij`5ؐ]A(a^aXqЁ &("C$PN%H(%__(Wc8B7e?gP8߇Q>~VވzZn|N!aj5e6a5YilneX1Ҝyby݀.2HzՔ58!k)i-w$DbXp#**-hjGމhM%dꨴ7U֩Vi+2>ڐ_ڙQN{2j gUJ7Nd3=z馵P!T@9ge1)7I.t #Ӿ*gM UtjzGf'0c+a3R1Ψ~#4‹e.H7!ܱ4C4> Th-$,a3dG s?r 6|52Dmz8M#m/>?a 2(b̟!ıӕ^Xp0:*{yCs>x"f}Kr#|X&][tvCw>';HַCdPsT S0rk-b"~kѴ(w0įu(!TbFk-a F\"~ לax-N0 NA'`;!z`[1D!p4^̄ë0p3axK8P t0@[A{m ``*p$}C6s(|ȃI:JN#p G!:RЀQ(H+!$rH `sHI)aHp.9Th,!xJ"b`Z0q8 aZ,IЅdHhơ( 6i=mp7Iz 'pvvҝB q1 q=y R bpG!Њ:䑹08nςTlCO[ h ,R8(0h$C3lPO4̅RuiWAE !,5@PÀ*ǰ4i"J<2fƱ( CIɓ(S\IR`A0|qbĊ.:ӨO[ Y Jц. ƌ9͛1j3ѫXuHOIGhӪp(c!qڻx"tpbܜs7v$"*رrIA[aĘ37/CR1^)PUҍzVOh#} ݊UEcqV.mCz,Xuٖ:^g U\2-Qlz*!aK 2w\?b?;(-g1CRp8:kVݤkh=l誜6dG 3P$|ܺnˤ†fwګVsXg*s* {41|bBu )W /| Uݤ;yÝ#|XjtqA(L/Æ<蠂3.}-18 @+U4|+ur ]r-\aF7h }s^Nv`C-z4u! v :IB_?kTw1 #\ *TXC0h @l# ɅpX3WE# a(v8! /CxDJAFDG)7,CAϣ*D )ktBCD tBPv!y B}n!$̤"DA GfgIA(Ѥ*ɐ8>%$aYX*3J\`#Y C +l&{\2?1$3Y @"Ω< Ӱɜ>:_QX䁜 r#:N<8O"b0xYOq8 rO 'Jъ3q(D+$Hā BਢB @@ 1Cn1Rp2BCêb.0[1);ozN1 rT|E. `.#O Sgt glG<d. HCb,J@!,;@PÀ*D4i"J<2fƱ( CIɓ(S\r c.tpĊ.:ӨO[ [ Jѐ/ \!D-bgУXj5pӈ#ذֳhӮ*+ؐ8ujg xb=H9/#[׮ÈBr,IA[˘wjdLji9Kyhҧc깠C {5y"+pE.wyk ʱW1ܔ__}s>?Ryɧn JqX]$r3%!H4̈>p;{ ƟQ8R-wK::!40%C'i\Px%x& ?#)HYؐ]҆:$FR"m ·_kW_H4X-$KNNSunqy_dSa#*TtSsha٧Hw)JRIh@RA'H䲠Ij# u)QdC{Y?Pű$%+Ҧk^ dQu+z?l"e,ln#1tŽ,R6$nHjB:t3R~dG ?[mb(4-E)B.aGEЅqӥ@u @M4)̅TShWE !,;ATG5(\#JQc3sȱǏ CIɓ(aB05Gz!iC#{d8G$!]՗oE݄}d)!QPVH| mt θ}a&y?lxrm%z2viҙ9Je16HI?I.:-GKht4R61TQ?n C%GR坸"7#HaK%G/v QEH_6p}N8 :t[9 QvJO0mF|4xJ됺\KJjC۰XsXAÍRjw-bB\򺚶XP08j^jqEx-10{ceA U\vq4G}[w]s ΘB25-18 @+|ԗJ2΀Ч/-%FޔwaY8C'|a8eҗ>PA'b B]62p#x x*(@b1Px'qB<uo4C;F!HCt"\xJ|#XብL>!Ca8N4! AbP*DA r8ȁN)lSoJ@G -%2*#H d%-x[$fQ RJ|q¼U"-qb 1O s4D`(c%'A>g} bp`3h7O@'2ā Bf7y}@8b8'c. w'?O E=JЃz$ =8o1.&D'Jш(0l"C3D hJW hx<~0̢&X: z\WJ!,5A8ЃA*\#J(ⱋE谣Ǐ CIɓ Gh;xgz;t# {1YngH\IG`"h{ZCFYr~~0I"Dp%Ș|*עH#HuؐEbM `qƭ8HFԝy4 NB M)risCYxcbY|@VY_Cڨ&!9cde'r璅Z'5%HGdOQ1@G49W yQқM**ۥh1R4:$Г9h|~Zk::O#QB7lVJ:|C{Op? cgg"o ON Fj- /l@UJ7Yf(բp!kIrKU1I0{CsG6wȑb#{. բM U5&YZk3#lģg(O/v Q\=٬!qe1+r9C7^iڵ_{dvʚT>*]X S K>:JK˦\sX Cܙ1|bŸ/֋ A>+h10ǣ!11ĄjO-ba }zHwQ07qG2GpE  VN8M%?HZQRB9xj8-qnØ?Qeb`?D'|P V`lN@BLt]`nqWLq$G U@TAf F8A[cC,PI=sXt;yh:8*WyN4`DyJH#mC*1& ȃ'{Bp29Th-IR0N09qi? 1Rs hq3 b3h; O@ ā B0@8b8ܢ'. w ](CP EA%Jыz$=*l[Z!,!.թ@[Ȑဇ#>FŋǢEsQYÏ CIɓ(I4cRFSɳφ+ T(2ę5/ĩϧPD"ŏ5n丰S`GFᠬY%]$ǎx2JSRi͠ is)=E6^e{zɕi骬h2$.F:'~j(鑜*"XQ1@Ciyꮼ2߳H(T^P屔 H:NH}.i*2!Eóyg޴ 1ۢ$o6$g T{s;i2j|G|vW*ݸ ?s?9sC@+)wY ^GL'kPI.S3BB{)Ku#NTdt3HhCm/b7uKr#ĊtqHaK'plY/v `ѿ Q-Rs81Lyq5:tsqfGv^1RzF. HYv` m ,88@|viP0D$4ؐ9@*p)͢?[h2F 'ġHZQRB`bF9Z!lK#}$H$HƐd 8`6Ԃñ%;B$ !O! N !-&B1h+[9#\ *T4.{i`@l# S$C;raqtf4?2j rl&>F!6Ct"#@#P&y?$\ ]G!JӃ!H꓀!,!- H*\ȰCQt8H"3jDdǏ|AI' <\ɲ˗%VxQƎ =̈)$2*p߮`Î5[mZm}[ \9z;2C~u0h&Vfm1\ <`e-\MBի?.RJnxK7 ym5}^lxbJ/ɋ1Lp +i \l@\0N V&p ##6ќvJߓt䂳թJw S*݀ͤcqf t? d1 (ٰ4AI.Oq܊]uB,y\8BAw 61Ta-Xg}8s.m.s~Ϻ6!l K 4d@bdzn@ksHw]o_ ъ :FYMOijaBo(18A  >sWE ^:w?%`0 q1 qEKna҉A (D;Lx‚(`I3>ayhG.0[?bAhB.a&6 PPAA1];?(Wf8JQ qxF'P$C>X@l# e68" H ^Cj<́:P3L1Ҍ@Z9[h,BnzӀ+iEJq(#L'i,j`%ɐoڳ0ɂpЀ >lE:Nv" v :IBt=#\ *Td@Aj @l# .Fad;F!vCt"#:)P!CarCځ< Ջ!,4 H*\ȰCQt8HD?2j܈ɰ 𱣤ɓ%OxȲ˗0!JhƛCgJF1 L1sO'J5" ϒ ZKe8Y39+Ie%Vl 1_u Rɿ Lmہ_K@nyϠMZcSڙkea[T^3ϢI6-jԬv[?ki,30ᦍ_ulХOkf.s⏷^}tFc&o- @%?Jw[WSwWy"ad@EC .y9!lAg≂ "nEUo," DbAXP`U$Jf$A &$9YZ\WBZY8s9BץؔcVi}hf&p*{9ٙGk7^FC] b59MAbXr*?LoII6zПdz\(fNB'jɦ;Pq`☹ua!룀Z+P"f#uKrЪy.֤K6 >\TC̲lip\0N uS ##$"({1 [-'R2B'Zc~Jq4 /VD5(I\mAm?}1-uB,y,5+S ]unbd s8s =7^0q!$cq YĬXJtc@@C\8:;ppxT|k F qqK+bSH;SOtCKYs7: O (^!6<#f-> Zwqp!0A, $WD5xD.yxE. `.r g QpB;@ \p Mq^㈧Ta x8 H 9MKvѨ{l@'A@HBn!YC,!7G8AO+`H`Wdr!$aK>GK$L2UҒJx%` 0PcŸ^] )!(42 ȸg>3=5OGjjI:-R\!pqh<Ё aT4]%@#$xP{dh8Wm>0eAlfzddRY%ɧ{GÕ(d"cN9ЙhAQ\"fqcN^uKrDiU9  >`6:V0YP8+C^9*}z81TeR5ӣv"mr z)-'Cu'>KЮ4jUIuǼ ˬyzϥGJwiǒ )pE>jM4 obKUÜ06ƈ+BLPaZV'А2')60i}tqB ZEtBk i# WLT '^dK͠N QH 0!P1w@EYJ 1^(DP& 5'jFXV(Ji!pHmNf<Ё a q*cWԣ9 tv1pZʴ8 {3?mq[0LNv`C-ڞFl-TЉHBhkp '0Pi ;&M-Zp)S8N#p GX!:RЀ32@9ThY=;python-telegram-bot-21.1.1/tests/data/game.png000066400000000000000000001200411460724040100211630ustar00rootroot00000000000000PNG  IHDRhHP pHYs.#.#x?v OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-:4iTXtXML:com.adobe.xmp Adobe Photoshop CC 2015 (Windows) 2016-10-07T22:57:57+02:00 2016-10-07T22:57:57+02:00 2016-10-07T22:57:57+02:00 xmp.iid:c8521908-e271-0442-af86-9a3dec5fc9c0 adobe:docid:photoshop:a11c925b-8cd0-11e6-bfc7-cb6206bc6d0a xmp.did:8d2ae8bc-ae57-9344-80c7-b693ea8a20dc created xmp.iid:8d2ae8bc-ae57-9344-80c7-b693ea8a20dc 2016-10-07T22:57:57+02:00 Adobe Photoshop CC 2015 (Windows) saved xmp.iid:c8521908-e271-0442-af86-9a3dec5fc9c0 2016-10-07T22:57:57+02:00 Adobe Photoshop CC 2015 (Windows) / image/png 3 sRGB IEC61966-2.1 1 1181102/10000 1181102/10000 3 1 640 360 h cHRMz%u0`:o_F[ IDATxwUy*vη|'H d Ƙ8=Ú]ubwص -H4((Ms:wWUWuC_[]W]O"q0 ` @0 ` @^bY)v[m8UireZհ]G)"bHq{#64iRREJҦf2mhRhRhRH)x/x@p\UUr[j-퍺Q7vnJݩXNr+ v]ŬK̬Y1R|ٴݿB$HHAB? !9SK>1`HZҦ6$ @fQ kFbUJc(TBڨ٥4nf7l%% b /Dtrw.QվDԖwo\[qbVJYSOCs8k͑93Gr9O5 1IsQU{T_,Bmب9]iSnص#ХԤ4 kBR "(q>BGbGԝ8l.3Rz.mRZ>eF>cSӃɡ`zf8;115i(. 5])7V6V*7׫7jkm+er\WvO4!4)|w}[ [\wŬ\ŮˎRbM SFtҵɡhvv$7;͍StG @knnUnVonVUK &֤BhwWU4{m׀!`nv\MKLU츮ɩXnn47;7Ģ20k bҥbVieK ʘZJL]օ6ۄt8<3n+ &vTvk|zl o$slz񙡑kHYղX_(ux~Tuۭ[.l_oұ驪L̾Da  |H&r\e;nv〈gL=eh'HA) W+7./WWtiRB I?=MQI8}w(6}=_7*e9J1=47zb'N@U:?_⅕nJ ]lJO" emݢ6zpb3ϭ0w)xv7㪩̑w{ϩS)p=娋K]^Bp]f ysmRԲgRMG!K3!RwLd>ɽѹM4u@JVp~7&u)5Iʕ;Rb<{>8g W̮RR>~xg}C]3/}o_/\^.K)3u¾eY)ؖ߀g ~ǑJ7U\Xf91g:||f|ǿq덛R͑i]w'uyn}åBxf3H.?gOL# `+W>+W8k蚼c׮qR ^Q򛎺[ ά+ G}w @RO_򹥆SRn:[wpok'RGwў,\K]/8>4oߚ/r)=mHA5nž]/uM5N?|@$7޸Uȧ.<}#&{URp&lT=8SݏA++ת;5xu i]f) Z+7)Gs'>pHDrٳ)]˦{(MG}T .(jy'0ZO}fq8gjR.ȲRbS~;̴ZOd>  @޸Yzjm| y/u \ *T&g `B?7|L8f ڗvWľP*lWKBTo]-[cy3@yzQ`$`J׾A"z@LvxfL G{+bB},oeiuJFcB  >oNv G/~Ņݲ/hˁa~dE)]3u|>_-.X$%94oL&y9C# n3aB_8_G @˗V7Gr&DľNޭ0uMwK[gY}fzfZq|;2o.{qX[w41W6?W+u4R_D&o0! KO0s3kq-H:^ L;#9KKٳ-/]PΥt.Nm+nA}?B\ϟ%| ./U>BJHʎVϭM˲FT=G^].퉁ԟ~SGƞ<<2ffHcDtqa˫yŅlJKZ":ynwIȇ|c21߼s9A>KWjə!4l̑ RDK?=>,8>=Gg}@0m{27^vÈ-2aisR\ި}0Arkvebj2wl2W3ǾXg8O3=>>{o- !&ؗBnWi8ʅr},'TA^j&PPfc?ұ6l%D8/E8ǤYSPxJ$]'6ܺRvW>1Y֍>ݱ筙‡&i"h8>}]^蚌ޡir|t :XZ*"&z)xtþ/Z)²k| QW+wҵ]k.}Oϕv';P~(dc{ϟ}FQyPƊ[/RL)]>{|˭ف(N&*_zW 5\N$aea`xD_ũ}'sZ=VE]ʲ^gISJ}$~$0԰]%œR=.h˯B]F(0y-p)x; >7ȫx)x4IDBv\|$ Jd8]ӳ]O YƙP)iޚطK 73RD` *&ANTU6ΦtM1R_x!xZ4CI6_bAFI1*&*k{xr7C)cb0i10dJؗ|O>Rw]fcAM=y!)CRI{P|6؛tAo9_N)8b_\4ԧu͛.'Ka_oӴ@Kkb[ 7dDPOLyɈ&\)8A eje ])#{6eh|t_Y_Rɥ\Yb&R楙p)ꠅ1=&0@ 3x R3u)/|ꭏ~Z+,͵rc}`MԾ!n/u1*w0x ̡/[ŜKi5#/\x^1eLξ{j{Fmdߧ6I4 `7aL15S?OMZοo\_/^zbq^ D]%yv mQ'{ތI"҉/͙P#g Gjvjjb}X3u95@{[' DS ?e߽V '}Gr1z>YRv\v]֤R0 MdJd7؋O(P`طKqKÙeq{: /ӄoWjO̟}lH=vAD"qgnthڷ$& .PDfny5MG&A)GHA{~:ō})0إ žI9=◓.0Gofa/Y g0!4Es[JF>5YĊ81$U1g`/֊6p>Vn5΄6R0&'m g'΋nNeY}E=}v|* Rp8`!2~Ï@c[4`5< C+ Sׂ~}}_lKA_! ^MMJA)x(KDa\:F)Kh "8iy1J_򠖂B 8MGn覣v+  u/J0:h!^:p#n)8* sgn즣Ļý¾{jQz@  g`!`F&p#ȫ[ "`@\ɲFĔC( N/=2\8tA@ Pǜ蘣x#~EOi> ώ'})ab@ WBa"Ă{-`kџbPB@~D_G<( mЯ/C'޼11-1NnZݙju*~[kϲ]#pjخXc3̪98`de~}bZh,5ˤzqF]ӎpĶ̭ 6 S I"#8<޻ $#o0z.yƊQ8Ø&,Y2%[Y&  -슖%9})ND,}lUs\?,18KKw':rLԾ(O.M K K]聣&@Q O 9HAݥ(b^uB)x@ odcC{]<"Ed0}co3MGa_)8=)hSboީ(o7"`Ч1p3C0M fQ 1M<DTL4U$ٓRp߄A)fVDDR !&5 T  P x5 שx}}D.Jݶ7e&Zu ]ӆ}Q􅅓 q8)Ts5΀!`!wSjvԡ3scu\;k|&}<7VL@-!Rp5Mr;[̾ K$$7?աlzX htӮmKְ떓6sgf_zc9%*wzrf#}[WsMk)6r(0y?7Ft}#} ui䄼 brͪ5S'f^83Cs#$XwwzIkxR:1a_#ck'g`$MGA3ϔ_ߋ_;jXɹ>==rhL'd;XS/>}3(e)80Gz't{47F[cN&kW7lZM]ͧ_|x}xp ff>eW)]! 0xCvB$%7L>jڡRj.U#Ǧ^8Gώ durJ 2 m*?16^i:7vW7V?4/U̎˖Z{tzO|DZgO˛qP"ʔKt\;ɲvdo }wc) ]URlCءOQ+D7_1ogK!f> mO Fkw&{[C߫1԰z:e}=z`J2ц.@Jq mdM->$'ujr]Uw?~O4K;H핒n+x)x@lSj(뾁-DZY}LTfQ>my߻>{j}CR"Dk5`Khk_}CD0&$iB AÅJM䑩ǏM]O:QfU=2_hXn>eFCc}YM}oQvW&qj̚CϜ}CpP*TsKRa-5)NC+$b6M#~ϲo.A+5V*6Oܳ'>0Ѩk4iPX)RF{d4MGA1':c_׫rsx'M v[t7qm\2.,8*i;8unyQvWQ;fu㪚hRd%j8TR/* {0=@ܸy zNn E&Vvr269{ϟ=57:M Ð{u6d7 @X/0yz=l2C> |.ifUH/>2}ӣٔ [O )I7KŴ'h_ND:X;8y"MG;U)v[.?ϝ}ȸFTwɲUM.Hbr16F$ E;tJ6h!Ĉ_uD ǭmFs~ѹG /]BDW bXi@[;E&XaBg!nJc cϽd@^d{hH!4M r\n44!j]n2@nIA_G 7vWA_nh(&umW;̩}ϞNkw&&ɩWJQհM\:ej+u˽pk]doKٌv i ` )]j1 ].-b]8:ulz@mPz^+U]W2Fh}vkIw+8, `!1gC/)eGarԵ#CzJ5M2F}PYBY rZty~f9ɖIH8;yW)QULg=yl3ivx#ɀ["T)Ն^)V,!H$9̙.t]V'X үp/ronA庭si|dcG'OnG7(MJ!]Gӳy)o; p{p6M!߈%@O%p6DK+9*TR#SC:2އ?zhb$KeG'ҤBrZYBf̮ⱡX^ i_CUga"#w M Dui;F+JfQGs\庪ڰi?sj{N;0mN,we2IٰݕbuTgbn}bfhV)rc7b5k^vWp .!@9bsGrw/b4lV>;Cϟwrvtw] hm^YL:7(%I͍dˉ4QDWKj;37^_R `O126Űo Xk4lwnlуǎN zw' !tYlU-Vl7:5)fFcUL0hbqf d .D7Me_@QdYkw3츪nBcG&>1هQá*m׬iR)^^.kj_"r24mn"?MJ%5?4 [][*C]ъ@_Tmpo^qr* е'gsfcY%*b2`jf-UK-ȧ M{)i~veNixʳ]l_?7wnT[=5}'d@AirZZ;9ʦlڰ݄giD+Rs a_v+80 4#o a_un;㞜y̾79M%jCMҚxvqT35 v+80r\'nU츪f9iS{G==s|24}kMJxP].VGi뀺ʥ)Cj׻su^M2_ߋ  ۯʅou{GTmXfSHS3ϝy؀)mB]Ԥ !UkP-,)ko,v9΋ ^"j=_],*|7&Nuh_ u#PM5nFs^zd)CP]ݟ +4]ZZ-,nZHώ5M:Jƾ~meåkŖ$Zg4&}v?Gy`4KDY,B߅fg+U뵕bqU@*Ð);}.S |'Qo|ς6ٴG#ϛJjmnTB\ȥM't#o"|/mW 7r]Wp `OS~w*vUn=qlǿ̳']ͥ 6_qRM)S`&fXyKtAW 5KJum_@ߢTnZjX?Ȁe[M~5)im\ꎮm΍̧fEo!2 VkZG }'K'?w]9wQ&j gyZ4P(5<j#Bh7?oUPN  r}}6or~hFTbOE3&rf;PIfř }Es6 MZ.__.Y)þ  T j3'g&F|-+5kPݨZR(j3Hvj4LJ)+ߐ:%BiX3u]0;œ|NͱbD7B.媕Bem#PK%pF6gPXZ վܝ CzIҊnC)R )eҸPX*Tɰu !f'&G.oit%hSo?mMABl-7?=_](m M WtWp@c_nyJm]LDqi ڍPJ˧5+=L2[{stmT;vW8050:v7*zʛŶ>^ХKtu e+[݆[}+}ŷJi96ڏ''&Ulݲ*iJbn9R ]F*ejs麪.-$[<.hy~u1{љm ;I AԵ?N ?sbC풫U̽} {qQiVI'3VG&P`KLJ@ 7`_p7+ .eL=c[߼<190>pxj# ԰B!&KZfJWۍs䀩kȾV7iolco,'q dߖ dٰ́7ym}ct ujnӧgG-͠I!H+BZL*5O7JAT-Pnf/Zmcf$/䛀g-? -k̤7jo7M 90=|`l &f:4)\LYDⱡ̾Ѽ亪(4?kR4\u暮eq] pHu )DJj)]moׯ gN >03#1`"XXu[&\RmP[n:{xz$75e&Wq1umXkk HZDa!A?ǹOcnqjg"Cbިگ^\֕O~pɣSO>49ph<'HRMT*m=ULD3,3+o"-$__][*֦اt]q2p@K LJϤ4ƅۅ_}SC<=7ؑy&j(r\VC "MR kRq\M1A'iwf͵Mkw+x/t@Ĵ/ﶸI !4i24"_ڍiC;89phj#ORNd1,vbR붔"ΊеىP.}Q 5v*\6\C#~9Wxvo҆XٸP'fG<>y7%-5ٰr^"2ڌ*L M}#nnM &Xt՝o*3ϡc(g<m_;z/ݹkRdL"rWkpV:0gDΰZۨ/W떣Ik  US?05I-'jOۮKR^*VM]h|UMG@l?G+}Oۭl4)4)t]6]K }k4>89቗^+S3mn1O-;B^k B%kߢGL0_䛠}޷;Κ`,={c~qori= j$fߤKq˲RN g9<4 ]ӐPqsq_jhl4\j$ZTHW􁩼{:(0` |iu&B(bxw0xPjYWpK^yǝyKSV[ $(ĀOo`QӎŪuiIm˞9<(*7 PJy@g㧔_/jfnJ13͌GrEET*v*{\iKڥS;o64 b_d؉H~O$//+=Q,OdY)[;- .VmmnDtkTngu>vW&,}޾m2[ޙLIQˋEUЎR'Cef!6e}CؖW>g[J7Wt)Z-ȑ0 C4?ot4R5 ;nvk&Gi*%O oRvB|\֍-Sc & o_@moDwDݻ˥Ս\8<54K.3UޖAzGUm3EOh j9s$c]70_o3E-{fFRj9kU*OwV &\\ۨ =2yRWp@ h:pRm 5) ƭBY4`Ou*;Do6luY `Nh{P3mMG-/DR+ THH\taKKҦB؝ami$soX6lM oݮ@&󼥋O p˾m9mh; ٴ䬮 C _8kTO9A(BG6? RQuqhn/ivW@TߗoEg$MJݾPt]埈DLI 6rK5!RdViX5 E񔤠bmPٖ$1GJuQ;ȚR!H0KwR)]Wzirܴy=ncmXMR2h7{p0SDU_f=~7{'_6KB P Y{&5iڕ^(R[h Vm+]ZX_\6oͲG>lcͮ-{p,2,W]Z(X뛅iA2Ս.̿} ؜ nh* 'DsRQ|)XP6hNI `Џv2d|7qgq ];{ݿ% R.7{>o}\߼ 1dlpsf`@/n ٴ%]#վ^ef2{ZD3> )jU_ƪK 7s,v}i_ƻᆿ}24mKٔ=4BP+ ߼ʊzq[%߉zo[zaB$@GDtAdUۥ_t)jsy`9J)f&E*TOsUk;wPSuNI ]D7V7+\muQvWa AG26'no ԙ(2.ͯ/+iCohJ.դHڭ_]hD)[Ƨ <{}n[[zaA  g 7|(qA)l,6\td3Ǿs+ 55Ɛ;DnV {P9~ giwE+ |C'\+؍t7uM zW? ?O?DNnu s{8ClV$]@ /?ͷ<|Jjme9۴KA;մOex_ןWD&N7myRV63e)DÍv .p?KqgbPYX f&Sה|+oȦ C49-UW~kՆcHTO{!HCe7-& )–_o: J)j +HHro^Ϥ `⁌27Vu) ffM3 f\/,*)M}7_woľoػoyrnf#7Yֻ'a΅kJ~k53)u_ճ7)]bC~_W36Sf;nY-v^uw+8 `=m[sC&*WJisgP*3)n9 ML3>)U/o򅍚I!i{3ɤuqB+ @<◺< niw}JbέlZzT22,/{lФq3elJ_x/wGV;@,̯gv4 x 㶻7 0+yNFZ u//51LKuϾ~i!6s,+3ʽym%"M!6nnX.Mmnio U\ DU]yg}Gl\ʸx{}PKJs;1s6oԬ[WrSӄOwyl0Ӱ_{˅J]z ۹׷j{9 ]A<{&ehW7nnh[[b)Ϝk6m>w+K)`W Ls(oя )6܍S׸{n  Ծ2e3yZe̍J!V DBɡ[?>~۴ +6q\u/^+807ܥN@޿ M{jaoj̹Q^[D)gci}ӿr&UozY p%ÁEi\0˪ITX(3psisT͵l[2< 33p.WvJrӗ/02{FoUL3{DW3ϴ-*ז6/*B򁌹Q?~B!6wdc3&P_?uXm=&v+807xA85TᆇiHa8,sisq/}k˘BD9h0h>-__,Tz/ZYN>Q2Qz.f_ivA. (kkW ߋ5)s酵} lHQ̹16o\Ͻq#Xkeԧc(}={9 `fY<=GMJMCӹaZ6m3&..|{Pɥƾ,gf!H8oޅ[k Jق#}l Z ަ38p%@0'ЭvMz4;21rTUե.ێ{cWJ\pͧG376]PXX/w)Ӵ5{z ƦviQW.,RʵԵ-J#B)83L=c_|''Dx\[X^DxA{ eYe.N5!ҦrT])V,ɤ[ >Zo߿3ŚzjwӦwJ z({0AC`q r}!PM LC֜>;y(Υo/;߻wnum9]-PTq ѿ7~9Z{N& fΘP7/|ⷾ|>ZrVDt+80b3m<3β.on̖kr~^K-i@DSDFHgX][ F>m_ן֥v/VWPZ.B&4:X nUР0 Gu`vWd{8/W)C̾vaŧ>đ3'gr{[7>_{h>ř17y_0!Ng0'q1&F+F T . Ӷ~峿c32)G7/-nԬRDuHD#h{2{ Q*vuMN,}.J)@}$KxL Σ@ a)Yt ]ihlWxr)80!δ +1d!:]+ MstL(`晢A`o*h$̑/PF0'Jڼ::= R`w礛x9AYѾNއ{5!Q N({}9zs S./r)x<|./蜄7D7_D)x4 { AogYRi_`uY5M1&Xc( G8!ˬKIT!S:jwq&dNqRp㴻ڮʥ\ a8k?g뷻#~8}(tIr&ύ>l,oߐ#~CqWk(OWq&e݇y0#>|vU3nv k(O!Df={z `G dﵻ&wdyckշ=&}L'9\B[= _E<iJ!uKJ?&>ŏ}#˥ZsN'8=&'ݕUn);Rakˇ}i| G scbqu`3Җig]>ƃY  fb>_Ocz`=j/=_wˌ Rk8)@)R [nA6j `S]kW./KbBRV-LiS[V !Sԙ1kwɷ;}[j&#͇+%jRr ٲ݁ls;o:pK ׫5f9,xW7n\-M-gq:?}bFJ*& S~gpuX{Smbg_3M: ߥ0R=~8나4JNuH|OM_~A_7䦣&#kCl)h ٷݖ:qw7R)G d@ B!N= ~) 1tDxKQ @w@ 

@ }#fH8Gݸ嗉{0>]'ӆs{ cqru_yb(ɹ|p]uLSt̆.'k0SC- ?j,9<=ɡGM8Y ( .B⃏,lqEx p׶f3va̦>rRk77.gSS'@.݇fG%a}}뾭/JgN۷7BGfK кi_!Da?vl>D`13NTq]Jv4!s'=R{`r?Uѕ]RR?@F~}g<1Qn9j}Ǐ|C{0}3%pf/>r`721ޟ{z4ިY^GjS۪۶VJ>+^ھ:M~1mjj]xow5V~ŗtSحOK -U88`)xbg(! F>W{b(?o !ȧ~k {8Bx$nc~wrn su̦R$ -kO?gQ{ `߸ڷ./ 91j6qƂj9zc83ϝ;kW?KȦ`aܨ6F?i`/Qϧ^15S(ϲor|c?zs_ԫ닅\*ߙoͷڰӧfm|ǦNb,*nvkn_Y(TV,M]5Mפ mG23JJ9.;lGH1a<:3'O;<=2#7V߼reR*b^Ff1Qi)ŚK鹴1K3ù3?=76KZ?&]Q{W,+JXoTjZH ANIOOC@fzdÇ&OΧ0fT[,TVKbP;m%"!KCH.5L&1mpX,*KjRw. ]Ȍ3сa)0 t|0 ` @0 ` @hi!IENDB`python-telegram-bot-21.1.1/tests/data/image_decrypted.jpg000066400000000000000000000567211460724040100234100ustar00rootroot00000000000000JFIFHH    ""*%%*424DD\    ""*%%*424DD\  "6   JLZ-Qy6zs6Mۺ\j.{- oBohۙFc;[197zsAk@%Tffk676i}9 OD!M1)=snwI)[DLR-DJ6#9:'Br@ &C91_ަ;XqH&R-I';S2ĺ" `D H &,2-Nw:C޲jQ2ѹ hH N%3e Y}o1ef$rS1ZҸIÄH ǎ/ ^-B$2%{u7MOQxi;MxjHL٬UNȘy iDec5]Z z]m?V2CKuϘv"f&KE;۽3f F &B2MA0~k:5DH12_ cq MAE!T&|tolr1`;9kR&B D&2 naknn1OhBB0E]MߝPԷ:4%nn'q @ڂ>I尿OS8;(z׶.yXa(K1N䍿H {X??x2<EL;7;?{:/!Jz^>Bş_b޿,m]NԱYGfSL@"am=ۭ647jO4=3={lrMoY{3tH MB*J&!1.ome໛[6伭ɮm_/r1YZ=& JDAd)J)U> T$|6=d)$MQ7|8]2})ܵM! *T-%0$BUUL6|}/xdq@j6j/>z/j4V8MfOy5b0ۧa YTbB@D"bL[-c+7T@!1*&|1$ Ar^.^Ã!M!\D{ ݬ,>y;::^珓 d+;;4iH hՌ=꼧-6K8+2,0L'? =vsnVpoT@! ո 븘t0@ LCi2F5ݔ0JQ&b5>iŧi\&D=-S|0h-YJkh}׿SP+ %j϶k<<̡TIDVF>l%ص`@Aj鼯/T Z6w8hnD@)"jn>+b6(J&Qخ(B  CHMKF) 7]CH Lm^p>9멅>Z:shq@  T%ma&"lj -[jEL7Yf7l If 7اZ_1х^"KhHRLS_eMdDD]HZJb>F|z P:B@yX_2!wNuuϺ|=%0 '3k_^L_ HS\ .|aXn潎䥥mTS{9 W$b|y hHQN%SV'κLvNյ` Oo'K'!{H)  BdĖ9wSuV^JjT@sj5?~0fn\Å )8v?1ue^vnN tJf,776{sio @&W#Gͫ5HDTZhtmTlnȻF3Nr$L  MJͼŵx F8Uhj wn^oo۴\6] EtJ& j ML \v/-gr͘O=` I-fhYk_-佛Mal/l _ޖ-{pXVoB"PxSJ/Ǝ{lS>W =McD@H3 LMo<Wo,2_V<6'7x|xc J]&*]aծiɚQfz~ރXoZw40o~=65'm1Z^/ 喷UZ^-U"&",*"?!1APa"3@Qq 0`r#2S5BRb$C%D?ej#/U_:uG%HJ_ 3[j)~pֳD$l-BδscW8:š3+>vdŦ“6 (l}#a/(F_Ql}"v˔8Z8:ߊs*si\6?L܃$!h=y-fI>#cϻi8E̴ Xi\:š3g;i5<6 PAߘSm/O1zΟwlp*٘ULUPicV4Q=dM;uUђG~-y!$Q!v۶B;M({f_X -zE|G8 m-_xJݳ-4:PEsϜOݫBASjA$^<"Qҳ \IX8Ng] %&0(ru:@Zk1\ iZvBZ# M!˟h*Q)I-+.PGbBɛwddW8.ɘ y@a% 2FcӁԃ.’쑞G(ʙ&FZDݞi\Yw !,NWmZ a!-PR;_ktӘ$R-kk5yBE 8R@BjiYA3Mr.Rgb`ZX7Ptn.RkbDZWAԸH@oehCN1g]SF,*^a,N,[A4O8nQ\S.*yiC`M"˺,%9;! i)BE2M! S8n%]ì #46e1i>N[YzFq/!5](kQhAjybTR~}MCLXmS2c>ΕK':BΖ>Ʊoݳ!~FiN!ŗtء\ KhJM2gR6)&d$sZ[+(X>i߻ȑBf&E_?JG.=ˢ]O!SɣC-$";M-&.L32eJ_fY4xkVPЌ֛[BjL]뼉$fa5t1"y MHy!i i'(}(LjF[7yT̲hRim-HX)ZeN!L]뾉be5|=t"O%S2ɣ4PATӲXuPvq[ S%>J:ݻL 4W3 BJj"=@; eُOtVle>qzEgiK)4u9n"e$r."F3\C)uZ[KR(G]+)^q{ ieHS>LOvO%/p $ e~[Hq Qg29 vΡUn.fϖBNdTG/mK>Zѕp:j5-<Et,ũ޼֑Ƃ4Dr/w% 5h59,d$T[REEhE"e"P}ҲPS]:/ug6("Ȑz~m 3Jpie((iz4QAXړ\t,ɔ=׊h)}~f^ҞMok. 2gqKA e, J\tײu>"n{U)N,(Cq1-aPs;Fc^Pŏ(I@1~.hJ6W-a%\,;QߟFnøekJ.휻>@%ΣR+Gv'%,ټЊq'u43_Jm 옼Vb`~Fv^{$TRI̓$jIiݹgoV8H BS q:F+lѣhJ]5*g[NX (%#HSK}Ӊ~!Ią E"H!>'< ZF b^Bjd?J]GNI)ZpG@=?"ydxsE{i!jJF.xmƺpHfRb`ѶbN;C1JDړS ˴@TӇbuͬˊoքmFp`Zƞ͏"m35AܚAӆ.lĥٜzrbRIBRjQ @)OtD^ &\QQ>%VӍziċ4ٹWXa׆6Ӯ TJ* JYTQNṼ OqOf+[H} `9 3]:RiW3EyĝaSt O0K>B': #HOٵ+_a)H.%9j((D?5Bs;α/eJ !PBl,h2|&s?8;~A]:{AE86%d֯u P%q.RjeG$.pbk{1+߳cg5řoɐ [+^Zq# x#-)36:pC]1Ut eε?(۬~)C6q6 H^YC6Ԭ0WNY5QyπړHJ8Lcuō CWxXB٪DHIbmč)8P*ƽ5oi1ի *nrT7nVYq!oNPP+p<ӁI>tN|?MX2=qˁ^'bŤ+s~ȼ?e@Y zk$b/-,8çdO;!8Y"p ?{ͧJT7aJ[fR쌣Nۋޔ:BƱw8[g2Ǣךҫ_ V5Gywy <Ϊ8Zy5uۨecLQaOuIшP(y{;ς_ AP4{r}(ZWiCkvJ,^+E3֧T9 8R^Yg>6 JX T6ʜ N#9Cj H#1nMi5+(JRr\*N3!J:_qs"§(;e<#0RFXjU!G=9Ϊ8s.XRF/s()U;B%hײ8A xΤ, !Ai LV&ٴr74qZ[tT옛Z0ꊫ>Zs {VXzpqowܪq)έ&Qy;iL4n+dZYhuT3ڴРI4otNGP$(oki3| 写q@*+8^e]YQq{q8FaIPk'%SStni,14fq_GgaP(>Yͬ(Y2h xM='YBzo4L'LsVV|wxL:^ZZnzZF*5}3r1^PvMmv`@B/w@^Yۭ(Eie/hK!lAB5σrTJ` m+I{V闗qD/<Ϝk- {>UźEåVjIi"By+yGt+pm2O(Β+.p%iğ`SҠeVBp'TTp AjXN/u=#}878εIc55FfP/) !IR"׼jP(Ej9{mVEA$ˋeahUh9hH:ek J}hR鹇hΑ+eCuNfK.~}.mm)`Ueඔ6tZť0]WnJ.!UHZTg^;ʱƞEӵZ,%u 9Y u&Q>ul-E޼(BeG7E U^0uek$V%VÈU1w 'iQ#MLdMbȡR毟!ԵP[+ Azy U| ^^YU|î՗jOGiմgzy UAt^ ™*ZYUxÎ啭U'<ӫiiZB"^$Ρ2"qFqx/ $ieUunKY< fE{!L K s}ŝygjsY\C |t7Ӣѵ%$Sp8$jeFTyt4̐P(kYԸw@S(%-v(utwDS8vaIƲjkjeH(kMgb{2<ӉIC:Ӆ- +XMhbս܏{;2ĕm"Bכ4M)YL;MK)^9{\8ˀh+ BTry3Hf<еvάf+nm!drKi8pHR6UT&{9pR֪3\SEU^ H49H\ʉuYOKFiQ#x[6qg6Q#x' K4CF(c}5%pďS׊B`UB;s?cm}cn#[W\r /(U𪘫LwѪ\ִg 8DqTi_:u9XOr֟aNqDz)!1AQa@Pq 0`?++3J2, )%l?3L +P\a\֪\r$/&%E c$F'.Sa&cvEpIgk<n-<8T#+)])2_2t{ZOKS{ f/Bv=m1]Vdo gstDn]wc跥M䧼\% 7`(čX|WeH姰Iсr2>tjVdbSn[eOreXn$h[t[ ^pɕ{ Ϙ:1B24/`Z(l'-(+IO] ., #q+qc',10rx#aUU8f>t#|A ` Krm%Jc [nxߪ]r~:[nqRL3[ibM"ĔBTurnV4 "&sJټĭAA=f ȔjV7}IkloB-{MFRV]q!;=ubT!7"$ʣ U@KkJYs>-P0^o(b1P\WLeBCA(:6Ozpr,Cjrq@,]8`0Gi,/j]*)䂁k[Nz #_ GIUG42GS2YϨOAmx\ h自zvt0էKF!ڽ2NoХ5n\P Dҩ>;%Ǡ55Yp^ͳ}!] ң.IOEnɄp/ qd}r%ɉTd}{P܁A Z 9Fh'){{KzaJ(6&&E;SJwB2֟A8鱾_+}H XWT1r!ZC0* 7--7/}FMB.R٢E_`異5_L>$q" f4Wc3񘶆1R)o\EP+1!.`ݳgh3"|zf熒 6Z! yak H}KzmMݣiҦ=cMfa.j:֒x't<'CY/Иۢ kڷtn.YQ957820W4{#.|נzK'#H=S:\Cazƒpos[4E=Qh![ֺC)[+V—Rj-v +)ur׸rK`deqJ>!nq Y43ݡotVQ~gjoP ) (Ր~w`9,޽@Kغ_.z1器љUAQq(fFhu%6odF}͠4Wro2Q%cMR fbZ+E3ĩe, 0kHg~RӋ`;/q*mN/gX !Qast!/ ~% .pM5>= +L s,D_؉N-2K Pr24\~KUF?fbAo$LQ.VcqÙwnZ <*Kl i3UgfK)|NDV/nQY >s%g1N kjͯDv1fCufВc3 ("=/T . BœQ󗈎8=hcy\ {ɒ%@e5<[4^% l j\Ne<˜Zµq1n9$nRk|ij%B1/3Z!##vmiKExs *iR0&f¶4mX`s)zbY\(u^KEI ~hT)r>jY/ki52k he2*= s.'J%drSC0FmLtx 2Q` xey\ bƻK1>˃, `duPh25@Ҋm~ -Rq(qW.YSC5O&B(rL[ fvt1pu Fm cdhP^ EaNfmIϛk=PUPhXRΉl^h^*oz^`b.dP1 !܁dKr>';x~/ܾaB#Zn F*vqu ?*0>v PcZK5D&`yAhf*y%3D{<*5Qz[߽}b*e~f͠ Ej^6wOvUMޱ3%]W%pHCf Zԭb\_Hn=@6B@u8" :7+~e^bP V?Kj@e4 }+U[aQ]\u:j ADd<}p׋*RJUQ.\Ց(DSPqV(i[)2h81:Uoq+ J(&B BZi8KUhlUs3MƯ#r9=9\ՠ`{z{vԦpR/PL/\u1$ -*'׺\J/ov ͫ2VB6Ƹ\gF#xRҡu }K\ZXE]n#rܰq;{@!1jU1AX  Nw{+$|mt,{nUVl5L ,b#f,ӂSU\Nj)-eʄwytǓՑ idKO&j#bBXkiRpRϘKKPnᐼ Jg&X"R1S; k^Mʱ]qʥ86jقBYoDLGY`ڲےl H) ͩYjW^∪+23U-YbaN#\tr𥳐^7]0TǏ2X<`ZaT)K㏳R8aJa5 gE0̯[ŌA_F_ض_-蘧Fk)8&2Ϋă@cfL7qsa*WLhѻeSwK;Ne}edq1lHFpYJ2cDΎ!o$\DӾ }gsd&)mD?zB(A=U RyU龊vd<"; Ch$'J0ܖӓ*, ~YxJ=gA^@۾F YB9@#B)p >*m!Km$QoXe==X{/Hm.6QcIR ɖc Hu~ 8o&Ы!I39"TBI D=SpBBA;Uu8]J3 jPl) ";B(mLE9ʤ$V{ c`IHҘ1b%XR \\-J&sLMń*nPR3F%\>`4h :>+5=YVi66}D$GO`yxFbJ PȆT{B ḜzllF+Op:Kdi'Fx8F~cG"g&ґ{u P 6~f+,baR[7XAM^[$QII>o#48޴~,̔ T>Ε'5F,a탷8v7ǨAazG-@gN%B~3j |a}U-$SdhYK,)J2\/<s%ZK x7Mn)!ISgZbJVy_K޾=9'PnzqA[%7Ē@i-$mNXKKIY@]԰x0A[ JYR>RpMy>=$XRp(fJVvChLtJA f(t,>\ 8o $E<9 @@d Cڝ\ !KʮM S_:X(sF<ʑM:]6Dť `OP(̭R{ m J~IBTO/**be=+P`<掷-jREWiۘTP7̥`޷Kn# ^KN)kOTs\Od6wsʖgvS`o DZH5m%hO<f_ `0^MVTLA+PgGNq'!Q%%o}۸<[r #+KXڹuDI( !Ku<䔸d+wjڵMmĩnJ_;y6m:nZiY}tͰ| `&1-)K^Z-gCoCbϑV\7!̐sxhmX힠ouejGI]pnlPpGhڄ%82xZE&FeIFnTUdDlo ڒ&ۏKl*?0Ga=C+p Q9o*jq㺬VMFٗNs$;)c J?VIgF5GO"]s18šI9s[)E7p>*"@!012 BPRArabq ?Rbѱ.`x*QTi+Wk2\T[MHtnq*J-[-5z, (f9Ej,,fY>Rg\2Wįi^-q[휽+ZKCѨ>j؈/D= D~V~7mw^kAy=?cM)e͗mAI?>!m%ЬA Dj;;=M}JiW =OjUŦ_'v뙴ż:Pzu> x[-3EƋ{7P([e6^Scd1Bݼuܹ+*nRlrfԫ2EoFJhJ/Wt=ަ=ei>ZVjMв aRR KLz-\|֔i .s ZaiZ&4FlivRFsQZ5UėEaH׳ KA؈LEnCF"0)R"7!?0!01@"A 23PaBQq#$q܊$Kp7+WZS/YEd~f1T85pc+tũ]V³)I? f3vC, I/BT)ʋVQ@&ښ"QM%K)N"k7±*J)M5%1##5LBfs&+{R߾V جVnJDǔĺ͈臬\eºD>1R1asmV8;5)֗ZuKȷP╄ƪj6aɔ3@t?Ҽ!^ iq+fѪ[k3RnO0kyR؈ҝz]5\9VxqX4a)ܛv9K2bciu:~+7K_Hhqj1rc:&H@P'-}tL'#5$y;y_ = )P(psYҗ_xs#׶j-0<}ո(TGzGHZ}X-N-32.8T\江Pw>~2L]?>Im˲ӹ2T5TzqbtýqUaUZMuv%YzsA6aܘD* 2} BЂ\K:KdEˉ_ejji5{pS4=daZMôˇazR[H#z9 Jv13-e-fSqk$pz~@QN{l!dCӐmU6nRS3RP9bEMhJL#QTkP>$\R{4 q_pZJSs(UMEm:$j!-`z(tbʔR NpKΨ,ǐmL-3.IJ"3ql06 =.;VJRsYD~+.sʧ#C Ge9SqtḚnl0T#FN:()fLa'jvߦҕ; ȶ2UJNQC@}E7H0~54d '(ޮXN.qN4(I+6ֱp*I Se+y%Q[# 88a7hLdbk1hOfG8C&82`&sX֍kZ~4:8$Zv S>QZN0uhvJTD8; C8zI&.ԻPC@fl\Ldb%]lVib*ܭaũX#qPiۀ nU3`1#) q@sScoCa\mWX5/՚ӓI mAĥcX̧L\qXqDD\٬&;-p1aۖ( >9VT+OOLis@֫7ӠQ=ozYвQ0n.#M$HA;<Ĕ)+JH Py=)~l*9*T1#-ժAcJN|j N=L}=m#*܍e8kg3 "@01B !2APRqb#Qar ?$OQ9A`/db ]rG,Gp@" >$Z~P!I{a3/>*ڞwk=)͗[gʔzR!jz? ZZ7mRJv Bk7ȃmMSrq'Sf(mKSf(mMo b)r%)3 )v_D|/Jr%GEkb% bM|딣ߕX(eƻ ̤"hWhĆE2P`Sj`"qCjv;d:FINr*;+tENuJ`pFzQ;-u D.K`*7ejPIҰb!TNu,Z)yd[Дءݗu2+y;D(K9ڜ 'python-telegram-bot-21.1.1/tests/data/image_encrypted.jpg000066400000000000000000000572601460724040100234210ustar00rootroot00000000000000k#CKd| !yN"]tT "qBo][ϡiykW\mvRVJv]`c}Ojas.eX<Qхx?z\v'Ӂgok'X0 -% 3 T?7@kKG U#Vf0c+rd%?ё,^+Shɘ5:cj4(j`/E&)g,2v)SSw+2>맍/m<ĵ}H=^T\yiW3wv 6w3}-T4)qe v7tљ|I1pSp C d/{&;aɔÀΒnj ώ7ԫ@K [k.u.x+ e7íVg@KbP!&Xdzc%vCB\Wh('ز[8mHDXDxL{ELha Ku&L_I\#JapӹD92N}`%hgjnr XϪ W'6uUn|w mu7ĩ/aA j &5NA`}IK7A‥?9zz ?3|"yYw~JFjzŨ)Z74F0Օ5ve IkZ\냁hlo0yG(BVy(ƏhΠJ3OT$sTV"GW5.]>i_s'vظ*6X3V}ݢUDU>O3!&PL7$@N5YGm`9ģ:̏ |ueLoW$>A@~|7*ewTMf%Pi*Me;檯yR p&IOjXTӕ3>Lޕ9fl/ 9-q%gX%3AkQ7.h$y [3n94:N\^R <|u֪1Qv.s{=+W սH|>(!uu޳rz D+@>W[756܇2E<8hog1,T=}|:/QC&̻]|iRzV/*s~rRUJkΣDs֫6L1f!0,0d 9<uj,Hxrј_OcQ_©2;mQ=^ 5CIGmTW%gK Q"$"Ya:$2XsO߷) #C]-bk\-i9| %/!p4v͚dK=h@m6nvDzA_=1AiW(u|{ɺdXeS) 3oVc>\.Aq"TZfJ6[.@1"N'ko -<8#$5-pvYӷEU{"Y `J:u(|AjO[(=&|Y90#&Jڏfƫ1zX_|l~ -Ahh77Сqy^^SII\οH.Hpƪ~Qi^^%[%fx2_ȰS(a0Zre=d: :`0ǣŏ}X{GK3"jg,:P$Lp\.4Y—> h:>۝y}CЧCG'u5j}Xʶ+(T0I#y5CcR+CW o>æPSS`z.1 ~U[(4OB՚Q~lr:.mr0L$Hd=`E+0l&ji6ds,)#zh"&x,ﺨNKRhȾKiba46̲גH}WF~v=_ (At U'tnʡZ[c_Xh%s(8-eE5WHj5DyaQ^#N@T".]O'wPCYJکB7ۙNS\ q RIU޵h{L҆ q(i|Ԑ뵐}K$0[ae-8v$J7kER0i!FYhSp6nsi"Е[ˋrhnٞ_Ĭ'!!-,Ԋ:A ϼ dZ&@dI朤 ,$kZj̭ <+ n|ST RK?k6]G醳-"+@&P*6Rbd߄?]JKw:Kt݈C8| 6 A[4#lx#yq{)3̀,Wu2f31Ep2و 791aZ;s7r9\J989`u *+ cx*ytWҔ1dM䴰zV[y=P'̔WPE3y+ORP3"E#h$,ٌVG!sZ(0܆DDœG|if]eiҩPAOgS p|^@? ];\  =Ss<*_p![)0  $s%a1*?Ty' ^M+?ص-:6Qs ?լH9UV 5@ pc@O>|WHgtW֘0G(?zA:Fjſ%mS~/!78/.I>oΙ\=x0#"T7qc"ROP7?Θ6$>I2DV[qg|dj%|x#WN ݇B KܨYrFD})IB  zuC踟~2e.Ņk12vr봬)U& (>V?1"%Vs "CƖ ZB!f'$SXƵ 5ӀO; n|Hka RacwW]pyB:aDL840/}GsBa+fQQ4Tdx]lf/:a#ܹ6 f9a,wn6d:XDdJT8Ш(A7@\L 3YqݬK)N{sjME L2mˈWYS8v)h%{qYy6"q}nOO­XE3G\tg5u+KBbu)++x$ EoDPBkkx?J΋es7ЌW%|u&3lBk9hVݽW@ஶBM 1ߣ3z>rFaq/ZڏEL){X`rJ L>#plMmR#M`@T|o,2U8}ɶ>"";-DpA nT,;]=e䳹xJ\^ pZMJ̄Ҧ<-{~ }iN?|t|(:AzFY'iؑ5@!v#j9yPk=Un웩!ZTOr46ke.,!f@7_|+>!*g  +4U`"M4Mج+T,.<>{uF w8y \QސY4[9Zλ4L?}UE Sr [O¸-0i!xH<:T0+([%Qݤ1|˺QhU섲A5oP%xTiE|yTLM+(=|^wRH4?@=nffV1ul=Qmr{3:u붸os/}[C nc?yDrxZKMNEd؝Y:1%;+<РexA;cPzCogR72KDmp-gR P9~Xlr]zmV%"؏^Wb_<[آqU{ nݔEd BH7#퇩ҞMT ?~74._*wo9_h +S;[).h[{5C- ps S讟Q^t"~WUZzu=í'ڀnNC)g)Ѣlk8sE07e #c\ )ba}XW`V ӕfF~'UV T);n]pOY1>*`KvhTJLxug5"ـqəvSo迧^l;\cm -idFCCZipf,`gT1bLWWYVWŽ{C;/2r_ Z:䖟]@Q (=>Wޒ I<`rb-?v ꥉ&TqFtc\ho%S@+T }.ҸIX-}SP;'ϐOp_| <7 Arp56FF(GM} 6m0oS\Ak%ܑPtCX!HT1MU(P,7tB|VwxNLm`WD Z\E@;Y[H ma %''ӠRs=:(\1#쨕-Wyr *U'dzyUh3/^wfմZLT)1+U3N"/5𼈬cمϵTuQTT.v0Qtt^=.ItgJo"}> #zbNW+zH`,05#/gV;Tl!: b?p@P6[ҥbP =be~Z*6ӷ HNW@쿱U \z?X>TWgaA_id<]_m,c[i\l|Da=:+~Лk mueL޺PHb@1)dhɨVV4ƺ6 #SpSii|O"ߡWH.`U\HrЕ; jXa\$$E (yB{/qPt|b*2y0a)H-(o⎣(@+pcR[tYmz(?/+e2X_v`5jv['H$ b<'1Gڂ!u9x54j]P)ljM= H%t(g\S)bLNvvl츝tŰV̼>bpI .aY7`NKcB~RHŜE^1h5[%Ǡ)r]G{O܂ؑ50Qy+"Lv=Bf:'$%<woUU*]Xr>d3 )5= lAM6 yH]Mm @^m8N'B\gm>7sq1`zKdU}hgfjDP 5}uf1A+Gyʭ 0Hv%ShGl/XEi(7 d~٤NP$)`0|y" gV)jޯYZ|[l9JbIT=sd2 ڀ۫9oJOEynj"~v DX!T=SSN_ANy0M=}<6WቂܼZrSE:;uVj0 fi%/+5kU7!^0gznt%M4ڥ.Jncc+b,g`ϓQpӢCQdKj$ҍB0vur/쳼(*Yh4|,Gn^s/2sd<_3"CmTxt77O~I`gEYHq >JvH=nG2F(M]=EUh')tbxo31a G?45wqj9}hx/sdpV$jgn4nprB2k'|KHN04J XO/s[Ɓ|v85[b[YԈo=ǘ ~xs?.pQU;"S@#Ud˷9B/C_GҶla 8YG~"?Yzjϝ==cI/( ;--ykh#fUXۧdh XD45 Ў?s/Gf`wδZbuL}bWώ)/{ҐqܽFP?7hEJ {jS~'w5|G.=,8D(}aXњ~"ЬWXiş/3Ϟ!kEu< fVJZJ|T[,!?לI|:U eaq|A1FaOvlM*pqeҖCRѝ&9jۇtO}I0 <\o%l(Aꠁ H >:$Cf)H W[ ՒG_mpm5E bS?td08 tJij KY"ݟ~DsPZ, l:'b.=_ٞ#wnd v*g?bȎ[]yL jtyDY(&vrLH)(0j&T(N:d)%d`lѯ'=ܕ@!WwzG!/2$C51*7BoENСcV(=6y] Ubv0dAψbj=BV@ymARaML$ptbd8'b\®XyFп"7lXRcGA#H vԈo#njBI!`edS6Lvl)=Y|GM"&$W2 oTP?7ZӁ^+q N,w؞jym u@M+H pm*dnfc* #ҙP^p:CgRSfq 5h5+R$ pzs_@eLVvg`L|+i e( u5 gpY}U+1HZL,%i_Fy E)-i݄xi_%*#.,(J3>/@U+aKWdu3bN6QZ1,rˮ=/$UCDit`dǭaLl=)0cI\MsW}ʟ,-CCՇKXٵޯ(~}34y5Hw,Pu}sM*~o3Lc/%6AJT@2U}g;|TVHaRyu a2:7&>[XuG3!ns9fL=>:u&i׸ƠjfU2vzIpЉ!8z?/&[R~?y&V-i!h,PH8ɇ0Dbp}+T?۠0vTkb93gR6y*51@HȫeJ0+RhuDD3D?EGL ?T `PD EB8BjKʈ0'&{ÙV+Wmα2 F<LHJC_#iީkܬ*;8y?_#Ô6lXBz\75zp:~[*0otB@zhHƭ= yfuLHqWI8+ 49m.n% 1HH"-Eь_QNΌL2wz1<`ՎC9x#mI[SײVLo k5dW8!H`tOg7\ljwyF~%?#s5P(&!Uj0\@x,!ҡwʲ cTTDJڻjC1&^B˽l(}!i ?PUxcy o|b]qN_g〈Í(^F^'Fc]uǸnYU_#ǯє3Іk>lM1NهH \S2uZ"ءfˇ [c>PU"c]֫\ʂc%Sƽ!;wd'X;g+DY^HYϒKзElV2"(Er(v ƴyy@w_%)OJ|(T!A6wf%ܻps4K0޺ CmYh>Tq$QBzbqRh쎑MOuNi8r5+}W%JM8k8dQ_EKwAsjll9XQ礢q?P!_e$*xKF-}X4Ԏ:HПOmMJ==X//TIC_6"eGˤJi¨]RUhsͼ+==KU+{$ P\ <2~L0\u򲢾 u+S(`}͍??_H0 bX rcMB@(FôT_-%eo4YY<<4F~a<$MPv9mdC0UĎ|Ccc,Ys.؄lA2xk;E_>3YBײN  zR9lw#d m$L\Hu浝ȗ5v4)'5~_Qʼ[R" ~wN4Q\)g%C_gNߚQ@O}´UEo[ջe Kc1=m ˃ + ?=X?稠||R=$N0}&_$BQ&y] r_=Zi=ހU.mjm𼤇pi(arA!P{FAX tiO W(>wU&_y&WJ>TEM3y uTԹK^jt" f9<$E>3XZ#W:MICjXfL#[PJD}O gxdyu21 ޘkܢD6W1nn&ohs f wMcnjjkL"r:x ts Rh EPpeLtUN4(quW#h-a0}iU{习]%Kۑ`]% |*K%";ih[9A7fnߒ/3_<0,aJt'cl:Y cSfbA2Yr6*Eq'=Z}2 #HV) *str7~M><1& YmuA/: En0J\~g9ZX|[xj;),%\%'Ɓ!矅tT)We cU#K=ILyXAC_d@#a$<>Y5>&8&Ģ-akGlX<<"TsFϐ9[bxN쌡ʼn+r94RTOV"L*uN4֖HMdF?v56 hx-h} e"Qy_`8)Db#2X A -ٹ":_gˉA ?Y3<2ʭ"`^zJ- $t)c+-e|y*㻎l !b\ !86=οxHMS! XuMFBVbcS"/GV9 45xOa+hKF<eC({l)7λ'bFjG< V%>+Mmw6*MS݂ce->RE(UZv"֦.cF ̃|&e2+dzUQE|I15D~U7-PD*=I>d qyOCNJ1pY9=ޟęf ''].a6+ȗf!)3LJ- 5 *YE /g.oTipq:sD^H"FƱs@{!x?^';59aZf1 >24E꣌e߅FuQUEmKZq *v񥏭Q~J[m"brl$Ţ~a?fˏ2#A7DexcGu,Y\ aH㴳ԩP9oQ*\8q6A-(^T $u \G6K]<ME,\#~BB{vK>uDI,@eW/;3Y]v4 '}GC2 yֵz+hѝ :>C%WkYն:tJ[/n;F+uoWzI2}M&ωC}K i;]{&43zLDh]#ҥZB Ak範d'4#UJ_<}AhoJN3'(gpjWO>>}D)G#!J12R->ƿ28"\@ $%;1崆4z` & <>X\~UH2 cTpL+kcJ.RdpF5mbȰ[3?!lm˹ Ndw@$a!%i.ˡ5 ,ʲa;}n?eŗEJ1kyW&3)Xo- IUq~T7JS3'?#nhD;U`"R !\`_)0g ^( y_ s&u4%&:[d7S3N+ r51ٗdJGؒ GwbۄWF Q:-~WmL,Ș G 11vcHژ0'P Cx7C17XkҀRþ" --5h991'1l"ngU*8 !]mwu UZI#tXم,`t|wHj1$9Pd#?u.Br#+x2'&ufs<=75~}EY{5/q.йo0S∭+$ a]U Of?I Pb>gQ.@C!?c?V,T)wv{R,3gj4nOy6[$pu:OH`_LfةYZLNϫn;\Kp.]DxJ胓ItB3%"+G-~u e5O ˼ 9iMgp"?Э$D2Gi6O]C(ݨ<+uĆ^3-1&քvzvl9NQGѺL@D1ƿ4nj-$|Կܦ`M&~TԋtsD6wQ*oWA}&.eF:;Jts":t#h漑•=M킰8~qKxJekOtjYf֤P%YfIJOrh2vYKC2!͇Y4Li%?P>o66$G\($d%EY&WEK&W/& 7=m3g)[}sL"8\:A=7$_x?nh^wGy״S oJwS;ί8ڒRQח6us>:V,RDTX錿FI'x>> 9rV?W+LqWznr BTs:) rnO~;YY3T_{7I]otS^UR" ۤr>$}&!ab{8]00iGS)t<" cm4hؼ5gOj(~b=FS$(R&jդPK~6qh{c )yxgI% "W_a@FZvT H'ܻĖi(L7 *C8Q+1-렢1 ) #W~!3$ L$#֑K5ͧɷ0^K@k7RT}9(Z!7q(bG~z'>酘fM#!}s>|[PvYZ/kBMzdN3M,Z#;9{QAu61݋vx+Impv*.vmM( |7UWVEnqUIGa\O+Ph9G=#РZRWq؜]h,zR =L2aK6%*&6K 7d$&Te,˺/2Bn%.L!Tf(jϑFVY_6OəXQutSurȼgDXgJkXBxM .2, tJ;0^KTӲ{&uAe`~ZO:ǚ#0XTy.ݫRI䴫}9(9E}"u 0ny|!Dk[J/-y+گt9݃ J6]ue<9=X "~8c (Mm0 $(ÑSX}EyQAKo;e|=m94~|9š~(9ĄSCI#yZ(nk -n=DdiR[_VMY>e)c8lzX3mw{s~C`~$ ΥI $HHAnY萛%Fx Ą0.=RD} k7-h WMa[5d|^vP^ҥ.Ih̽g>^\#BW*([\ F "qL]!׼J-AU;5[-F&eA}Al:,W ]}’Qi>+t_Ķ0?K0bύn0uDI?nׄ Pr^*R%f͎6`5Z6$ lHeA29*fR9Oe2 .*!E2,oSO&fͮAk!'8NP_]֓vMڹk˩답؝'F>T2VqGM 2my0(i|CJ؟$V~WtQ'zZnvwDDf!7"WdFNⱡ׬Y~t} _س&Om2Mr/-hrKPg+=z刖W=ljTc$Ja^BVF?cl >8٢H3_tr{Is@Pv;T[\BZJDPFXE7΁?*&@l{ǏǶ M~ALjyh&qTd$#of I2bߨ ״[_ +t}z͓Z M$*ZAh*Y =63crj`菍ǁSѯYa`3"BR濩ƪ !}8"nTt|_HD%b|~Zd/M'V _yu ]/'Ho \AWKLx€CD $UW]%&ڔ3 HzkqUSQ7CR;8uHq[Dw:!J%@k  ^ ߹vW DZGHo$c1InXB~^,%}ϟ+OX#/XJlokx`ݿ5. j"'z(1R,pHi{ ob3 @wR$!z͂%_ӗ{lPd.iZTǔRut_i."BHpZc鶛O|lP@*?j"r~Qwt%"GeZalOs M%<޻ڈ9n!>ʆr_-[;1r0tM3J8}vrg*:5F@U?~&z B1g@=EFg|30L*/J_'fX'ʀv W8\qkn|9@%1\`;R8Ak%s.jI3Q}O ⥑ RVBJ"F=;EɖKc/k[7F̷4zKk3<$xʌ(VLW ]hE-BmQ\(SUkX4gG^o.kTEUw0=Lj.jH/.c!jĻ55U4Օ:&쎡E)U\9cZ(^I!άUw^ D5C2'@j2M..] fк8]. XģI (0pQ\ezF޼?9c┅7/K YcҎ͒j(*G3#N]/0B^Ҝ|CMFlX@7ceWl^-|8Ux۵+Ӭ=Kqͧn& R&l`ݿ8r$R#ӺEZ$ܤUOĂRT0.]W>듾BEVhEU$,i6:`N|{c™k"%o먑.yHjB% K|=zj,h)|ά[>Z^N"Hg{@ z@pP/,>$䵯IgBPJB,R'r@2J(+4u۩wb3xE'.'>ȠpeU|e1Guj ZP *eHql2?\\ 0쳡ߓ hcP, ΎI]Os_֣+?ᕷ/}" @O!eطе@- rbDGۥ|ŎG@xkr=&B[R+Be>!YuR˛j`3`*#ƒ}\ ")&'Lp"YeZ̺IT ]睂QN7ꀄbS+ў nVmMc5e0%WU>AI-vS8wN g[KaJt0bLuEM4A]y3zzm{Ek2qjR.EF.?<9N9 1ȬޒhkLЦR JZW UD*"l7<6#Ք]Rb-L?`Q+tpSܲ #eS&81RHŎ_I=Qȃ~=>8֌G7oqڂW71 Xɗ)Wk$E\ߎvko0j>\wR_e tI6nEBx.qN"t"X4WR>֋ 1;7y2ս/Nx"Pbƥfpu7 'CReqAu8KO-U#sYABS&DhB3!r'1kY y!<\nݟg41Ѷ?crf1wnd>M>,QyHs]vӎؒW͆O$VD{MBĪN_ ̪čMI,jMW`⪟G\ xN}^A:g$hbL>|UaS pظ^9!|1f *?2bIxϙUƀ*Ĥe7Q˕77g=] QQ-Vag2ϊp&Aeg_JA\xySRx jvi7UBO.v/wQIeP6j\\b7;گҚB- $⨽00\h-ePaOJp?FLUg ҋҧ̃5!'ou Zӻ*Lu(&$!^11UɗSTd"*B!+c}lDcRjMW q’dOdf4ִf8f5)?rksuW.Yc ʼMI@aXhiPN -pATE.7#{c ÁW5;-Õ %q"x? EE}._ڕPwPɨ<=Ceb&C%pDD Tc)p5I7n$&U/c>J$ts+IwZ SK]v1|EoPĶETn;}FU}뮌:d^\z8~2W}TSShґݾW]kN\.9n`c]x3Mǒï;H"sYAq ݮ 7ji-$B.ӯOXwQH:eսWgCXbD~2FOfuN qC)?q6nJ-Ha8丧kϚQ"&`/]6Pg>WZH-BT:^6$t\oߘW?#YqrsrNbEAwFOՐ^ )ǎ8)GTeev]CYZZͣގmp/I9m/)?xiO•T*=(n3r(meWV@}mMzwWUɋĺB;`P#Gr5b9&J c%|2n7 Q> g)ǽQR2 ixm3IR4wC p!2ݬ L`{NB _ RQ @+Tr*}RۚNԅ@>kP$W}eQ>W""Ǎ." -)7SsҌ$njtsNj/(#%5M֪-igX] if}y `+wD#e&{_ %map('wգ!| 4؊ڞLflk`j\C&uw7PɕE46CWݾ&ʑ(n7^ V}X.MVt[[*v MK l1 J6ْ 5SAcΓi6-ѐåG  s/i.\&Fƛ`gM27@xK۰k\niJ67LTyX*N{îցpython-telegram-bot-21.1.1/tests/data/local_file.txt000066400000000000000000000000141460724040100223730ustar00rootroot00000000000000Saint-Saënspython-telegram-bot-21.1.1/tests/data/private.key000066400000000000000000000033451460724040100217370ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-128-CBC,C4A419CEBF7D18FB5E1D98D6DDAEAD5F LHkVkhpWH0KU4UrdUH4DMNGqAZkRzSwO8CqEkowQrrkdRyFwJQCgsgIywkDQsqyh bvIkRpRb2gwQ1D9utrRQ1IFsJpreulErSPxx47b1xwXhMiX0vOzWprhZ8mYYrAZH T9o7YXgUuF7Dk8Am51rZH50mWHUEljjkIlH2RQg1QFQr4recrZxlA3Ypn/SvOf0P gaYrBvcX0am1JSqar0BA9sQO6u1STBjUm/e4csAubutxg/k/N69zlMcr098lqGWO ppQmFa0grg3S2lUSuh42MYGtzluemrtWiktjrHKtm33zQX4vIgnMjuDZO4maqLD/ qHvbixY2TX28gHsoIednr2C9p/rBl8uItDlVyqWengykcDYczii0Pa8PKRmseOJh sHGum3u5WTRRv41jK7i7PBeKsKHxMxLqTroXpCfx59XzGB5kKiPhG9Zm6NY7BZ3j JA02+RKwlmm4v64XLbTVtV+2M4pk1cOaRx8CTB1Coe0uN+o+kJwMffqKioeaB9lE zs9At5rdSpamG1G+Eop6hqGjYip8cLDaa9yuStIo0eOt/Q6YtU9qHOyMlOywptof hJUMPoFjO06nsME69QvzRu9CPMGIcj4GAVYn1He6LoRVj59skPAUcn1DpytL9Ghi 9r7rLCRCExX32MuIxBq+fWBd//iOTkvnSlISc2MjXSYWu0QhKUvVZgy23pA3RH6X px/dPdw1jF4WTlJL7IEaF3eOLgKqfYebHa+i2E64ncECvsl8WFb/T+ru1qa4n3RB HPIaBRzPSqF1nc5BIQD12GPf/A7lq1pJpcQQN7gTkpUwJ8ydPB45sadHrc3Fz1C5 XPvL3eLfCEau2Wrz4IVgMTJ61lQnzSZG9Z+R0JYpd1+SvNpbm9YdocDYam8wIFS3 9RsJOKCansvOXfuXp26gggzsAP3mXq/DV1e86ramRbMyczSd3v+EsKmsttW0oWC6 Hhuozy11w6Q+jgsiSBrOFJ0JwgHAaCGb4oFluYzTOgdrmPgQomrz16TJLjjmn56B 9msoVGH5Kk/ifVr9waFuQFhcUfoWUUPZB3GrSGpr3Rz5XCh/BuXQDW8mDu29odzD 6hDoNITsPv+y9F/BvqWOK+JeL+wP/F+AnciGMzIDnP4a4P4yj8Gf2rr1Eriok6wz aQr6NwnKsT4UAqjlmQ+gdPE4Joxk/ixlD41TZ97rq0LUSx2bcanM8GXZUjL74EuB TVABCeIX2ADBwHZ6v2HEkZvK7Miy23FP75JmLdNXw4GTcYmqD1bPIfsxgUkSwG63 t0ChOqi9VdT62eAs5wShwhcrjc4xztjn6kypFu55a0neNr2qKYrwFo3QgZAbKWc1 5jfS4kAq0gxyoQTCZnGhbbL095q3Sy7GV3EaW4yk78EuRwPFOqVUQ0D5tvrKsPT4 B5AlxlarcDcMQayWKLj2pWmQm3YVlx5NfoRkSbd14h6ZryzDhG8ZfooLQ5dFh1ba f8+YbBtvFshzUDYdnr0fS0RYc/WtYmfJdb4+Fkc268BkJzg43rMSrdzaleS6jypU vzPs8WO0xU1xCIgB92vqZ+/4OlFwjbHHoQlnFHdNPbrfc8INbtLZgLCrELw4UEga -----END RSA PRIVATE KEY-----python-telegram-bot-21.1.1/tests/data/private_key.password000066400000000000000000000000231460724040100236470ustar00rootroot00000000000000python-telegram-botpython-telegram-bot-21.1.1/tests/data/sslcert.key000066400000000000000000000032501460724040100217370ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC56MwT6O0MyarZ hpHdFDNugvFRQHYfnkuK7CLRTnR7XpDawW3ByByhKf/SWdjMphzuR1NklOPyKMSv Wmr9+2grr1hIM8Ca++yqGb+GnkyHsdrIBlxDGgwdZ+nwzkRVcdmZhebwYuMGcNp6 VnlZXnfPPFiqEd7ZsOM9GkTrL7dDnfpZG3CvpFMBQCLdQNyNmcg0dip9t93wU174 tCNMWnTGTT5b/pghzdkJXblQUhIjNT9E1g1/iHgcckcFWUGqAXsRfI+EDz9afQB9 8VHKZGPulVOxCtLAqKwg3JSTSLR+Z6omr+KmgRKfwzHpdctvBZrsoPOvr7zmG/cr iTIB5KU7AgMBAAECggEANMEXsAqnwbo0The+qnKCCbkEi170ZhKAM0LAuo49xYhX KIw8/gEwBpepbWJrf98fVIpO4rrRWDUzYuMQe1PtAoB2V76/x/r29GnsDGI9K0BP 6fTMF4p7p5iGLPwLLgfpjIQPvWUCMSCzDoYdVzvUWa0xJ8l8aF+mi/85UVev9HKS l0RXXYg4nVT6EU+eXGZEINjC+DAd1zuVM1VMJJohgLSguvOgVypuloDRP4m6Uhuq v/Lk1e+dwRQlXMURKgIIFU2Nnu9QY+KqVkSLomRg+hh2O2o5kKtiI3l4b22wgtOc +gORzL5jIpkYPHOsMa7euCDqHHKhJgnKhvy6u1ZICQKBgQDr953WQXdDrVvWaB2v IxXfyIo8ZIw5tacX6tONPW97mLySCL5mIEPEaHMvnSq759wYorEu85Jj3cvETUFy u6022xipJ/NsVBkPywr8JFsnT26ENuei5KoF4fl/cg/INetSS/0HbmQZWvjOhh+Q 0LlngelkaCLdi0ymDue1uLgL9wKBgQDJsT/zsExVXBoMTj1u25dvIXp53J04unQp qmndUxFgy1vuT08SbjHjK2EeZD/M5OLkXurdIZZ3kXPHMM/bKui92uGRTw9LCAAN tDkNw+E+EwZfwbZsu4k3mWbSN16dO85K+Yo8hjsRLgvqQadMwbAz7RxEFiKX5tlG gGaZkIH33QKBgFT8lgh5A6+IXK9YSHivtk0nOUKPJEIUvt3KYe9Y1TI6zI/8Pjci H8Y5qGLZxG5xD8B/uDkk2PDHDYDiIlRka/p55uPl07KMh4o8ovQ1U+9QmIleDQeK PAJqZSYVusFtShgV7kgi5kKLlVkszWmnA1/YVmsnZodMiIq2i5XTtdX5AoGBAK2r 4tWDSTd3RzaxaFS84Xjf6wZj4T2nz77Q7reVf7FJaq+ZuwyztmFWSRpSWF2l+XmM AdDHyzjKFle+wDyIhkB06SamXRTOnr0uIrKnqJw65ZIuy1Z1ZYJqpQ7+fooFpW0J 0u6q5tG0RK5COjztyzvrQBugs8j5Dr6WccJpnIBBAoGBAMOm2g9OlSu8tbFXK9GJ sFadmjXgM1quDkCfLJgJInw20YCy6NFnujbgczbrxpOg9sk6Gqbznw0iguU2mAZQ UtDt3mbKrtUtR4kPFFwG51OgFx3D4TJM8EkKLKzthxGKjgJuRtP6glRgHTMIlwmT Lmi6uZuyrC8kxwQiV2cmlA5u -----END PRIVATE KEY----- python-telegram-bot-21.1.1/tests/data/sslcert.pem000066400000000000000000000025771460724040100217430ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIID4zCCAsugAwIBAgIUbxUiUtDxld8EMB7W+gh02eBeqJgwDQYJKoZIhvcNAQEL BQAwgYAxCzAJBgNVBAYTAlRHMQwwCgYDVQQIDANQVEIxDDAKBgNVBAcMA1BUQjEM MAoGA1UECgwDUFRCMQwwCgYDVQQLDANQVEIxDDAKBgNVBAMMA1BUQjErMCkGCSqG SIb3DQEJARYcZGV2c0BweXRob24tdGVsZWdyYW0tYm90Lm9yZzAeFw0yMjAyMjUx MDEzMjFaFw0zMjAyMjMxMDEzMjFaMIGAMQswCQYDVQQGEwJURzEMMAoGA1UECAwD UFRCMQwwCgYDVQQHDANQVEIxDDAKBgNVBAoMA1BUQjEMMAoGA1UECwwDUFRCMQww CgYDVQQDDANQVEIxKzApBgkqhkiG9w0BCQEWHGRldnNAcHl0aG9uLXRlbGVncmFt LWJvdC5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC56MwT6O0M yarZhpHdFDNugvFRQHYfnkuK7CLRTnR7XpDawW3ByByhKf/SWdjMphzuR1NklOPy KMSvWmr9+2grr1hIM8Ca++yqGb+GnkyHsdrIBlxDGgwdZ+nwzkRVcdmZhebwYuMG cNp6VnlZXnfPPFiqEd7ZsOM9GkTrL7dDnfpZG3CvpFMBQCLdQNyNmcg0dip9t93w U174tCNMWnTGTT5b/pghzdkJXblQUhIjNT9E1g1/iHgcckcFWUGqAXsRfI+EDz9a fQB98VHKZGPulVOxCtLAqKwg3JSTSLR+Z6omr+KmgRKfwzHpdctvBZrsoPOvr7zm G/criTIB5KU7AgMBAAGjUzBRMB0GA1UdDgQWBBRhCKLkt3RjoaSiV14n1u8590Pf HDAfBgNVHSMEGDAWgBRhCKLkt3RjoaSiV14n1u8590PfHDAPBgNVHRMBAf8EBTAD AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB1yXCnOWxZqhda5sKIQLwHPORz9kfPplYZ RxLZaymGCrieRr0NWPy1CezBsXNES1ICpEZ02P6Bel8GEzGS5cAbYPvIP8qzz/Ic zgN5QG86klixLO6Q7VWYRGMFEI9d/2/UVGbw6KltIQt0bznoKvkrTnNTydQc/L7e Ae+oqVl3OUuhtdU0DOjncEVKWKY0Hl18juSkTO59oHaL3R0SeNZ38chv9wtSRE3+ ACDH51i6L9cwG0hdpuIx1UKkSDvU4ci9YnZsTdwkjbvi8VX68Sn9WsnZq0k4V4vt +uhH8RVdxHp/TSv5LSOTMCg2v33dZjW/xOnvpRQvZNNXBxOi/ZH8 -----END CERTIFICATE----- python-telegram-bot-21.1.1/tests/data/sticker_set_thumb.png000066400000000000000000000033751460724040100240020ustar00rootroot00000000000000PNG  IHDRddpTIDATxMl5}@BhI i )ƕ{%(G@!U\­z\&;cU|H@uBe͗ڴ*ik&d_ٞE*Oai2\7TY $\b&Sq]!I*I1\Ir b(I*1F˄0sr 79踍qI&3b:`"ip64<-EClD+c 3۠DTOpbcJ1C脦!t>+gW1ᓘ)aƒ0:% +zK}+GNr7P}'F]ON܄jMk <m&kXI1\or;̂P?vc t|󪼝t%<4a/|xAJr itpy+Bʂ0S.-&N{ۀy!~BAo]sdֺhe-zf= jX9';jFCMKqh8pE4gU Ҍ(.] t4,N  3CCOG^Y8pi3s,Y`ıa,oba*OHg?z!( -/tY8ͲjDyZ](hoc'dobO0J:!n -GT΄RE2@$$6?_W*C'1Kr`%P{P,8i!vaIENDB`python-telegram-bot-21.1.1/tests/data/telegram000066400000000000000000000000001460724040100212570ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/data/telegram.gif000066400000000000000000000074461460724040100220500ustar00rootroot00000000000000GIF87aX___???߿,XI8ͻ`(dihlp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~ H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKN=7ثw:{9q>˛DŽLտ x (!( x z>BH vTa~az#^!u%H/b +xHDf`dH&y-6dP(T9Sj)NzIك`p0.jfqΙCk0m9@z g~9@oIQ:("z9P@{`}ꩩ@zz0 %d@"p "p~V+AA@zm m0. w*ʛ| * ¬k-NpJWlq {WwL+{e j*r+KȮ/3 K@@ՂlA=h I3;HhZ& BIկbݡ[جA>o Asm`2-68w5W/ݴOs?u.{ȸiݱM =ch ]88;z 2&뮺 o | wGp />do z"hZPd7HlE%[V*M&zi Q-0_PyʗG-;0kܤ];n0%9( peL5E@JTALYVn-e8B@\ N4W -O (Ӌ;s+ia@'A7QtgP/J+y>3*8ZUT+9uk)R E J?FvGFiʠs:HsMW9U[.rA Lm53@B=$Աh/(*+]] ,3O * *d' Ux#gjLUecj)8ϺU]iZgZv>]l*0ڸ6j)9\^-&ApqZ*6(+ 2F60 VS2 /Y)։_b?*J;XOVTud@d٥iK㢀.׾"_~tX? nPatvucM>8 S4a:궼D ^6:&Y:pl?U|!tloy\G0kw p{nՌMf1'63O&YpIF+&MJ7Ҫ0 DO@~ {%ɂNMa?eX{Vp>m*2/F> U,5[C+]fNeKfw{>{d΍`GG[әj%~/au`&)Z}:`~mw{.UpS# 9vllIX ҅;! =X@w(:"#Al&ňjN9huzۼ8Uh@~OTKwykT{過\7^r;.0m j/ D߈]A7n\ LN+ ۂ;} ?8/m:\ l~ZȖsgUA|2y197%K6=) 'e4T07c8mpgux:zwfSfB_?9KWffWJ%gHRHTWjzrx4>'3y18h CkA9R8-t'l808:0TV(oPd7kGz>k/hseHc/x`"yX>XPxH XeHT+.؆7@<]t~G ȈphtI`ox>XN/rXOt((}8 &(+x(2'sC*5>xW9ͨ腡(=| 9h؇>MgW-?G5LPZhI; EȀD gSLj<8FU89J sypˆ2lr;?dQX8+L ҐNf9 0'9*Qu4(7@(iqq_5)3.鉷ؓ 2NJ"<2rx=5);#2%3dtUɂɈ0.[9_]# ԃR.1I9?oigEB8)L3xezm2}yvBv| eC1x#y߱1&1sPE4Y+ lw ,c/)x`wb,Lq!C[d>9&6k!pza"&Dwgz-?٧H`UymxKJv\w>$F,y,v: 8yl:Q/EBAC,rs"ګ:ZzȚʺڬ:Zzؚںڭ:Zz蚮꺮ڮ:Zzگ;[{ ۰;[{۱ ";$[&{(*,۲.02;4 ;python-telegram-bot-21.1.1/tests/data/telegram.jpg000066400000000000000000001004361460724040100220540ustar00rootroot00000000000000JFIFHHC       C   " L !1"AQaq2#BRUt%35$CSTbr&DEd'4s7!1A"Q2aq#BR34$%b ?;2#jIJē2j+02CV'w&Ļ/͉OِGr,k8}qp8._!#{|v(`1ꊞ ROӍPW$lZO =c2̾wu1JXkϮq[>ڝ5E,,؜7GJ8w2}CK冣l^cRw7cuzm[gh{r7fۥ8ޗ!1"eCF6g^v_bkS nJ:ҹO hΙideYm*%-۫AJW,<)+u>[1>"Zyv|<=FYAl(.{%y.?y>3&GS>R?$6 Cl(%?J?Jg?g|2؊4n6SG?]zCԨQݿ}(sʷEq RHd(8?Wbi3}sOJwnG;PGg/{/c|q~k5aCtۧy~0/Qz~˝+t/92]}]+єܮuFwc뮒KekUۻu9#㩤Q[+CT-&g)zQ +߹`18jfvl8T\%y8)g+odZVv){KۛRRrD)'ݓdoJ#8,eknyB%#ܶn,|zJ䋢 Qp% {v \ςp7eWȚٴ&N28D!F6vp^1aVZqreM_̷̟ 4wm[+J '^ꍲVI#;[͂b<&yV[fοYvzCxt=ο:p=v[f~˥jiY ۥM7+Эvq0~ To'^eo<.UN/їmԠ#Ԕ[]5ѯ~!H&x;0Uwv/3st7h *Yn[3X\VyM vU`r=ڍi~uoGF w̦yO9ʝ7 ,S[Q* wv&cqLY<#2'lH%0̆Ȏ}лJ>;vwM'I%8kܳ4ݒ( mJӜoSg'/ʱXV]Uq4 |u-#7!댙χgYT+۶䏕?錰?.c9e+vj#;kV*<#F z,me7w߃kj~KekWۻu9FǦNH5^[b Y_^8)p`:+4&cKqM~)1yv3b(g_lïO5r:56p#u s,75NXɺik_5_Be5Πh gYmAnŘNwR6fVt^$ѻrӒg^1C|"npұ}EQIr$$\2e_ny<,*ZȈ؆ n\NpI*$zl~N;)ɻ$}.׹2ܾ*A6^s.*i.]䌍qy$kvZ|[4֬ϱTi֒[ɘH<.*p Us9AѧO''F*eٟiEn7{NJ9ΣGtæY*)B+-ѽ: 6ﶜlY-=ͶNXAouo6Lhd#yu縎oI'ۃ 4{>~ ̷V;0b6DHˇ`vU.NX  / F[;v۱+%p,Sطoň")ǹ2yD-9'rY;GonE!a)^eRXMoI^+$>b)R ^75#&c.u=E+ЖӍ%KwJܦiqĢe,5 &heb8E=ˊ0+Jg*2[8ɪN{~[Vӹ+W%rH>MOSW9AԢwC+J2:v`NtWt8y>kZ6K.-eѴn)њǓ.]ʔ'&dJ6bq\k ǺU%~J@.~C-ܯ܅QR%E˱ 0U%v+Œ[9fͳzԩ`p&tmW3W]8oԛ\}qw,AZNKٕ7J/V5f7*!ZOMvICtH&)V:R$zKt4s wp.d^[J0~iAS;u IMrN;~b\h(Sk\V=%[%h!y_s|v.EW' $d@_D k4RM}N7(3(Ur5UN T|V{%nWIӹ٣ҍZ3$5>25bq*3~Vۻ2ip*Spu=TnSܓ^'_~O)Qt`/iJBm·sh]oRw'T]~6~3|>cW6<'9*6&{5էWPi6sWڈ::2?)xenM4Q_dԽm"hW[]8;tc,d92oYF=_3e,[BZզUs~yc:"'/s.tf׸ 9)`Um-ǥr<>GuF7&lp԰XZ^K{sJ4,>[}*ӝ?(~iTX)5/{3Kb+V7{(*0\"֍)Gyy_QT  |dr1_q-(=7]-@kX*2qr*PQn~gsLm'\6Tn㊈5*5˓~n_q:T٫~e[ڊаVxHƝ0v^{K(0MJI/N~M鷇 –uʶ-RPR<֗N5KT^}4_?Nӳ,G5o6>lT"h["S+{k/#RnsQz~r7ΕmQc.e~Yu}=Ҵ'['_jFU,5GGtm}cVʦɣX)e mev#5CV% w'uȏ%?/}we\ ߤk#s0J}SbLu,U%S3m,֫%7燌RZ*g5bbLTj(]oXPsgd\Z)c5;{͏0v + 4hA('cӄU+n{|kMiҦ8F:FˏN1Q*?iwvXɄx Zw\eR;@RB[Ye)KoJjѺw列;{}Y8ϹzrEx_U#Zh8]rkWU%r\Ve0*Uov?={i58Irf~HtǔyMX̟W:UIJݎh~pzWQ<)fܢ⾟&yV7'fyҫJN J6wG$մug k45 7ϔtCw.ɭZ3 1W#=&?ptڎqr|݆"Kt8_;^U18VRnrnoVY(7| D)jmGLάo5xətMq=9|/qB`jmI\ެotTn>LWoRن콟!}εeѱuZU|`z/&N7QIRO t- $r#-(d@cDMy%E˅o ^a*M7<ޔ[RW~>L >}c/z'N}2EFicrlm\3:UIqosjEroOx=stQnQJq]4k8V6 *d(rMWMQHz6GUS|G@ 0F,ŘNA0vC)~7)?d㌑k.{ܧ+fX,ujVQڹ|(X, խZj E_o= u.sx(U]F5ERo x}И :QaW7AE>LIZ4Nݽo/GuecƂmbp| 2Oq`9@HHp @N5tX:MaO8ߏ;j^GMt挶+gc2u\6U7Fqc馷G~!<xxS)ŹE+Aw,e8ʸ,„ե'+;j5]2R^ bVM L^$axMXi*IV \{gko R{_ZTVIs6MO ZOTgkF|m~4}U/kPG}JƒU#%N>C<:6YƬ8͙wҥ%eG_`ptb9Rx6ES'ΠjjwRa JH L&Ǚ{>֒5ſJ2z~_nPpNߖ630pY5ԣ{\k163=*5>QHN5$\5VPqvl_Z ]W$zIٯv'G ~ӯijTUj˱Sqmnv8ύq J\\7W e9bx;>W,=JkIA(F\)mE*5gtyRY6U~^*$t@e]54tMmn|f=Д֗Q~2:MiI^k)=E+ҋ쬶Sn V!64 `}# K MS Tw%ڧ1dtݢԄrĸ>k?ʺd9g*vv{CMsFi~]ܩԂvrzZ{pb< WaF^c%q^4qZ+t9YUVu_c<_ )Xu ҫJN-M[ڗu*tvmJ9L9%aح%Ak.Xfch7Q𑹾WRVPNߔ|5u,.yTc/ns,_˨SahSBo'}LzYR:/8ʌGu k~-STqUuZ?I~Psʛ.IN_~~Kvi}䫗[.1=(KS5J)OZgAƌ=;~ZF줬o0(fxZv>:kEũ+37\dzZ>k~9UhO/Mpv^/kIZQ פv4t蛸\w _%RmJp("qw@Z{H-=Wl. cVc%ٳRO6L،kU rNWj_c';ڄ4YV͒gNN. 'ETN5g9Zmr}Ŗn̬Od}gm J ZUk<(=ҧ$k]y-vim-)"qpy);ۓ㺧7m-nӕH%¹IG?s֝'F}լn#^6VsVt*R*HS_=4hfӼ- I$-;?Sĩ-&. [n1I=KTn \>8%4Pbċےԓ5o^墓x+.ӹ]l8iPUUB7{[$? dbXFkGNM k/7:jߛ3fvP\$zLaW޺'T_ %.b6o X[Q%?@v)y+p,'}e.ri.\U$ $e')y-^N6nCi~F1u%͙msOFX9TV ܒ_Qr *՛|n 444kOdU2_[T 06F:ZҺ\3:χireS955fby.a%˒ bR,KC\dorUY2Uqk_xu)TYڟ'ӥJ*-#k tR`|;5LiV2R&#[#NxxmvWa| BQMY9%j)nbJ7ߛY[|2)h퐧Tn+NiT#=Į;$m.wK[] ɓYC{i]TwUdž@&][s_+' z/%d2%ĀTQU/fZRTiKoρȗLg8 YSӭ*ԥFqMub[^_yRZn2=iթFjCQfn1E8m?ӣ<7B; !yʷRV#upꋴN ~n}hEג9=GciK*|Ylvh98yulЭݍ1y9tI}H %[I$l f@!NDjTa;-lXCg(TVHI._v3y pJV~gc3 dT*77QwP먬2,9cҚ5jxz<qR;S3~>¥ϠbC+/ZGv)Gm|nԧ;Uq_.hJ^+>yO4gAeti-ԟ Y~.Lv%&'mF %((]ޫR/~s-/(ͨNU9l'5-.=3,(pXcYY@ISq[iodA:Ń0lOWƲĵ. B7sలb5]#)pe 'ך]Ur]Z|Uյ)UdӬwO,&{*R n\:"*XL,$[ASv-i4UimhݬQY>A9&4{ 7%,cB6ck>Q9䴡.V*椶F[7]f7^] S9ԥN #m5r%(Iڜ4Myvt{ԃFa^¶ڧ#yYzg#;aӷ"sTU ZSzW:P|~67JKGiPXljԊgr)aX$j7dlX•5GxPpozSZOKB1aR vĶYX57?l8S]äauĮĘ'.r4d,HR# '"e]FZPJsMJQ>Xr)+?羼˩Z)TN x8_5Ux@Q͓XX`.X"jM%rIrǽg&$M.1~czL6bp:KviP+ W Yo1)E]Q*Gg$xjBni;veZ+z[tk֖bk+qKnޣVUuF3fߺ>嬟܉+3=sM׺!\d~K&6ր .Ta)~Tz%BnUm=-eWizQvzt?T^L)|oU+y\3 v@@|[IQRwd!$7gfkqqmRr^r%'8EzREȺ~Mue}e9vr4W8UJiK2WZhJ/&QNUU TjUk s|5h78bU)Ԫ\ ?P(j5KktnoAԖO–qpqT3FVUj.o\ʐJ+jV˧t엢9rRhiYmGq$"o~D̙Fiqv!M/ZE<=(=[؍廔m"H.b\ήey?9W%ZkoΣbNVJykJ=0)}G>ov+ɶyjF?LJ +`khƜ~ xu%^5m{;FU!NWqY*F^RnBVVL< YB I PV*ғ_hK$7m^ZVoCwWa{bӗ,m,+sLBRd}/_U)S <߇cu[,ԑP{F OgXepv6JV/ڿ2Irha0m\/zʌTYq<;ƣi? 819 <ٙIZѹ4Ƭ ͦJ*+d 9oB.a~ O Y .(j,!&ډ' ;{ \#r8JV&I2pbA ثa8. ؇'ج.x.Rw"Z)O%&\ZsGU*Zu=ї'ZZw&ZTgJSxmS~Ҷ"M|'q@."iY<#X,*m2WbwgZG]_Ɓ8VGo+kg>%a>J^f׶sKe+u\eZgH%q4{R9s\/ܻk"R6o-ci}iqkz7Q[7 MW_ 0J]gc庼hNH׏ W^EL]Q9KAlގ[*IKVmB\NtJ1Sy6jakI fFtuӽ3Y¾:t%Oc4h y& CLft-'V^ƢA*جrnt*i?T >ohF(ur+Js$ۀ}5 #!R@ .-}ɢԝo9~joc)Ais^!4&o{[8Ẅ́3 TWT;'ݤtׇ*_Lo҄v囎wJ4JWk `IK-HxP De QNԂGqEXӯ:귦޴Tz;ҏ0Ԋg*40XP{MvzuFٷi-VqD($Y+!;bR}xo؄˷,)|QIhA+b@N7rc@.+ !!{=`"[jT89I4kO7Pv|\۝eLV.Eo<.{_P^a]jtmͯdsnUF1?Ծcs}no#8HD Esl8J FǫF/ FTϻG+U<.-Ze9*q?'XQO{Iwk78Z[Ș>U!UF$xr=CYZH#jCY1jA]H0#̕تJٕW]EEJυl94J)ɘ3W9+3N8U!ky`p7a>}ыv+}1gi??ux|r/lOV |k+ o @8["hy 9I(?&4YAJ 2jk1xDTOR6 K"Xh~'IT;懮ZJн!vMBx7:]*si:n_SBs}<GvjϷ O~?ZQNgS~2ێ9`hWd>#rҧ+E.䦟 Qy>*u%tu*IF**yNy y'SGXC3fOI5z)dz8`gzGvk0 4;q4Ӹh8mGPMOO JNb_u}y<#iንJxUyMZx\kOna4Gэ6]A(~増bPrRLl[k84iVزqmK-g[28H>\X+ܼ̅TP/-"O$>Ș!)YI!$`rc-nYO ygu PQ43Vj7(j_Z`2?-jKIwwQ ޫ)Tf8eb$7^VoR5`Q⬫Ufܤ]s+b}M RCɔҔ[|47w6ѡ.ݑɺ`< IB8+W%nl:+&yQN5hoJ~{Y\Ϙu5\߈U_ƒR@Q%X _Xj>Ҹ\]9B4#.)ʍYF~:.-({!/q)&|X}kK&8E^ˁqݑ'  @9ֽ<&y;yp-PNO,ziuZ>UO&֕I#X r?b&$]>6JOo+X@$Q ]!pKؖǑܥIE[5FgK,1U5oZud+ޔW~c?QzQF 5cuuWQj,VgZnRYA?gOӧl]]%)_ Dz cɇ2P\H"\Hl/_ v7%JxF-rws{S~앙?&8Mܽ_ݫv6#2zSt[=NVtoE}Cx/-Azͩ쵑CAH%'QM@X>}'l*ڵJ '͙5U86QWf5%W~0MkwWq^&պY4 r'Z]=;:Š8&@?oDVU@CwIn"6\?M%V\Y11K ~golW|miTޛvK]I[9#7I}{INe}ONʹNcwRHҖ>-*qK]{ZݦzkZSܡשɰnFݣԟ΂qEvTgZMCO(%'/B206\ͻbp r, ̤ү:M;-ؒv_&Br h^j|{/)ӣ^]=+Y۷+9'*2\QKtig(` jxlM:J&vDuWyӣOR1ޘu ׸ztTV)ssO4buMuTHqwV)N^]mq{b7r%72`Y%d#DKqk{/O~ĤFq>{\gTr<5I'{{|yƠf.NMԒM\ڿZ`xJJi65e.Y:e/kq}<Jv@7^QZIY J-^WNkJq^{X10F/j4e88UV)GOU[ӧ>#MoρK TxZ7i7Ͼ^я:4]>| QWL0_^ Oz3QrX=;p&~vXuXY _-..=e0kY$io ?D7f*݈(~*/>M4mNk/V󪵆-~.qa)ba$H)+}N=ZZӮs5Zn~Oo65,mrJb/$r`!/[+yn㵃x#ٜԗhEN亓3՚)lz[Su/,k+z2?jgth݆6]Øwvs|¶kⱕʬ8MӏSܧ/r@kNF8 Ӫ٦Orۋ7+ *Oz+kY4'KЍ֣M5Ds DXغ|u%'rg&( P`ݝrmO5F~sVt|mWrf_ywaͲeOϭ8Жul_a1ҍI|]mڍs-nfs{]ȓO}*[Hqr7$m@]"ȳm#\T"2jTVud4į͑[nّ:}Tqմ6ߋZM9^ZSV^%Qn蘻tqTRgn7@*k<{ q`ջr_H >i+ ^koeNUV؇%663 W.<#Bޥ\}*.ٺf.ki=o}%_,o-Sq.?TxGu=s^3+R ,D%?/skO bzQH۵<޹ۏFri=_t+Ɲ'ZWr7,%^Wc6v4Mww#id܌Q9౔qt]=󮳍yӺ9zђWcXɵ~:8o򹹴궹J]um\ǟme %K:rpm\\$p/1}݋]ЩOwjjB.V2_tN;Zpn.[UOF[[+hOBuZҔq\0u3{2\1jXrN5"8Q>NZIQ'^$JH+%tV ]9;;H~~jzcJbqTݣ@qEI9:e%6CV*Qk\UzJƹg CnZ|"@晢q $Ib%JU>Rc:>dev_wgMԗݛGYu<(.njn]Sw^і.M̹D%#ܞv}>]]q6mܕ5VY$A7&Xr fy*TV1E׺;9~.XV3 GMFۜ[xw?z '5X;TN2I_C):g6pw:7Vђ?7kv9> %;=˸+Q9^*2Z T_-Q)QVOKУ~l'ܬdNzCZRX8:e٭u6֮q\n UIYFvlO$R*JSq^)MTJh{BV$v:9&8<|D:rGy1\b::%MFU/M{ۻygS~ƟuRJ|ҔܡM_rSbkj?<΄m1?6Wwu%R^{#H{Mf^V&JOe:"ԧx~_'W~Tɭ_~P2M+R5Ħɵ޿d~gB>Lܻ͹ng>a7WS֛+|pʮ⴦,F֌iGDɮ <|=VjIlU䔟7VR,dac,ϾsZ:lN`aKERtdA/I`s\>&t_!Qas S Z5({.]E O? J;c݊[c{9YY!,!<$ Dw \zIRϤ;գZxIҚбZe|AJ.^UizTqU3 Ut~KWnTRf`0ib)R*I9z{|p;I,< b[]n$r<$АI7bFy\/+ Sؒț[j/_,V`iڇ0+e$R<׺Xʬ*SfVΗds>]S~|i W^f]''%r< KWE wψ*(KO#Fg=?\'tJӍ\(feksa56:赜?gwou)6Ӷ{\0ia1:ν(Τ'jmN>TjF5()JO`^(K0X!,R@ KxI%J'c$רSΖ%I (+'~l eK=45jo:Lw4F:|}O$R K[ qX %w{L^a`+~I]EDB^jcV/ p~PJ̭frqU("ut֎ԵJ˵T]IGZnmUa񫺔bm/:KX:6O'MK5QܯnI;l%`!<`{L 2$,^7mrSe9F+2ztFRVMJ#NR})ʵxƚjޤ9zN b]٪U''߽ϳPs~ Vky*1>vq=RWM=i90Cw-\% Y(I8IrK߱x8{S>h|?w9֟oٲѴy4S ߒCGNM:SSNZڹOxԕ6|.CXiZ0U:m v.ku:xʮ8:jWs7X44*AIwO'}-UjKs.6D( Y{l$LpV+!/>m.Qc` [ 8ktv(%u%R!>. =XEEr[vǞe[81Xr&.U]|pi׺ˣ|E'(\1Ecs*\\ Jx%42'Pqk}r_T}'uK:[)yJjMp|W4i|mn#Jl]H̫fL."\EUrM>g\7c+*UZIٜSTÛ?HV0cF>g7.,U,$3w2>rTN\",EOwqܐN,,/cԌ&U^Rd~=fTsj4 Ԫѫ4<9r:!Z'1O-juߋwmgjI:ߧQ^lܷB߭~X2:\1Qd㸌g]=AgOi[1b/%Z|N^D=l)|G Z{SL؏]fX KXmVЎ# 5(˔<͊*u)۔lGUޮkW|JV Vw+Rǹ5(F<K-49Ԗ_௉Ω2URt-2j#_3VSQIߖQwv Mo'Yե:"..ĒAM>' Hq {M}ǔRdNm-z:j_ovpc,Q+F1rnNݏ?Hu\nIT^poLB>]̷*zv]c:*MN9Uk6KIuߣ}ʸ۷bS%Gt~H%vI$7p@-ܨĥM'ܷ~Mcu,n *UiII8ױЋįߓ9%(\dz-YP4%,NqeEWUu^f9M9W{.Ro'=7MԽ3K;ʕM(r_u־(_ҮΫ/O8oFOw%4UȀr DһԛJ|>=ɲ dKlGnه{jA$ex>SmF(&<>Eo[I~VxdjwTcV,M晅[hMX(A{߻'hݿk#ޝ 4y}Ͳ{#䂛y<@$ DYw-}#.?z7%Xpm~>:'Θ3ž{{KZzkb%4vqj}IF֛AޛEwYz>W5ͱy6;^uj֓swt;I裗%)TPIDʓVVBYI>ĒVD"%/@.؟ |z-)zR5=zu%FJQor5}W,j)])x<~aB*Rd[Y|&[*-K'QN;~.͛siq^e/4?=]M&TM<0o1$w'b  ] ;NO`J&*[j+r̳^0Vcpyn7(Q9Y4ͷ]w~?U)doDe8?zJ|[]ZS>;ӺU]ZQǧ1}&OFvnK*~9-))Urۿ-r89 4V#P娒Wcq)RVɗкq$ʳ\fQםԤenQB`xRѓvɢcxܣKNZSRFWIժ鵔0zΉCX\g[UIK|>xk71ҊQ{bsg_ݭ']jTX>N w]wBTd @ |䦣"fO9f\jK._#F͸ݿbaU*:(o/L6:z*f"ԥ`|+.rfSEҫ*k7LcHRNQ}42̱.7^ujՓ'~ٖ36έjrnNΣn_9Uܟ{t[}"I~߸U==&ni7bLI!;&\2{w&rv+g噮7(`ΕjRSn=wu&"zQJ3olWn~M SieY7*`kΕZRS+]3j,&auEӨW>$߈lӚ)%{Ev5=Rk|sNi_S8+uQŹ\iF2a:UiIJ..݌֏Uj)'0֏oPt.|3ՇN[ܓX):q#|o9[ʰvm1p\m6SJy:jSN.tbSt' *h|?3QR_E6s+&n} z*5i蚚#rFoXLӂ 7!SR3ܗ^媐~dn[G/ʰUxRJnSvVFuSžMG*ezۥFrgc}аc;wT_%z|@h]5ԋ{E3fڸmyխZNMWkbslm\~:ի'');΂n9>վ'ݴ=HS_rN.wbVܝهrry3..2@$wwkefU"+42aRfFm}lU*VrNk]h˛OH<CRVY=?]F]G?6nn ͮ;ry6L' ԧ&vnNDNTrZUG)M{bt!qq躶Ru,a w[ c/|po2o}XJnMfCNP˨8!N]dS @ EVP A1NMK߱Ri<˱M-Rwkie 6 ۔|_>S"4?O1<]yVF/TTiTzu3?؉m:qonn>MW[JvAo}9ѵ/Zvd޿oiJ󥓴kGz؅ߘ$ܸs;|OӨ+EoŽOm=5>8O.r{~ `9d  9+ @Srʥg-)oW*@e97,V+]E-N<ćaҿ\ kw7)FoesdY5ReVX)'ufyEܧ2NWy Fm(N5&}J.QӍsѹ 775:iΥe$ StDr `BEN99w%b2’|0x!TH(ܟ$qydVb*4 ,h((ݝ5erЌk-$|_Q:A[9N)K.nSr>+ ٪TgfX.Sօ*TݮTodXCʢJQzvv0QIk}– ndARsܦ߿uLҶuZVx{̽6ڸn"uU-9Cf)N&tXAmHwDj+X`}=]_ͫknv8&r)rNH7om!9^}iTcT tm)auܣ,%kt[ -NYmmm':G?vaN'=f6ըP?_tQ^Us|)q*SSQڋkmJ<\dɰ~r~ф-6o_TƲ[̵΅ֳ{1]=8v ೜2R(Vh+?ܜe ^/+uc,iաSeX@&kSER ))r&JEUL;s54x YKEJ䬞=|󔧒@g2Wܝ<Ĥ'RGOe9u*1%FrIɥK^u?H(4)ֆ4f/a^xȨXCgTg*ӷh,RW͛9u<-ܳh= :{lR`浳]' :v559kVcZnnq>׋hz6t *O]EJ;w-Ow#d^D_"VtEav e *%\RMb亞Ոa_+ْUQ}g%$J9Sڒ|2w-.Ŕ{ǹ2}ZT6l7(3La8ANY/.<>%{eJRjܫ{3'gٴ. F: Qi^29R|{>*^ qosԫͱmњc2e>0WxwBOv#s%Yq7-ԛkݕ^sN14 mx|ORu<:' W^ք#m]f2FRR 2T(lpȳn F2 øX?$2$$F@,d"fL_˅R\>*3 Ԗc>йl&uj%Ip|wzwIK &QpfQpM=W[7ތ\5{Qx=ybgTjBo*qPk֯[6NR^k&y4M%~Ψj:t3*>G$`^ukW7ykTQfq_d|UIw{Qe/Kn:ZiJW'#zO%䇵1&5rXf_Wd/W(tFpJCwv,$Wɲ Xr@`.Bv`.7rSi w c0r߆T/SL:nЙwyv&5=pkUyXйڑMS;uGqо#:ȰB[w){W"Oőf^n nt^4`am[?/]w_1%b+TIn͜ݓU]Stfg-r=JuC׍;tbz/-EJwg9;أ{둳PQI*BKw>fv}YE's" W*JC)<{ IIVRl~墜yAOAث\ʣJXaV~fR 4Ρ;~)/qt?W5n9s m۶ܣoit^M}Oݟu-J>*]H'x%L]? oJs2bzS?biv,S_R^O GI-lU}+Aո}Pя@D[G̫|0FkMFG,#-0g EV_vVϠO8̄:x's[mҖ~ym`G_Ҝg3Ҝg3=?֒)=E!_g2=ȟY.93f?93f?֒c7d~ d|Ɛ9Rͳ\•j[5)$y)Ͽq3slu=ZL6:gRQQCݮK(ޯt$"?Ҕ&(;}]a{{EMok* ~]c cuέ +ߗxmԷC'ize 6 xM 3_Lgzuocգ[nݴf Xjj뭲[Ku:i?٭c1U<^&iZrrp);)>ӕE\(YŴ)rس%B茒-JWv YSe]IX4 Apython-telegram-bot-21.1.1/tests/data/telegram.midi000066400000000000000000000074461460724040100222250ustar00rootroot00000000000000GIF87aX___???߿,XI8ͻ`(dihlp,tmx|pH,Ȥrl:ШtJZجvzxL.zn|N~ H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKN=7ثw:{9q>˛DŽLտ x (!( x z>BH vTa~az#^!u%H/b +xHDf`dH&y-6dP(T9Sj)NzIك`p0.jfqΙCk0m9@z g~9@oIQ:("z9P@{`}ꩩ@zz0 %d@"p "p~V+AA@zm m0. w*ʛ| * ¬k-NpJWlq {WwL+{e j*r+KȮ/3 K@@ՂlA=h I3;HhZ& BIկbݡ[جA>o Asm`2-68w5W/ݴOs?u.{ȸiݱM =ch ]88;z 2&뮺 o | wGp />do z"hZPd7HlE%[V*M&zi Q-0_PyʗG-;0kܤ];n0%9( peL5E@JTALYVn-e8B@\ N4W -O (Ӌ;s+ia@'A7QtgP/J+y>3*8ZUT+9uk)R E J?FvGFiʠs:HsMW9U[.rA Lm53@B=$Աh/(*+]] ,3O * *d' Ux#gjLUecj)8ϺU]iZgZv>]l*0ڸ6j)9\^-&ApqZ*6(+ 2F60 VS2 /Y)։_b?*J;XOVTud@d٥iK㢀.׾"_~tX? nPatvucM>8 S4a:궼D ^6:&Y:pl?U|!tloy\G0kw p{nՌMf1'63O&YpIF+&MJ7Ҫ0 DO@~ {%ɂNMa?eX{Vp>m*2/F> U,5[C+]fNeKfw{>{d΍`GG[әj%~/au`&)Z}:`~mw{.UpS# 9vllIX ҅;! =X@w(:"#Al&ňjN9huzۼ8Uh@~OTKwykT{過\7^r;.0m j/ D߈]A7n\ LN+ ۂ;} ?8/m:\ l~ZȖsgUA|2y197%K6=) 'e4T07c8mpgux:zwfSfB_?9KWffWJ%gHRHTWjzrx4>'3y18h CkA9R8-t'l808:0TV(oPd7kGz>k/hseHc/x`"yX>XPxH XeHT+.؆7@<]t~G ȈphtI`ox>XN/rXOt((}8 &(+x(2'sC*5>xW9ͨ腡(=| 9h؇>MgW-?G5LPZhI; EȀD gSLj<8FU89J sypˆ2lr;?dQX8+L ҐNf9 0'9*Qu4(7@(iqq_5)3.鉷ؓ 2NJ"<2rx=5);#2%3dtUɂɈ0.[9_]# ԃR.1I9?oigEB8)L3xezm2}yvBv| eC1x#y߱1&1sPE4Y+ lw ,c/)x`wb,Lq!C[d>9&6k!pza"&Dwgz-?٧H`UymxKJv\w>$F,y,v: 8yl:Q/EBAC,rs"ګ:ZzȚʺڬ:Zzؚںڭ:Zz蚮꺮ڮ:Zzگ;[{ ۰;[{۱ ";$[&{(*,۲.02;4 ;python-telegram-bot-21.1.1/tests/data/telegram.mp3000066400000000000000000003600501460724040100217730ustar00rootroot00000000000000ID3TXXXSoftwareLavf56.1.0Info  "$(*,.2468<>@DFHJNPRTXZ\^bdfjlnptvxz~9LAME3.99r4$@[~Ʊ=Cݧ4?C A@[/Жa#JRӸ58C"2X-4@$$|]F_ƚӜ4/"/SF˜r7h陖]eN!B!$e$l%D4pPhGIǂ ̋ lͥSL̄Bь `0L5CA.Y➤%.BFhPcpgfjbFoņz044ϛY$ zrI1/FoHQlT(!23౳IAh70);[ j}BycBh7ҥFz}$t Vaa Ȫ`,2od<]3H,\sdP̲@̖kN Zf4!(x`4ײ@zP0fi&x *!AE hpZvvc@, %R}`IB"PT O& 'aD@`b(_ G0 L" LPR"aXT(` FCf 3@1F&eaO0fBVtBH.)pPCVЩ0O L>+0eS FEAe%ɠƄ!*4b|(2`&3r2T\<-2y&]B%fY,NVT`UDURڏTY8Q}-8o6ٖCzU]"PDꌀEJF ߶<,:/*P;{ k<=Ds3f&B@*8! `hqxFR* * $$ދs\ѥ1̥Q&(FjAa|[#r`8ٓ0YP@4Ssс$EP3@Â&BRa4I0 0 "c\^S KSL,13A )L1@1 јf)0,0j40cN(5A C4`2`BcKjU @0*怠"G̾09`I\c3) k@9eEF^+\F`fЛ ayf(ovD+X|bх!w .4̑RfsFU`O$fI$\#!LF2YӋ 7TxqOIg(avtIu0$KP%Zd ǃ&r@FE dL!LaEzf f2 `@50 ɉ D$1l s0()7,5.}:Qd"( Yd1`f XI0LA9 {ep0?0C!̵$ J ZZ"@2 ZF %LkgKbBI a<$vEh|Q M"wlʐee \ bD@Yj@/ # /!`f %5S*BDF\$$DT"Q<D U xDN eT%  W `Hǐ 04p&02()IǏfFhd23b7D!Xေ:A9ÁTB!!!f8e`35853WW3BAw2 6S;C/w)q‡q񓅁L9HВ>=APqs ,0,!ZJ4*L Cy4 c1,"s  X3xơ X  %8@ 0V#Ë'0# 10B ,$7΍Q`HZϛ/7ksǠ܁˘4mX akF];a1BěxᄍQ@TBRBG QcM0biӇK4$S4&G) :3Ǖ1ˆ$X0(<,@$ĉSP nHɍLI#OxĀ RƮUAF'a(0 0BE TaT e Ǧ\[tDh#F6gf4Vf&F`"n`f^`$e > KHlS4(p<@ Mvlb'6dƞ`&vg(N2 %o4Z2[0vp130. .0jSP g8XNэ<,ɗ=g6rG'*hnjCㆎ2`hZ2vajj&pF`&u(Y $|I``Ҍ̈́ YpF05C<Υ4L[㫤3TM{c4L$>f̓#rl7FL^}8.uܠFiOxK Zݣ?ݟV5bb-RzIbW?o;CMN;vtFmY %½X5 R,F(i2d4@P@1X8 _Q!'8 aQQyWbb1tа!&Sreէh k.fkd:kZdfc<2SR 0.1"#0p1O1÷8pp͑r"Rj`t G ޭ1\L  d,0]aXmY@gA&H/e )f2&6$::8MInInTZdYl[hj.j s+49w D 鉽ʼDZs A1do F/ O`*\6 bhM0 hЄM#@D t$ Ȓ D l#JIذ|L\CEGMf9-3I1HH-osO`b8S, >o NE6)@ 4)"4lNaDC\ics-bsKMz1FpL~4ۊ#*MhAtNh~ecYd4ɶN&rF `<0JTĆC 0 :ị&"khbFS"RqR*a$\_d1ɁKy|MtytRHdZk1v]+"y.̷뺌TQգ4b6^ߩTrj;6x{!%߀)Ttʝ?iUL:1eHqX%}ݓ\)VmXZU!DMy_D;@ &Xd  VNL ")) Y(Ӈ`)2y( )񃌦#Qc1U@YTebkX dT "`QIF|xf1Nf7R.`Sa&H&eT5XsFD0,0D)O21 iay&81,fPɣ^7bAN (c)4g@teك̀,3+/4 P _S=EqE&"dV&ULo?\h EaX0KcΝw^}G>C*JzvU*})m^‚@DJοP8n- ~۬c7F ŭ A!0%E-4)aK$↖X*6r.9Ib` w$^Y( k lj&^Yzq|CqK3JPCND4WSQ3[#s m1 (0R3Ks0 130#00d3 ب6¼*Os1cf8rMi2x֛p1\SVA `1b8̬7 w2hB#M2L c`M_3|ʢsK L>V0LC&̊5$BLU2c32@b Lt? , [2ʤE٧$$01€#@ @c1@ x$%# H`Z|U8,LxdS%`1C"rObqbƔå^QXb+>R_܀Vh;شoT:I22z4 vw3` 0#㩊F<5l1F,jmCA&2l!q "L``4+7 Z1??3C ԓ99f Z((H(p8l`0iJ1p֖Px|6$0`4@8sK !G!41+K?CCq38c S0=v!21c3V30&35R04B0+s%P[01_ # A 1Z^7V$fs,s1ǧ]&ovQ2'%eNFp"1x0Cg4rfT$jSɝrjz2d0 A΃ 4\0P99̸2(ڭC }10Т!~ 2hsC de5,7c[T13{#<̌A4ɜԢ(d!p@A@v0T3E$CHxSq!c+>͊FEt˴%ۃ?tTؔa79f̪IKyRJ݉d; .p soGH f8C 5daaI9 ̴0ΒcJ4F8DcE(1h# %0hHfD)YZbrG1:@cIH\il%5374#zG<â|1;6]7#u1|`5/6PQq+1kObN'C3C9E)"X,CL4AʂDY  $hBӔQ!1n1}kXCFʡ9O͸vjS5Ӷ~396%ISc2| $5eU[No!%+-;TMf 0; "Y(S_Q~@cBMHkE0qe-p`H$@bA$1a75=3M8 2T23'72 0X!t1~$#1% p1#P1q+і 1P Q';1a#(\gٸFQ M%hŠ=29q L,13(@X3r顱 |Sh| 3԰Y$y 0; \B EA0B@F`Bfn1"v4r3)/ss݅f8QF2UmI(s(T08g¹F:?jQe;PdaJƁDD"*S4`Ȥ4 MJ'0(fs6/jG0rc`(d4ʣ BO0<3= Jr3C9.@&!\b) vH#0#A@'DC j˾}Dc -$(,rD$Hmij-OCzRAұF:@"@ڝ35k.lv$2g7nW;?(w yýjʡN"r؁:(iAfM8QmA8_Qƀ Z7= YH- j]Q}ICCn" ]2K0(¾1=JT0gH"2 ^D@2$M2s 0#2 0Sq1B SA0SF 5t#\L47c8W z9#$Nlz5Kc.6s2xhBL1  \` `afa,l!U!q>ƢGHg`  ɝn aՁ@`Ѽj#C. jA,Bfx$`Л#,8$ p(1^ 9^6BS̼F x1SbF(bԃw ˲ 4ܯj3vϴ6wsD#1)\~>8X E pI0Ĥ訦Xs-UU 0 LRF(bn".%XHCZpff+'}<AC I6C0:PDP%Я p?_L$L%0mm ?Lp2 %qOa=]fqpL`F Bz|3db1$}8 D3?-EC.BG2S̝ 80px2@haf) j!F67h2hQK'd1fRp2lӆg3YfFdYK#Pm0ZfA# fk (`ADA!h  b5QBTVPELK`7,]Mu\9a7=nweX>V_}9QIRjԳWdKEx&Q7+|鴙si 5V23V8 p 1!S39"PA $ȆNPt$HB| Ch+@V5}'Ә552W<1u5;c50g6Es(15"2s 2+d1 10 0U3!1#.7l3c7K\#h 20( Lj:0`@s8S6Hد6NaS5, %-{7(d1*3{̪n7وgk ^2y@͇ J4 TE% 3t@`dtC4UL6ii%TUr;9˷{/* nKy7HO1ݬlIu-i\61T@ âtE;޴ $qS!ERuP,DPLP'ܴ`T2u߲ڱ'!@ͅa<y`L񅠬 -bm!bun#1q 1£CFCg5hxǠS#F.fЉ̦+ c1˦#%pej@m7!>N01L;Ba* I IA6ђ̈(`<@Ǧ(c`3-2ْCQ\9ph `Px'yڙ|@O)8|4aو=REGaQb&Z@ŬPNnz'3=RWԳneo 0KACGCOo&E1TM{(y1f-},DRJݺ8w-]:T@3%fp:F΂0EB7C*P| nz213 3PSC3RSk{bф0lkJjm>iV&hj( _* QJI0(X. h!= A@euA`Bb0`P\42B*&&S*L16sU>51:bT1A3%r1K S#P}1P \5!<?2y,nO@S h f`Й@rMI^gAfр7DR"4]Pr#e[I ghq`fZcHi`E+c&\ qS-Rt~jT*ͻ&jƐ%mGF$3 obrLJG_V4KIhlj$ۘڰ(9 Zޏ졂daR+BS 44Iӵ2/swf8$C3RS20*S 3cR тo!(ZiH  iXLKEwd}=]fqXƌ3̉l|L0, P%ˆ0  <ĔAsOG mgLxz4/N 65(ģ 18Ld'f/`vlb i WFs"!LkCLHt&8huiAf -0c#V$,sSf)f邜,hCf5`uA@fL=) 44)|c ̦F2@ AF_0QpCKe#] p w^ ,! b0b廲Iٕ9 !(S/G~](N~*]'=JI(m˳RgSܮ3ԶIH>貶:%TiȤ2DwKVkV[#= V, D t$/1#61`da#BH*khI|Qj (Z9 4MS LD$P`ѐE-T dKL0p0,1$ P0p0Hc0 C`3*vYG╠It:"wltP] 3(a5ұ@@Q%xIʬ")eAņ2q)% 0`QN \G `0 Q́Oss=A0Rp:#P xL4$~0é`WAwh4$߳ӨXX543Dw2I20701M@81ܣ$L]FA ADaˑcyf9j*R]`R 86he#Ifg &lX5d0%20H042L2r#c2`Y2@svV ?2-3a30À 0`Pq Wjx" XHXւAix 7ӖFﮩрJ˩^1%bQs(QNn=ݠR#f'F%(# DJiL2s+:tb<鵩{ZsҖ:K=@"cp1O᜷=BFnc Bd*WXp1b-q s90!SjW3;1%*2!af2\s1\8p״GL6I1Lx8c񌗲_+zOi}2["@nK\$Vwu#";F5Vm_F*E9apc>& .f f}.䃴Osjǝ=]fqff~4F0,& LfmiFxMf &b: :y9šqaKfZц#T|\b? wy=fI)=\u'L0h"94f6ln჏vM L048ƨ&cS8,S@ 7g! &5Fi4JR"̸LL"2a 11@ P( IMj l#χC_#" _VIPXtmi,5<6W3P38Gc>g4qcEg7Ӊ5W#A)A☚B„ n@aF t¡܄ 02[] vod23! 5#C#@0p Qa"X$ .!0S%1+E!) $%%K-hHTlxu:`]*iȥ^W!SGM)0bpw FR%w"fN(@h.%7Nek1*/tV0"rA q%@@ow-peU4t!F 7ʖdۆҚεaJuwaYb\C K,TPG<N,7LS#4r5Ïumɰeh%3(&`:ML߃0ˀ^RS(biN4ÌHd1+Ndfy!oÁ1BLC-zW( D9aA+iJ ,KWB|R;_niM>>KP$p浜VCJp0aŰdl50Q€+F߈T p7*<O>Wg-j.>%T:\3ꮦPњL~? Ir X2 -@LooQGm f8TqB`g{W09@Őg/Ƥ; ?޵#sY5^7 Ѹ 6<ΰ3!"Ա72*" LD3lA1K Ce9Ґc$MJe5:s)N6l`q鱫4AD&lP֠sM%Z6)8c lS[ 6:@E>2Ho̺0$PJg0Hg72Y-De9kuT Pp-`s*\rdpQ$@'|3@3'ё5ʉ WiuڤE,3Y<Ч4Eщ #9J-3J)Lv͊GWZ{kKL)f(ԞZ=[iL8o"QƘ֢觅ARj%[*0%]B!!`i+.7I*L(h(@ÁHjJ-3tQ(]S H 4.S3 ,3TXC$x3t6^ sAUGLsSJJs)pIUGICs;IBcstDx_0YiF3sa3}aq#8?5#*B*>p8e G@mZT01`S/!01QC"0rB'9n/Baw_:>,/QAɎpC&?~6"n4ϕ85Tbb%&f1z<=-Z:ѦP;3s3d@ a/sߤ^ZU1Jp`H0\b&;F2٣h;pz unFn M1*k\s]ua䥆Lj5t)"`@}.b!_sɏ!󙯑X:UsmǪ=pn"Y fiLѻJH))G&٠0Ѭi  !(oiF4444H8)5zv4̣250. L)7"p&þDvuCI[.d8\Pva4xBdm3V 6b}&H1 C j5A8C28My2HH /@̀2T#, <0r|: %4 10`L(Ɋ&!( T0H< vQ牢]k.'2SDgbbݗ+vw^fzw8b&`k3;%yDUY^u_(])JWJ`*@`pX `H NHL`)h-NXB j D!H@If54R$! yU c,ƽAM]L Dʐ! T= X848hL?xU|X\GNE$ѷ3T!f;jبQV `X`wQ~p f΁;5נSiH'lo2qdbډT'uY}@qiՈG_ `>AZqIgƢAHaI&^qdpT p@F8bс:z3&sL56*C)L*:*8c @8Z`B%Ln/xl  aY!PX`@Al @1p @8aJJ F/l&]o( X8oD4 5 F>Y}9"JI'vv_ nNauW';%M`Wr}}i*@?|J wks'Yj%QdCk1{J" '9Pa Uf ``(xZX2hd&@5*< f& x\|̪J8NL-DDNTx|4䙌ƌH@yAr@.aD}mTʐWLd( ̘(XLKhLlF@hƀot<,d , < ^B o@ƨVLl@YOۄ\8 d @PT*&ab6b"3Xad(a``V` P0#(^`A&<2`,)f1`aD `` a"j`!@`0HPpB^@\`A`:`*,  H@\`4`+Z8aygW;w)̷uw~]+gu5no˨7'?"# 9ٽa SϳDkRGaEr/)<81Yrq461tZ.a0d"sz13,2 s0QC10 0K 3*Q1cj#gCjgc4X;cMQ`DX465"2(2;2-0b4,D11p32t@3H2$3eX3<2T550K4123h1榀&2-9TpLbD f7F</FSF[UN#x)1xi31dS% 0 * a @ёB@Ȱ] T,   0sEJBHsox~zwY5ܵkw,usf_boE)򧚽^R䢚W1 z%tTkLAME3.99.30G2j0h,1#3cKc1F20F0z0c00b90?0T@^0>0@;000;1DE0Ck6$0s;16LI9 bi ɡ%^a+b9Gq'AB9` R*y2381D)393P7v2&,S&NơfU&9?!f $8ˡ ApV4LUMʏ̅<<@A#:2ă* @c'ق0`F( v4vuko_3]XW-_IzghN^_RSzƃ]q="EzX63itH J.Mb,NW"^F#KS7' sX&HSxt>C$P#P##X3(`,). N$Xf%f hÕfƷfKf Tρ80`ò\a@T1`^1u0Ș5:3.5e4A^1660<2" 6a6?4." " C`̓ ! ~ Lr " 6U$1Caa$`lap.aahd\ 4R d@af@  I>fd&.q 4G00|`F9gV*Nr?4cgDaIaq*;I(=:LAME3.99.3UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU12G:\S;0È3:83R!1Ӆ0SÙ0)Y0M1Fss"01cA 9'-8B__ 1#,p3@-#01? C0k)07Q[4Mr0 "1# ж1# .0 0010^SQ1!-!1&6q}4v듫mCM^=aXS~jUJS@H"RxH@8.f8q 50(M^Ͱ+L\6 cGHCJCȃ\xG  haB c0 !`&B I/k8?,{~9y5{Z+gS+΃i q)8h]>u1m :6V61lM0ۀ0(0P0@0,0Z1=1901͇1s0 &6(TO1v83aG2#c710F"7\1d1sP0=0 3 !P*]@`L$QD"`i)y鱱yёIK A%шpd!tLF29Q%l42|pp0T$6dΤ<$f Z  4cB!`cx` `>cQ`"K ("m>;úw9?+7jJk/Ϲs%İی i <wco䥖$te7J6hyĽaq<Υ4$tVDǽ T=dl<> 5T= &m dLÕƄ :CpM o˜IPi DlL!2Cc»0t# N1c\3 9s1: C00S(0ls 0 q/0 0p3J1b(1 1 Q3>-32}?&0(#Tx2-@1S_L&2BF1 `!!2p |`j6h&3A, dBP9H#L|0X uJ`!,Lbz ' @ L8!i%v\y\{WZ1mΥ-rzi,flS>A݃!Aqq3 ·C1T93C5mLn$` hxHb4?Mc45HSV:.>3wm9:2b sLQ404s9C0'36137*1P$010#0"9cH@ɋ .I;Y~ iY?thifHa̬54uMք2dk6L 94Ԑ|4 $L1C?" $,.|Rt%03DϰfF@*`rHę2]X\zok;[t=+Ur^Rޭ_U9Ks^;R3IEgQ|2Y<=52LAME3.99.3UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU1IB?ɠ4(80|p"GM K0`gAL|?-kzw=ka>s|9[;[[7,_Ύz;ֵ;Z1t#>63 0O1Ƌ2l1q0D1Bu10ń00}0:0y@x2<S2:7##0ۓYU6rF@RF23Cn1s0Y1c)<qܙ&ǔ8LO¼>dF(L4xF` a Gb$f=`7&ĺbfA f]2:e:d*_KbcN+f5@\rE<key+g $uJ&cha:k7f5d& dY\&Y:  | 18( T& LM z)Ir$X/08u d^!ocgYos[3u\y*l>5~TӗpYZj7RY^،i"yQE01E+s)|~h  #Ȣ3he3a3Gc+,AS$X#*i@<>Iφ=`u#*U靘$u+a"AAjH:1*10)u.)~/= aXj"H鉡fd P6 Uh(b AH偸0L$ 8Id gJ˸A* l^LH2M2` D& Lj#nZB[3Tqi!%Yaih IɞE1%SC `;1:#tY0<'0K0OP Axo$*ԫ-M_ox3w-x˛}K1T{Kݠ䢞XK:գrƢ av nrkE򌼍:\LAME3.99.3L♟ml6ݙMíR?Y`(ؚn3`N1̆Ik|nc7~a+&ك)&bF@&A T -F|BD  1@ «Җd±,aks,U޵k9+?YXa[Úwrt(%.G9,{[|˔ ܽwt\wKKI` &ed9jF1cLF}aH)cva)eY[a.fxYFd^9Ecf `)Rc2 F babIsk?fd˲ixhHs1q\lr`b$g p4:*-*#M,= X+P EZ'$P & \ףHix0! \߬?(z%HKw9f2KvaݭxjVWo8͜-+ab=@ <1݃50Gt0A0q0;0m`/`}ck"ff2a&gxgq@wjndYdhqezePP )\MyA N W La&Z&,K L]W 'ātMUuNI dD|tLD0:M(T,_ \B vqkr$ص2lP?,{G`—,kVh؁l`>r `8yJ`{$xbBjf$d&Ka惢f5bdN_ #h 2eiAb4r`0 w0@̆ه@jنPU` Qy8J0L+,t- "` 6J@ Bi( w~x~ޮ{msug *jOLo;veQ=z?ح$,%RKdp-*d %AzLQT VaAy8JSNJ430\E49L4 \W3ʰC 33ɠgSN6,cL)3<0@lV>0*Cf+0A/PP0Es!0,s3 2 3+"3CB4h3#dp6S+sgT2-6S0SW37<$[1#2p<132 pE C!11mj01Ds$0 "r 0$00҇0t00*gN`F @$f!00f#Cp!0`s @0C $0s 0 "I0qhT/m6_{?ú;cow}5,c,*v_#NSTW&o0 ULA2I>Fu1w00 0I@x0 00h`ǂ ha!9Q5 ,<3sM. "5$ŐCO2̬'6Ǡ#$A>L3ؤgW >2 Hibi$OJ<3927b1? C rV24C b4 F4)(e80Ps63%n14'B:3hc'3r92_p1Px'a Ʉh)lnT YmL pwI}EP%1d`ad@`N & `a 040F0Shn%fe^o5[5tWyե<<"b_9E5^\Ү gX#3`%`k&iE-gYX% t!xS6uɮ >~IƝ''PhʑF @n%[AHPс)&q(z x. kh DrV{KnEOao[۠Ʈ3sm5j)9-ˬ`hfQR?LAM5\V6l2ER0)0qA#0q0tA0:@U0O0j$3f00444t5$1&1B62}3@5|\2QL3(*&)5h+ag9bPYu57(̫zΙԑ!)ul ˤq0 Y4!)"'%<Ʃ+i~l)\"A(;Zٷ"<ũXTQ))kЦ R@p%a8 @mpE )PM هFpn [ GN0(A0 =#1284A(.49 @H5U-cb l޿35<=˿}嬰sWqxޯckIM[9j]Ivc/YnF#TRvSQм01,ry4 ِ; !(a'hF1pl&.<!`z& aNza )`AVa 8bh  `fA`0  a" BNaaH%F H`: ta%>a*F$e*dj&bb">a@bJa.,8hle&jfm2&bE`ff>btgc #F(bd\f.f`FxNdHFW`~Flc+`l`e"81s3H& E:ò 4ԧ O7c Z* 51Ȃb遁Z( 4eyH`b"9m4<7.~hY]FQ}ʸu{20{GmB^b4RB>Lj5d @ 4dxh <*LAME3.99.32S0R2&#,j0 #>220& p0N!G0" 0 P0 1 S`2y+s"!1 d&ED / +̇CDB` fa2Gf$ a@F a! bCtfrf<Ƭa%F-ak"?&pm*0gl5gc &lA&@,Lw|bg&hl&jXnXbgrf"Oc@ԯ1Kc0 1U@2p2e g80|o(Brt6B `@ÁhҐh1R  @G A 2l07SʀBYR`j,@Pz.og,xw<̭ڗ䟖RMfHPǫZU<}RʼnP8@X0h`P:J8|&K#4biCWЈ0S0=# 0b@12u>6=!@a0# *0I3`0 0g#0} 3 1Dq0 p#mP06W%00<  p11C$Я0$3b;2$0}2e0s`1 340SV1{s`s 31lUD IC(P$ 8`F.&h?PqΘǙ\=ɦQ  ǍgFcehFbh`gvbacf0haȼ`pz`@;$&)#"`pa^ah`弦DMCDL^_kgs:p1<3/٫vhGV˔cZn8j~&v]ۻIR̪%8J'pŠLAME3.99.3UUUUUUUUUUUUU4}'5`3q1GC 0' 0+c `0c]0)30g1%0$s2c 0Ecp[1 P1CP0C a23`1s0S3)C1u!R0B 2-s3@p5I E252SD0!Zn/9\n-40C7D+VPm9aI3\d413<'2* 213SVb2!"#2C)4 $3q$1A#$6 c\zo#,Ac5ZC"mB/҂a1d2b8`T!2F;GPbMx$GI Fgl1閙gE⭚_l VffϟO1 J (BX~wbQ'pftj&Fl3F3*&$A8TI3l0h109̾ Y980000 0@$#L2 Vt Y(tIoWL13n01ê6 ;b0p=( G F0 )"z2:OR1C0"S 2 p0c 0@>1 0!Ac2 2%3@36:'CZ2J 0P0"12 p/0 s10CЅ1k&Q 3.(r1034S,B1(R!]0sS@|5\&!@6sY:#F3ys=4"3:A2NA[0SQ`5cs*00EG$3[g)R3B4kS04<">dnbmgcKUL!> f79 - *`qHƱxLɂ 8( 9 ~- L%PT8NwyN \68̙ť{w|F3U/81Ik1$4,$C Dhy#`0G_ `1=hj}0k&qB36,Qc52}a<0 se0Ou<^0# 0gcv0=}A8o5 ER4b%S3E*B0#Cc23G33Yӧ2M3J1>G#?77]4V3^Ú14$%vō2)es(1's%z4)s6Bmw 4 os 4?hAF1SlI12mf3'S`10Rcqd03UC2o ˷޿=ܻ_;򫝌&NՑIn߳%,ZCIszӚчݬRXcGTU@4WQc05P/0 0#X2Gp1# "0fS QE1y#~3/*$7aSr/9]kN0j .2-#¢28'Ի3VBB5I&C2;n6=9ա3Ks7p6.C215 O4D2FA6m#S2IÈw:02<467kFs!3QgȚ1D c41HT3,)# H0YHp0(@i1116`E1w/sZ4QF+f0z,1,0<@ P1z :0&7r0P;=@5!!g(@V,i-9Xfv$8.`CGeiOfh ca4c4.(cUlkՍk s;y;}9U*rǤuK'޳qۗǣ? %h*IxCMXu x)?oxd:#1-7$1@}]{K=aj80Z3,2_ 3 1kCZ00|3n0YA1[(#r`2{U$F5-bq&25^Y-RQ[뺖|4ɔoa(ȐèԄ8C ŊCǢp a!7$R{Mf́0pau~nИJᐱAMx 9Z-芙 P2o"a~ma|"qڻ8ܩ`y(. ]hRA:jܑ٢!Q$؛X\2 19`i-43Q٦y:C1iLa0q0^q}qzI(?abi3yCxr!XH@ꙍ @ 0Ǔ=QS2W%KbVd(Xcc`| GhH1(<0^1dQ0@0ă1C1t+0\& hN T&Úpo.;r}'ss&;id*=`Gy8V8tMYx̜@tpDZ(:h(hb df(T$Al,H g7<-o͟}O:rw++4r?MKۗg"h ΎToTs%eѷԑb|` uw|G`=`EǴ_L"J PJcČ. @D Ǩ8 $8c̤ѩO C̤]N|CIou 0X䍛B WhL &PLLdN6I N $- EjLiPMXt ҍ -LXLlƠ jðh;l9 ܄@00^Lō*C PΖ RMQR{L4"M<|pf%7 3 G C:C2v @.f4 YR4f  P# ҅|戈8pA dɌ@{j o,yk٘*ٷj~T淬s>g9eMHea0Lq"1]+օZƇkId ⯫݉nVPj0!1v; ½1S¥42S @C39 j17 qa31PSl1hC"CE09c6D5\#.=0C595"z5#B4B 2x3<#0R13H4N#F6#8;e50js032j*b1,L!1;QP5\)#50a06#T1M##F0-)C81C r12 ca4253Ũ1K4m5s27 3;9 >]37n1Q3wRTF)C\#2KI%s4@q@  H @62u1bAC2L3ԡbk8f.P".Os0ђ&YЭ2ξ5rUsVzw*zz[YZFty4I|NE~ZT~Y9 *'JňEdP22iw|GS A̿^)oL L(_LxL#` s a5 0Ћ` U".' (6h@0dM00WC0@T0C1w5JĹ0&00m0*0X1i10@10e;0U0B 1L06 0AN10V0I0}1A00b861b18icT`ؚ1i ؙ Y;)Iř aȘ @WzYAAaG1'yoنL!EϠBQ  Iˆ)#qI ɉ`omƼeFFk~zFD8d&0Po6dD&PމUNr!.FKc@"H&05DGq-L[\_޿<3k3O L] ģfMdS4 /},9qitC1惸wzG= CN4 C1g,02Qa?anB ؃( yB]h>=hܑ釩j'IP9f б1ȓЏi`!!QYFAFB١G g!ə/p.jaL` ljchٮfIc lSkh`d[,4\ SR4I\I:a"BKbP 1l0|D*  ĀD0<2SQɒj!HaF% }$4hZbfszե1WXڱ:?vzWԫCgh14Ji|_wp !8+witH#Rǁu )iQ ZԾ 0:,64[30/b2Q2n3n3()!9ig6,1'<*ᇨ2t h#͘ ikGdzցDmBc,SH>&ѳcBB#@PBenie>C2?XXq0 ɳH͐ 6&+fBF-Y5&GQd|``z^2) 0|"yv9pԂ P"CQLy0E1gQhQpܕN`fK'zdSt[ic5&i.e[Tl.KavJz-\SdְΥ'kPtl&9eі)LyYGm/7muy-e/@X`@I>qs_fft-!Fi &d:Feac5OwT X`H8Vnde&4@c1Uc`)bd3'&<)'i&Ffafe$ƣO9}Jf&&ƊE f9&0&0'#!h `OFN,l*AGp4b% h36O4$FC8Z9L[,gf&`T60c`1Ё#L^S"(N`a&\p8τQ(D8tL,f$nYTaBGIƫIA',4Q:99];F>?0T=7@r ۇb7-rcrW*Juv3r~vjf#dF\+ב~<"kĖO5OuiU`z@P~lu:#BllÀJ  t_͓M/p˰ЌoHKCĘ&N tP)B~ xp$ P`xL7hŠr ̄8ԙ|C ,LDJ JȰBLn@ØƠ[dѮD,9 1[lj%JQ,c9/B&%6l5y@Rjj)~lFRJa౟ &b-HN` YCF4T4e `XB45΅c!?)V#:D8J2RFP0Xd" `T2=?td#bo3ۼ6Y Aw܉P[Q&/L_Ζz1f^D`={ &mY y `S81un } +FYw"j1 %@0It+&U]ed0",$@(, R">BIyg.͚;T:a9B=7XQ57<0 ;f76j40>5\1l7u23BG8R2<13!4ȩ34431IFKJFERƒ@0TFd_KD:G驀5!|a8y'1y#Ќ8 xl M]ΆTNAϦ..ij`f#43i 4UB!LČd LdJLY=hCBF3D&GKӨa59:Zs'~5%j&JTkIMu0Śa#@ 2n˚Gi* Sh)no ,P4m0FI%@ʈFE @ 7Ì ڌr qL VIdΠZ[q8ǣaf8G %l6!p\  `9 l#L1, a& Q*͂6r R0ȼ4LJ42!ݧsEM`23Q6:c36 h4 ɪ`fk6vf)EQF@fY&E*,(5&6X3<0"2 "Έ⁋&CAp@ B4r m>rMO@!M%/fi;SĒS)zI+́vt_iL}ǎvq8R7%эDH?#51g@^\,CE/eD lxQtI Zzj[#ɧ Rδ¥ɖ9̸ЀSzA#sQhZtPTre3!25y0=9ad1 S 1I1&2&q1 caq(GXY0AR9]L w21sP9ʣ^ͅ"ti,$ŃeMN0x  Vg22#w25 C0xDI5¼ !L|1 @RQf/ u8eQfy@Fif(,a"Yf8 gqih5sC3:~30T 8ӎhK-axM.ˣ |6{/.]:Nm-yJVj-fiUworM=Z-}N)رz@YLYVхes,.0UFAU[G+j^xtD j aȂa6L 2fX\@aQN-M A`2s1n=i0@q/C& /LAME3.99.33J#(s<7s (2R4B1:l54"S"@1]Ys27O9,әM^e7rcHMG58jhetM K4+R3qm5.? +6B@˪#CJ QE4:x 4Gc8L:0GS75x8CWJ55sǔ@tu8:Ck9P42/Vi`iIE#D1փ:.1=h0Cs~9 DՍGf0ʼngF X5n=ʥ29_D.Ă08.,yw-yKᦆ44U5,iYvRKafi܍r" yx%(Qc[/s =PզS-@"EFET!*AB Zzr`AAcwTزb TD5smS^(+0, XM3՛,hY៉əoqīqȮ1ޛ٪Q`сb`d`af#"3x34C;G3Sa7sH Qنqԕ̈H@ 1zWϊB0@zm,؈7 AOLqC x %1 PslT~]Ā-ThHjyXQVD" tJ&>WQ=OGܜms~\tӳxzzyHdpno z7n-ۘܮjQFcbpZL"?*iٰ$ZȔ Ҙ2 RSt;)Vr#m,e*`5&X`CE7{ThňiLoƀ1i8ʔKZGML<;|[ H4˼EsPV(1"C^3)s./ :3áSM"%}pdi!9Q)A% ΅ Q ! L L1hhĆJ( e]fĹHNUpğPC<( YPL P̄EDH%  Qz^Rsd龼P覄a @/>M4Uu6Υ,6rq\:Fi_CPTMaRM%HcoW硯57a`ܵW q 홻oS&Θ& 倶|sV13FPjg, F $#_B:a@`-AaK6"`KXZa`bpb۪vNPX6ArA d4|#S$4]sii=m d3-# s scpGμ1fSĜ9t7SL|9s3@P9M|iybNF8%50OΨ1?2)DԤQUDF`r3 z5#gY|ƘGOAM;0wEpj吝j&TT5Z1-T:BÒ63P0(AP 7``ш"$99 L.N^AcԙZ^2CNG 5Fd'7 j0_"Ozܧ nvד4@]B32qDhgYkqyV!Ym]tLS3aeZecahO  h8 ƒ#@Z5C(0 G2RJbPAF'F,2x5X"f/c29f /61Es2~#8œX0:L NfSR(lcQ&@(`AF3qXaP05S0aAph"bmPY-!qW\Zx,\N^a?UcC82A֘%wnMJJ`\BP.eLHԾ֩0wtLe'prN &mUA@tұ5^,\=1tplQ 03.BU1_iZ3d,eLކMĊav+'dPaT &5b /s1t^m`fd:c01`C<*`R>)a|&:!gAA<(e& 1M$C5 h%fErhp$Fl~T`YF>]eX ɡFyQff-4,j1Js&p4 40 ٞF7^ $dч:" Po l@xc`i3C _" \2$!t(9L ȄCF&b` WW P,XSbd &[ (dpz%{IS7)oty"n䋏?jynOVE-uHfL6]3HyX݅omZ!CƄʗIh\Sqd! uTF#ƒB# ~%/A@b0rr d@aQq`nEC,½ߏx Մp<t,LSCӁҌAD71ȁSm8j6YF#7Df8m >eIPky&+8x1F@h3LD`bL82a< 12xS;7AO6@cͼ4d-́1ȀƎoFv62`ga/CL701hbٍ eB& c4Q yA2KlIf6\T3ddf@,RGJ"o[~GYKȌi*w6tЦiG$0r] rh)=?1Z[ Mٌe4ќμ”20jae'z:D/@cjזIV&< H ) P(\aPLA% [N<> 3 ] 5y̘AL4L fy/s1i=f8lLT Ш AxBp$ D 2EL֨1! 9u3D|; $5,l3\BB1|"M4c̘<`DC$M7=h H7##р>1 @iD:*x0P>qQ|mRbqI &Xn3 8bicYg d2Ș* "Y,xXbq1pt)A7!B2D5;^[x8t7n֜mnjK;}br!CrEie&2zh%Ĥw:urc+wZr 2N*8D.4aH̖qD6ii ­k# !TbjV0iА9N)HQbѡ@MLBa`Tg ׄlL[P< &X"t/ [EԐ:E i 4;5PiS<̎ -G3."hi@6XVcfnj8" y!w:1tÕ/14*JW1v)#g(iTCC"<r#MY5sQ+8N4s2bK 3ɀMD0je`y3Dn <۳B:ꂃ=3K  N -` N$8ff")+j@m2"(i*j DJY 0г)?q2 (,C``(854#EYkݔfXay4Ԟr~0l_sy4Iق%j{̮h|0(YzZgW:(nq&° $YIH *I_ D!' J3 ,!T`M@ֶM&3kDV8@S>OsmG|=f8%30'1/36c1J?/. وx[qx7bH٭}(D4c$Il-4iEAUDa2aJ3%FkVfgͦ/ "x4eMP2"(4̼W3(!4`:`B@5,1!LicǦ$"4 dg&rLeaUą#LLH1x14 S ̤3ȁr\0M&Y ͜&4 i.sU 4zk}n+_TY q⏴&BۚWLuP$} UW27Kg@mJU_wP3kMw7*.LeQ 'a%Kh B!=踁aȎ(>BFp 6Ą*!% @a@i`8,"'!J0Ȅs0900PD00A60 0Rp0190{<05;L2X23 G2i3v1 s2H\31 "*nT&,J"-. bBPDʳ #fLsC`AryFr00eKo &lcF nfpte Rb$fz $ʕD`c" ΅Ȥ$fqTs@ C?2`j2HѨpq+1@!@s IIpW鑐P)!`M!(P )d,.Fs*ve3q2*I.RsepH1}0na#9}@6ߩc,`-“C,==XQq0N!BR*t. (0P$.ƥ΀{ ǩ.ace¤"ᱡu$Ä FF:.Ȍ kLmB W2cvM xw%ƻ@\f9EhbnfN%Zc&fdPah&0&"eVHb&;zf)+dga٦kcMES{jaD02LgƈLj- D\4ר(33cN S$,fFPoGvHcIo9n /2Ai9ᬳ @đЀGа\@ׄGs$54$r3 KR1Q-J8C @_IXI 8@Lh@a0`iC*p.vL8m #*]Ur=~3/NNqm1iɇLt 4/,.cOa1WaSw dCR(K0<flB *h0 MYp  s0C1 P3_D"0 B4e0a<a썸a>a^ awauv`Qa :`` ```ta`dIkiBRhQ8flqee M6*sL;ёCuX w5Fq gimgfuND4πuY`񭟙@цɆh CLB53Pc(oFi2 pۈ ذRaC&%/zIC@I@A$( !( BЪ@ӫbFGV&}_%Hڳ4Lsf_> ("BY*PTS+ܢ9S`Rl];it !/,a )1tҴ":,)@p0CLb (,@`ɦJlM5X@`mGF=^mdpZ/ *Lo$^ Q) OI 0LWU 0L0L.PL !LJN )̖4Li X7}lfJ$2d{axQ`[B #e`G9U1x'7hs+%^3҃23MT5TC~f3A9QњA˘1A,XYx (FM`A dH)dPB:"c"]s!1f 0%$U&ՁD #iwBGj ͆8o$M+\,p;#ܤF8KT*'WpSaw帿nvFϼ9F]lS+鷑-/GQ@!o#|䫙+ϬӶ3PeEq)ޞeā=WeU5XtOA3 Oddw)M%/k`=x5PpJ {F0KT@!b- ڙl͚n<0I7#>2Vԣ>Yj^? #ZHBt|vWS#GT7!JESA q(83ːMe !* Q2];C;\8+9D3Zj2Z#41 !LzLU8PMLӄ |>kq†.zaÆ D 1P,aŖ3P(uXMy'`XCG~zػAoU1j\lK $o͛/,|줴+9O*GbQOE,D$bvaVR(g M=ٶ$:EFش@.N&"rL:[.g ,vG%)~dt:O+hx I! Ár"5LXH@vA\ Ȃt  @pm47L(0hLGpHM, A ̚exn9ȧrR`F9ilh4>w.HL,9p 1jިn18~e! *@a̐8[1\æq 6fTDj4aCFPX6cPuܒgɉI[W Fo#dc@  & Ja'pO̦э0Iq͠:dp >HYI5`x6 vc39=IT>Ӡi [S2 Y#:IdJ_iނ.K#tƠ_)Zc rV*(I ~@bfdi5-q ӔD$p,$ [(hlPdR t )p탻a/s3fIm :L+ 4 C!AL2 %׌!a:L%Po DLnj @ PPL(3L @] F7s 3t;4 C8)}qA5asfN\htᲁIiaAC=gɳFjhi9I#5cq sF72  ȣD ؀f4F$eFt(Kk6 th)G@Hgʦr)<$t~ fn h惉Fev&({pFLd H0 KFCK60uTՔ*[MZYC6Vf[%{fI-t)l !6nj!QHf |ܦG(ܙ`>Ȓ+$C)u|o kK%UW}ip u69Fk"$@bU8@CP2 Tc0!Xw#+@)I 26g+1%s"s534sQf2@h3M1#1-00 `1131c X5VNFg7 δ#=4@گ6 jODaBn~gyyX(5L.3+Tp`FBrk3Y" ǃQ&LVB2B41DID$M5ĬH`3XDD?02X3B 430@ɥ2Ȅ$`!F L(f8l (ڜ MC;LU٣ f9ot:Kf ݄ykjSMo;˪T9"#RZ>J\(d[F rj,4< .k}9 0QɈLXABPXdJ1.@p.".@`S胺]Os18=EmYc6_@Ӻ4@#JbUYO 8;icI9g A^f2]|fg{(l6P $ 5!h!PFΨ2U b)3I@ V2L7(ʣL`0٬l>5#S3փ?;4"D4ce& ZgG q5!Q1]2S*1cI47u8ްsxTǎ`H˅PQ@]TTb`L (P #:HT" 00P.ƉIT>[!v]-^9n#.Srlj JZ߈9څ8.-\v Ik6oJ+TvZh>ӗ)V7JC/:p:P/VjRa`rPN"b0%`qM=tQ̵&aHB ;1Sm1hCL60C)0s6k$UB3@!(ٍBU)@PyHY@%]W kJL̅L`Ɩ:4DXP5ݔ>RegI0XLn1 CQ%j }.$kvl.~u݆v-ev-0Jl]@ x1Z@aE":APAe('N8O&%\.We{,; UmMRb3&/Zr}VM9Q,n+o8jV..IR ^ 2rŊ~C;L[1 Ka0 @TP(ABb#?0AjU0sW: Sjo6:@ 3?Pe3|,5t221C 1 r0 B.1pp6M<GMN6`c[N&,aifDndaKgQjtqKq~g  p>ttfEzF)8fԁc@S5"; $Ө2DG2@̪8݌:ƒ:i _hUˏkQ&jufU6i@34.i3+ 2y#kN`ABÄA#221QUAe`A$"Ðt.[0黹+PJ%r V+97bř *#5bR-rB#+[ӹ}7fv$ J4\UT= Ri2GBR\H9v  BR]a(sok=fD3$ jrgfFljD b-`1Vd&f_ b~NF N`NF-&`t/a$&)8af`Fb.F` dD'Fv``]0 p2>S`5D؂!\0KsPç㚰 !ؔ9hfhbBLr'=<$Um4K`g!B6 %cf6RFMT +1&##T̨681`faQȴG0 0ȇ$> C1b`a#@E㐵1Z԰yʑIfgv]סȝfzaO֞b_9ؼCU!~r_nV[c4Ka" P0++knp zak9$_괅"@OL$Vh𘩭0ږDp 6GtB@b!paR/47X6=5h257x{1C1M0D32o69B04@V $2$4L!@uP Rc 7*FBr 2STB٢2#G1'ICCMr& PA",,8z Թ8& Ƞr|`SGL=AXͲ@̑dw &ə=(4 %]FfS˂> B4#  PpԠS[FILP E7% #7%f3 @PKP<"\Rj*C_uUvH-i&;!䊀@s^MK 2U UXgj2mص%qZɣ0u)<㾫EjV4 HULY 4 Qb (:<"\8Ҋ0 1!f0 TetHqgPD43FAXP,Ǯ2 p͌7M34dħW [LAǖ$TaHDkd0(nJ'H.i&jB e2900>q1QsbK)"LU͋Ll J*d+apA⁆J1Pb(1 QTXĄI HB 'O-}.]QwXWALYrkd${W+dDH"qc y1]/ C6m)^Ek?NLEy+GlIPY>LT' "n/L/ (>LÂ@‚ypA#&<a  $-2 dB1B]@QT&pѧBl H 1WD .G\B:qBG Q X@(4PS1P0t1XX@$ae0 6C UM/ t,R0X``@ X >WkeJ bx t Oh"[ۤFP뙦06 oEYx@5x"q"ApaFN߂@ĀWnDA@(@! p C [4 "A@412P(.hrpa'K`cs& 5?02370s>^=%D@ g@~n e뙁>Aw`C` x' h+&k`  @`4410B )PɁp ]ٌ0{KY`hBDc""&nc#a"J 4!oL l*f2a)D&`f(d ɀi:@ l@.0p)@jɂ2BlpC0  L1DĄ/LŒH% ` f`;-OK_Vpt|n`(0 0P {-xĵީ7;ۦLB׾F@rS*{ nQ7aCw_=_v2lKlXB#5 S|ǀZ\}? f) b@TN a^,Ġ0 _8 0{?3908S'L0Ӱu68Dy5{]s5eC!1]0q J0+3k<767W2$553H82~72ү1Qi571:J8|54 4P@AV$7l4L3 cYcٳEC=B#9ë l\Ep؁Ӏ#pdeVuxD148Đ`TD̑x`n73B dشbPxLE# !8 ;1D0|@RA008=Z40"yL2j)v '3R-<%cf"&-&)l ^R dR1l &艔d 9LiqaAn45 0:3#^1,2OT010_i0¿0140N(`'r`  a`2a bJnQe^9U9?_ʟ7Zr?s6eGM^LzQh6,HR]n&}W3qՔ:Q9zb=`ЮP'䞌C'몘, ᅘ6O G00T0V0z41TD1p.02L1?D40́0 00310R@0A0!:0)0T00D6005R0;11`?2l0r21'1C10U2X3H\#j@[3 Y/ǣICFV3*èRO#).d*tGhJbVhi  3#:nC&:.cz La BQoÚCNd'3sYx)FjB05Y # ' l)֡\ ~}ϟ7,Xwn)5IC. y}\ءK-ؿ;f~!QI|~0C6.S7 K:LAME3.99.31)35O2 8s2 S:978g1AH000)AS0q\0>0Y1 3!e1&;0Q-330 `09 h/ʼn$} ĄLX!LT:@bFaJaf \`: fLa^!@R0T c0x 80 Q1k$c1'(1510(!03LO2  Ѵ0pSm4S9f6F-3`xq8ۡౌI LT:-&2 ߃O ,8 DH@VFB-Kp\4z/?uGX-{=kvn٫epם,ʃ+%-'ݵMcgN҃Imٽau0O l TĦx򤌂d9LM L@dg W|$O+I0 H@GYx4̅¤dL GH& kDHp>(h h$L* |& ! '-A+L2m2 ̉M+  MZ 7s LM Zȍoo1`VoM,D%AaɊHTexbk|a1ǘ&* RlD@Ndn]h:QD% !qaiaXO$  Va8U2B0@ iL0%ZjϹa9wkֱwgy0ֻ^jU#4;4c6o#+={bTSHrLAME3.99.3UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU0 1|2c:z2A31Z1Ğ1KBaIx`;``r`/``r`/bH` xebT`@``q98lZ1̟bl1.$Ln̊tQ9& 22 f" Q f@  ҫƩG0&$ 4 FU3DԦ[6an Jh.f* `d`~bh< ,@`h~`XTcG a`X iL,80MԞB@`1tm(45;c\v=UO4X^ dȻ؄k%" H4*Ib, $F:g|gtt=fIP׻R'gju!Ɇ i&`ufh.-Lj=`¿1F S;:hlKJrG3:iLƆg☆dfD f$a Ff F#e"$eR($$4dB& b&`v `Df`4`#!av/`U &a &Md^ $1<c3'><2G?#I12H(O10 1 0 ]0 # 00sa0j 0 01# w0)`"'A N9p@P.f`@F`x I&``$(PC< 8ٱ#84 As5VX$/~e}濼:k>㝼3 ?W {{urIv-)T5RJɛ3Է;=,:M47=TS77 9xx1D218L2v0@s016A0C00߀d:G#603Z#`?^E2R 4Oc#p]4=R 1l(BVi;Ζc1<CPK130d KI2-c0c!01=@3 @1{ C26 01_jvS8y'0!c0| 0+" 30nK0c:4 0D cN x"80aV0pp lKL€LL'$aT`& ydx( nɋCY0?3`cNp_2 0!c 0 S0ui&+)щxtɀ.IO邈f#I8> $ Bh``0`."ZVD) CH00@/ U5zYn<5-sǼÙe9^J(gw:\&enQnrH𙷹Qyz??fLAME3.99.30X2?~253QB2_4NP4Bρ0%000A0K\00!C3 )qoVL$2gI(~Uɂz<~ A0&x" 9`%uɉx,\(SA? ᆠ]6,X`{Y1 JA}a2HyN@7<LF z~o g2nvF,|0Ha\$`S&sd%iRCd*A1vJc3(N"a*f( C AT *&0\.- 5_}_kZ\ܹzvK֩5Cܢc E=Y֭=jNclڃy ƴ=aA|[)hż*`1u;0 ɪ D x&0Ž{dF9ɄFvBJy0t  ) `!ƘB[0N`3bJ&a:`P"`bK.fpcfr`غb(dP`LR5H@S1s)#n\OC NSc@Ls^C"CJ) -h:A1L0&17&bMED E1Va*5V hH`p&( %Rf`(P¡8$04uȐ L<t ՠK_LgQբt8t':J \cR&N /bDԄa9C{ĨD/zLAME3.99.315 f[1I|3C1+1 2F31C~041D0fAf09U0f0S0C13,U8K c99cd2G- qT130!021@1-s 5w6 3ZFm*#!/1^ p΃" 9P5Y:5%91A/L(*RkYɌ 2p Fד"a0hq?o]rguoJW'ٝRD$pOY~IGxև#կ 2 %[v`F 1͢>#cF0EM1dT1 Ate120 661#"B3dO`PΔG> ƈ& CĬ`>I0USpL1O@ȸ wRMc MCLwS%4.@),Nʡ),1 A¦A@уq9$ёTq Q01A‘m)j٨9IYyٻ0IHjNAʙmɓiIy` &i1Abؠ`0``b2a chB`r2 `0}0'+r[)go_+رvjejy߭^}w~6]Vu11¨)],\rHEOHмPI$b/;ZfITδF[*Is.9q,a tqq9Fq/ǀɇ89),1Mi QAmf7ѓy эA_ qAjȅ q"La @̕4U ,¤όNDX:G t: ATÌ̡FHrLJ$+EʍnLy@hg4UX0L) 8sLȐX̘Δ,\L'cF_w_SDCEbCIC_pH$ÝޣL]mR'T3L 3CcOCLF`sQ)H3Z3,#gdX 9\1ƃ½Vn)^SK.K[LAME3.99.3UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU2K2#M52 21dO1.0g09)0@:6ST8 ,KL8ËAF>cdUPTaćdf&v~g)f2f`&@rbך*@`bC % <``XAX .c`0-bDYʰ%@`r D`kaFX@cj2g< #1#>HqcP (P0B`  0@pd 2" @ȝ`t 10R;ga,& 008B @| ` "vɠ` D]ѮI)sMǑZT.$Q&x;&.>L RG̫DA !9 &UL uLLY=a y8e5c @S X3:όJN,0ݡs? _3υ f"F3 iAPF? d& #YԧQ @ {i b@f ^`M*crbR)*P\AS!y h(d3-)/სOAF!C(ʘ!ܘCYx18pC#эт8 C!\|L-LttȂL7^)h`@/=\ǟ|Ż797gɞks:o)AGiqְSaYʑJ*R[=PQo%Zx~7O28:;921L11 ,HGA`:3PmS+UM3<) 50~daL&2, X2 fBoR,kQ* !acbaY i/o `~h7i(fcb^-heԆgBhiAd:tpb a1f0% 0 Sz0S#@71&sW1\ )02 110@7  Maple`@Gلhp] X0A0+S(` RFYyeƧ|e?Xeo-WJjVo*o/V޳af1gT])p^-]&\ʟ1܌R` `IT\DluhsL q9Q`y8)Tk̂3(=̖ 3 ǣa;(cSV@2(HdуF fh2xCP֭`tGfeJjbv+.`a fad\)k^b @dRe)CoF^8hdg~8b~d _f&i!a`Ab"c)&IPa&%2c9Eb1a!a. b3&aR +Atb;#`h2bh` F la`T @/0# <0ZA0,pYX܇i5sXwyqn~3vMCgf/FT/G/eT7LIe9%ty mf$&( S< f:Ht#lrkaˀc@XLP2B J0$Ls6B4pņ7C;eU TH. @L xMJ6 \ ()LŔ̺prv{BíK x4s] ڌY<׸ ĜgL@Lt Ę L>Ԃ Eä] ,.LX1LL|tN@D'LH aX Ā* S#!p9 O5C%05T 3K8+;Lj ?+o kWX{/7?2_suu~f_/ݚ7WwpԶ7AI*0\"03t04\LAX "1 0KS.0#0 #&CyY! N Єוa8eys$91^A 1yDI|!`a ;SəSyWx[ta0dFcy1Vbh;ZhbPJ W1$F/ 7+0i@9Fw6 |e)/z#pLz &g ǝpnSaqr5}xecS^f "#♰4`{]z{s~<75;[}wƧGk;sW禩yzK|=;*ٌ~`xmRHw{lVjSFwaFcH( S `/0g 20j!j Y@M0S0n $0Z C0b1 :0<s @0 `O0p؀ E( Y!$ha `T 1 s3 "2A3,Fu1#&1"&skv3sFyB5"m~4S5(cV`S2w^y4WS 5=Tss2d2>,q1E:V0X39C1;0=c0 2 !0o %G&EƥFfƤ& ƺiF)= @1@cP27M*7SgxY \tzU~M$ ֜y߹ao ]nk W+c1ys|. TΒO~˗5M*֌?GnTj%XnY(MJؼ _ġÍLAMEa33#*0)cPQ2{ p 0up0 " aX؍ `A$d0p, |°LLP <` ŸMj 5^mS0!Q3@ʀPǤ ȳ'IG5Pb@b!P Ta *ZwgsyeΘf6SX6ne孰{E]Z~k '_]-"Ϝ1&LGs"Vs,6.,<+ f%BBǂlBy\5JF$JT&]OqM=`v!cHIHa+a"!lQ 㑔A$kA?WёAZi!q jXԅѓ+:1)%IɪqȒ!0C"Q𘟟YΘI&̖YήY;ѢM껹ѝ΁ lџiLф1 qip NmB8@ `X`p|xtd\FC F! /0-BHbn:``XHb,cȊ`x& -ֲ5oY ];ygW__OOjKffnYv[2[Mߔwj"x<(AXU5ę0>u55i2yn1D2X0)21>1218k30k2|u0P^2}3DS5 O2451D01NCs 0B As2-(0S;Q=39#%hc4os-M2OaDV7FH1}r5Vo318^و1!Aq2sN?z3` `8``P``ph`o_2r,{?u-cQ;]MٝBȝ"-%!Vr(F%q%R!xĠLԂ$0L ԰o.@Px̦`YG`=&iF"L9 % L8lLG˅51bhؘ*,)0Y" 3t֐{MSLܜd>lQKM; Nntݽ= 24P^L2Μ" QKiA ːW EHШ} DtL _o( T̿ՅPз4 <^x%@V4(JL5ij5×r P3(113\Z3210d0Hs3{d;Q+Pi<LLcdjj81IH4l~k7?y5s{ w7i;K9O;2Հ04*U/Ve^92HçZiknRxLAMEU@bJ@c006,C 0!300x! v)Yɤ3i+YcᙛtiqX kR;1ך:iIX3ѨL1JQ~{Yii$!Jiuy1.czn댱c`ha)jQhp^-K>GXĆmi?Շf-;F7|*抐'k0/ bF`&fE& dLTx1FwF9bY`xcBd9c8h`@a a0T`a>bвQ0 0(K0 `_ַsg4QMw7lt߿3r,Xu]:ݻM&r (:I Imf W@mՂ%s L,d¼$L (Lq= %lu!Ma/t L  żF5f!`a S&gabJzndT+FkdbOL$ilfJhM|f!{oF"g,fNfPO iC`ed/AFc~J&:bbLW#ZeH^Fnc# cdRlmXkL5m$ԭЌ9|%r&FBQُ!i1Fє%ّ cv>.F>F; &1 e a4HA0 G cH 4 X\B  C>$&PNH:K_pib &;:oF=eIД)O1! ujHG&0 8 h244ƽ4J?8?v;q^91#5D18R1<503p0h3y4x2 96Lx66>_2X1I 61>w000R@10^0yIb1X0wY2Sw11D0T0_10Ay1rz11ZA0 52^1 0]0L0w1>!0+1b0W0Ap1ށ10wA 0A0#0=0]b0c00:%0zBA80+:8e&z3 &YN\A@pb,`R8t %!bY@$  \+l8&]D=ge?Y;Z-_o.}s,gKKs/3ig(2-^;C뷭cVvf> u(L-pMS7  ^L{Ͳ3 Gf<:MXQ́4@q9%YCن,ы%VZ~&ы!. :+9a6'yCb)'j-v!|%f%& و$% #i" !i<(yiQc0yn)$Q-1"Y9fa//Aic ' IrsZg a4dɐbjd c`.Z uz:)6`H`a8VLLA`zLT4*<+UPV;ɳroL1:`6?͹52QV8ť7V2`:535?7154#B IIf ٛ!aTcqcɀa hj f! "q$Be @4A``兠&Y&(H(03?-jL7׿ Xw1gp|YVVQKZn]4ݢB>}%͍Ha8Gwv^4&xßdQW7HXj8!J\[)SQFi瑴ٿ)a0))wZj=iu% 'yD"Qe2p Q/ e L> DϽ:N,M<@wzxXECKɡ(LCMF,p&.̲̀Lhnj> 0hjH@0Ā 6͇W L͘%MNNhsnYa cLA 2 ? @a&DZA&ѫᒣ1ُqQ&i  8:3$403 ,AdLL/7S CEQSUjAou+單MnSk[K41f Ԣ!6nq}jHYs#qXB"%(J蕰f-1o9o-4-AW49dP$LM wx]$ ;]Sig#7Ӯ  0DswL1̒l DHԐC̠zh XP@P@0vlh;":# <V ٸû]TI 4njdЏE\QM̌8GeՇud˱2L((Z*sQd1fҙNgNY Y~}j%4"@l B8tH CXLz Q9J|ތllhLW\ CPJLr4G̼TX$V%LjY b [ȸ̤¼,Àa?^3M04 Zbҹx!X4gqobIS6@ d`9KD h Y740\@C0hH1Y(§:Hi}7N3 ѡSЈ(CE3 3Ӗ:nȈ ,?W]8vUr̝6nfZBci}6TYdf+A(z3`.[djW (5]EIc S.bd Z!$R+s‘s \E<,= ܲt K$( T*2L``#D)Ӝ<2$U<$AsX=903r>1#<3@1T52P3,T =#F<<& -mFsf;F (fEFKfl&FJ'&&LG"&7 P@phfgXtF8mq 7F0c".>=j6#N46WN:i2: ͛8;eD?2Ħcx3nnE*f6ذmNRr5E?6#f\r-H8=؝Uj#,FVP40빤/5bCGLbI;3Hevh"鯷 RYi眘/:q7 x\Hȓk/̩~: hb,*rȁD/FN [ %1e 1 !pD21 Ċ1@A4Uci sЈ;ͲA_ЮkµÞR jB94)H0AHU4u)o]$U*ߏQPd-:{Q WJ4u 3Ȱ<4 XDVdQq"tj/>NZmT?myZW-w-%>#!̘!L!$E(d R J+R!<d &,h NhMY2IB"T6)5ġAMsM=p ãX0i:!ǀ\z4MS\>6L80ƙ2́P[)-)cКCb&(p 3VDcƹAӮ 2jVGFI @*&<ZDDK 8($? @$bn?Ijjy> "T7HUh@S$+l=W$鲕:kۣ6} a<2 gaӀ+Xf:֓ܶRR Nt ;0H8V29'21i:c70r2MS.\ڑ3(@2 #H Hɯ<#Jk1'L#f 61 60l1I !136`YCz,Ճ2@Mh@$E7%\FLex ܨpQ083tׄ %0(dP6a'hA# ވIKxa&w a$bR!Euez܅x+[Vkߌ>,AjԿwpḌ1i^OJz$ѹs\\|.zR#Jq~XU6SEd)Rh#\) HQDIĈ - 01( *M$ qT%k j0@`H$.p@D l 1Yݾ݇r-6)pc, #=T3}j-1DG܁HCV6FA'z<y5 ,X` ) 4E.162xCAjH8L=LY@nDe4~Qp # 0SLA5>2 aDT0Ģ X ɒLTL\tE 8Bl B5 M!&l0rI@Q8TP4ƄU.@;1g`x Ɓ\O}ᤨ3`^p% H`T!(f ` iaƢau&1y8S0˴ |^*|K(aqoM=܁Ƀ9ij'aq+ZŃcSsnA8 Dput,nD&Zf`CVbbA;hZh PtǙ&DlLHq&  8*KԐ0 ƶ)FU!**bU&thHdZi^tᆣ:LR~|&aff k 2 Jfɞ>P6 `v(/>k@sDHAQ)(Q_9K選ka2uFI!YT9T #RisqP66;S'ݔhM:0ҁw ,3Lk6Pv#= mZ Pd2R8,8^cɁ@ I> A!@kP+0@th41E܆hf(S^XS/RL @41 7T  P.@ɝ` t ܖLϝ M6hMnÞDqY>JQ1Y 4BvÙv'1*lTɸc-5 (З6EC P44ê9Wq:bO5*}s.' oP FT TaQ iĔ ` 9`ap0P&_FbF3NjFIjd gbm+c Se)|劜;tQޜ',kn0 @$'/c%It^E -$@XT)T (`!˕ L"88":v ePІ)`#{2!:7bl@) hdЪ4 QOo NuA> 80 sDy)G=l.8H`ـڨ~2is^lg6mF` `)i08 5iٿ) <+ 0<L6gj,fi2hg&p`@<N9 $ 1.Xi9$bʂWPQKhb zF8xXyz(d$RsLlЁ:;Q"A]&_eX`39qPOvkvE$fiIjP~j:&TP`p1;x77C/6&5h5s 2c!g3S0!PN(LՆDD(ǁØ(aV\V)XHPGʛ dheDtbfB "0R0Pɇ22R15G21 4@<(1P 8t2"Rf0B4O-g$$4AIJ`ED<ı  kAJPј7:*0ak F Lv[es-7z"hZK `j?+ZGfj̙IFUiCBk00ZI Ϧ0Q Kb0'"@L̰!̂ 8@€E &0A@q8 ͉ (rCAKB`4| aA =1sc,=A0 `̂$2C\#W?32;((#9xE0_47!c9:VcND=Q +YB))) 52S;dķ (dAG(gPg\gs d?E̒" l@Ӟ{f"2sB"@Ǚ()1cĻ2) r ;6 d (, iD ,Y: a+Kޢ:]nZ *6xe&^^zM?hł2v\L^1%]b}Nx,P<5uj nrXzZ+eNA_'qJahxY"|, 4aZEj"PS(0"|x*(|X2kX, R̔.3H%4:֔YZkkȕ M hT=芼4\5=j@%a#iL81O'sJ(*Xi2N3LĪ5 Au t 3&HtQ|id  ]ٗ8` ŕ.c@L2,1DMAw$L ^R&8 @R bC58ZD( F A0 !PhMp\k/yآɪ1rEpհ 2nO75m"n #e/inn\zhj%u :jy LA!"ABD0)Pa!Q `(%bQ Hn8@)0:{&0†hQ42Xy6TbW̠؅ 7 dPm@n`bTm2V[1B<5̝34 XIy'?Qi0`bed`D9L P Z qL;sDZoᒁ+hE_PQ*Th$x 5= 4K:dѣdPXQѺdy :2)@MHH%'7% C66MѦh+wGH ' >92\щ3"(V8EFG,*b9kEF PȘ..! ‚|F TLV ׃S%/CB@ʀ(ؚ0M w/J;jKXi(YJ{Uq`1t"Q!!bZC &beJ( .D h pddIphdŃM ]ؑ9 E XAX,PgaRc ^!611P/V qsϽ`1a(D ]L$ = SO]Ո΅8ALHVPa (f#BF$83œ4Imņ)!CBg,yxqL,̅2 8`u*`C:Pf!PT88УL04J2Ɵ5I tnx^B1* s*2bM8"T_p 0E% κ+.v݅%zXahY/S5y5Fc'"U:3P3-92 ݑI^k2'yʛ׍/819^]C!t,jD` e((dDM4\"` hb L#.B0KRQPT@Ic$Rx t:2hS4Ihg^qMP@ *3Y55':1301F2023E>À3 D s ,27'cPn$0T L<::dC? HCt`DD6PHټ@FL(iAT hUS ̼> tc(%7rA(1ox5C83QCZ4C 1,ST8; iD<Ӯ+i`$Rak#b3fypg"@Q\%Qc Ew䕅1eM-S$2>57Yk 鎧o2%k04:ġAv, z_?˴Z]paT.h^("R4Εr2w$@P궰d$/fCH&!G$ `  tP84Cбg.VUJ0uy8 m!4 gZ1f1+6Y4029$5T01X&11,0`!k183A '8 鉁Ba<֎LHߓA0D\̈́4ˇM@"԰@c`&tg(k&$PaafL"X 1) PHDL XZa ƀ8jBD^FPNbC@``yY)01lP5 PBPa 2C-0PŁ`)9"$T6B G;VEcg•Ĩ@1jtZ2  "z_ヅ*1[R@owċ)zJDR8kJ vԔʒ@q8aKTjBk}h[ @ jb+c$$^@h"ŕG`eBI4` ǁ7Gxc`#p8*L+$ f)  4h]u+jkE fDd&ɟf妘S^(Tɓ1b󆩊 Jtv& b`+zhK&1.(O)0M xL3(3!ASsw0 44 P(A6% <7R#ȼ; AA& 42:C2d133#3 %1mX@1qL0&!@ 3`(DA@Yx)x( 8jptFBe+Ë PXif> jj“EDp6aMѡt4|s %4)ADUN@  SpI#ӱіX0'狧̱`8D';G QS|AcA0+"r[ca 2:AVYX҉̘%1 T$6˓ʌ¢H1C  Y8sc̸8ɋL0qM ,;Lu$Mkr蠎0xA)xoYa!&AEwu'Znx*``ľvCf4dXMt5 t( 2F% 4"z+@ t"I<1d̲BDXǏ2(tDN*`R1!d`A΍ ÜgLX$ka0i :0BTHc&?A& .#.L/Re|&"hSf񸋵I,:It+Zu赊EvPS.inKjXb*r&/-3B˥iٜeO$jDBFʚb_!P n^j."+ R] P8R\ Ihcʼn1Bň5FHr3EF*ALQ!iJ&UA3 b2DӲv2!IPA0ၵJ QLAlagxVT`c0a563A Hsyj LBsR3Ӗ,x&a{I ?3/@:iਦ"HkOADHדHPPQ#Τ[@ A%@h5Ft`/)(6VOd`ޫVȕ$o7b7{\0,W( \9y)k 81h 4)SeB!&D"A9gFN8ħ0 +c6lB6T.X)Vf56 Lʝ969L?9ق~zCu$ tou`J*;+EM=l O b͆NK`j.zPen&z0Cn62B y@'gB>2a&|k`+11!H3#0As%(0HI4$0@pa@"A]Ll* 21Z]53L 2rM %0*1c2/6+>1@c;Q6C#651 YTD`FZZecfJ2Ba2! Db=CB *V2#AL, .3#@ vsm,H!63,`HкRUŀP> ćX邌 DvPlmGMu?M5bhpSUXS`X@ Ug@`& e@pHh00, b$Ap£& *R <` J$o)10C X2k !!@eR-`1嗁g`$ H1ABAC ي,i,0@PecJ!B 0@ Pиe-{C BQ" $* A/Ûn'04E0`GuCws[xiuX?MQ76)35I%Y Ù Mԃ$1ac:  0ŀH!@c9$@xIO| < %(ʉ L&|nNVUA8Qɱ"9 B1)fA7ɇLv<8.J(*)ALP֠yr Ҡf*q5KUR#%(aE`$EAPH*8,q P9Lv1emaL|b}o}7Jf(^av/h..J)He0HAD0$0AdѡJtq"EJ5"M#0*;)4 |`GDE}c3'ͥǍI:u_@;AjC D8Y12K190f3*1[624e ĹiIi#QKij;0hipLˎ,xŠBL`lʌd@ LG4"77E$mFLkF8Xx027<4 ct{cXUɑb`G@A$dTq `BGd* TYT A6`5!,5(#" A?TkY9$!+IQ(V;G0S XDP ]髗ITkϬ8@qXqˢ2\gl5c^VP}~_oXd4hj#0D2%\Μ$f Mɡb93WNC@ M6L0B1Ñg_XQJza$]cxdddWFY$P;KaܡlM$/Gurk<F,|e0fBVdq_Ii!:QYQLTVQ4 K`L͘ FW*LLD8Ō,8JAr:kp] `(&}0p 0 Pp2zAxH5FȀ͸$*baFfըE4ݚr9()傦@Yb@j ȠPJ`$0"/BӕGn3vP fhm߆0duzF`FElYh ]׈$[1!В-E:/j@!EPDe$xmlHψ1řB1iVvacހL@FTR:PNf4MB32@2Qh1̜A 9^ ̨e6ˌ3:?)z2ɯ aKG!eq9Yyy@Bbxb'DlPΘД>%l>e,:U9tsPL2`Ҿp88A 1C Xܨ86%R5%Pɣ"Ȍ|Zj,XG1id| #'0$60'p*Ze1Ct(GKDe,8J—A[  0z>IW!c!$ ,DOa$LaMJ6JBi HT%i NvS qF]쥐n-Z-/F&0BU14B+%Us2( Y<4p700AXqU͌#*D65 RcDÎ0 Āla124T6 TdO8!{uODŽSbِɨgyYBJq&#a'b`diR 26UM -]|<L @Z+6SR2xBs8"P5 P ]P.xdF~&\edQTNpp:k+f8tf%&b~tΛQ au٧@kJ3=e9Sfe@;MiT`o0#KA#dxDIhL$T 0`e0'raDAB+hI` r ,=W3Dݞ5=$i }g.:U5YS^vW ilZiLIƅ>$V&K` QAYjK)XyPl3D $4\%0yTYHe48:4 0C&3Tâ7{B)ƕ,t,(E1AC"@r<ң*( ,j5@gv@o)%lɍІ6%*@s ZHbSFH"pR` J6 H\dYcD&8`SBCL(1((Ffpu8BaL D+4 #B0`dLna.PE+ TJGhH/HblE9^Kѣ)՘EnL4*&@Ǟè1x^H$*:] n\Y# ųFϗ#bW `" W"ak[ `fChuH@L *r 0Ă.SW#Taۚ+!Af0XH"d`%hDi-KB&^AvFs& F6fff4&!C41k/[=Ux呸٘*Y9SG04@('1Ub2Cu 1ă40#7C " ʉ YЈ(4G9 Y5J e/Z,ҡW@rq SB LBZU07.,lX0P` L@@(PX)&:`$( 0D d@1-hCPCs,̶ oOCf *ZxF5!0c>d4ÏBG 9]j 9* 5͉iiٶYq填ey`4tմ B913,+@7 Zѐ%b5 ibѐ8ً( ə(qHҰ \ | xT y yx9T' X @\q ,g3G Z;A5%:8vnZXlF0BgN(pDfF@<( 0$S#1PT ;CAÁT@ڪf$%iMQ@$c"j|[1GRqM(Z!vԄMVʢEaEgH˪ɜFi>ӠˀҀ@҇0b$27#[ Fյ2r`Q1 }Ad!Á`s$10 42 BA  a`aSC?01qr'I0)9ii)$DlXBdg,pcHMn106{341 WU5̷C PS sU2#.`kLTV m\4Xl\f 5NA D! z'0(pȢ1-p21 3Q ZcC2lrK`/Fi1@CpHhC ؀z`հ*iE(0GC7u̥ӀPjY C̔ B J+(8#&sf#K>cf a"!("=B9Oڑr+_KU12RXKcnn4-.y*,~3ӗHc-` tYDX7!>ӡ~ LǘU`p%jReF4F45!$bxa==H`X!xA ƚ D 20YVEq r"\ kDƻfj)PdK)&B&Llw.r  k/ĽlPG) ; ڥNQTM˂ XقLbaF~bKf`" 1+  tX4(FBRfhx`$bm-ThDdJ&PdC7 4d`(Z",ík`h3&0,赦Ha2(Tۤ4C0I0$ABXhЫi4 qѶ.lšbELc6D§87TT5qe' 8 %KI=Ѝb{N"M0 L!' C ]Ó H$M;لD >_C>Q4c 44I0Ba #(7/)!QkE0XXⲦ)lŘ29>dӦ8f: qҊHh2dthH RB\< 2 y$QaqfH}% MnC (DBR`H " eIBAhKvST --}GgSV v0ƶ3qoZv sР~" .1]X9 \)bbDWXPQ\D(DDd$ NbD05@'0k0ˤ1 #bЮ2u7@ǀj ^S:(O1ATo:52Lc !/ n 6p CAmQ#8"dzDc=S&G@ Cq `0a.eibh` "$ + b0dcP0S@"0#*0``0NpRI/Q6$`8 YA IAق4Y@ziAIX }Ʉ &0\L L.C: H` & `BN40 `^F<`8`&F8DbJ0haP 0@)BDt(ǟۭڧ(OVnUH{9ȼi؝{p}l@6\tܛQMJ1 kUfԥA|?fvbC㱄R._-q ؞:NFx%-}Q JU"lz6-(!l( &:ߡ`[=IU_=bM phK~(5H°V bliLL #3h "EX | LA<д L@F&aF hyF`| <`vRU$!@1hW(10> P 1i2-a0 c 1300/Cl0'a00 P0K0$ 0Vp00Iɐ(| `Azk $ 0 [Vp0#0 0 a@ @M5gLG=L`/b 9 }e&1~;yԦ%&Ze_XNN~A- KtU{r" K>}Xvt\s`ȏޗ{I $ϚWJҡe. Iu@8zT` +DX.l:~\Ő'Lǜh3 <:Aʼ/A4(^fb#Gfbj.Dar!O~` B$`` `FbfF$cpUp`|` V`5flad ` 4+`1S 0@0# 0SpR1" 1a-03P0A pFt Ɂ9F`(X!L'9Y*ɀ1>0@ ``&(`@D`A`,`~|`0"f: 4`)fp9^zo8՞>ΦuҀ>#9=K>ZtJ3Vn#G Vc1)K,$yHe3V$?)=Ԫ_~\9U#~S HyHQb!鞰0S4[?0rAĬ\LU|51„TL5 ¨& A(ULT7h'T0Q3TL ø( @l`!faPf?¢cT& @`* !aDB$A:bbP@+0Cr1 10qР2у0`ZL :" \Q1Ei0( @L Xt(f`x` 0O`@ (f ~R.X @  `n)J]Mvw[rZk=Qpۭjf%%oX\S<0nZ͙EyanR\~tl/p~Z(I;P[r)Y[@ I8X2g ҴPrc1#90|B[S1P|2O6!|a3Fc*p0b#@13 0&#$@P2LF#1iuC890`c2*)0 #a&1I# 0z3G?+4i 21^  }3j24-2hu1T33$2h51.5# uGAMTF\F}RFg/,'+ FSL'mFu gl#@J=79%$1`pAٛ &q\i18ਡ6 EC4S.6ʇ30E"s;M@5F $$SG~SZnv?32a܌ƲIJwg=2JYrK|oyQOַsXʞv{Rͩ-Mb,v(gkQh[ugtlo%VqToq3dY+f:hXt pcH9>@e \2r2):C7#0pczh$c5)ج3,$\c<#T3L$#s(/SD]YvH5Wc$[J cm A nMx, 2O^K-0Q+IS##f ds@V3Cͳ)SBv-z8`HĴ0!ՓHR łh׀r|‚D6(l<e6z&0t$ H APC]魂Ա,ÓL r hA%`lgFpT\uv1i(3@s3K6T2pF1K1000^!0R0$0+0Cq032 V00+,-&2 I t Aq!♉aB1JBdxg`lacJ>c8ab`bpnJ FC"#@3 CcC)3 Ay#XIC2 3S(RG DHC8ǃ4M-5EcA023FY2g1$ | 8$Beb%ƪf`L@˄L(,152128 3 2@\0D~0-j0D0m^0\{0B0DAw0WB0210P10O0$@0[24 25%2Ⱥ4^73p3ih2x3X g Dʳ@3ƱHa@@ɲ Tł`ȔdLDxc֔DфX"0݇8ԕ, 9 caPzae@3E3/10r333@'332CLD_d[R$@d088 4$8(UX (ZOo_ }y~?c5¶W~ζX{ՠuWyNܥJyK g%KT. -\=QcLAME3.99.3UUUUUUU2D<4G?z1&^.6*1p9qR4T52x001F0-0B0E1)42~V|]M:2S=w+5dSs2>"0+r0s 01015S5a2!cB2n c1q0>L`*&P#FE CŔ z8̽!"|˜] < L T̉E$ZLX55@ő &&t!ph$P)`Ó LFR #x0dD$'<8bPH08`A8p`@ (h 1!I&/ wsT~-2{-u75y_n;^WV3RRĥ۫=^~bjk)F8Q_N.ƉaAoa mtNaFc:b.i2[NnJ,b5``-ada"bJx`HaX`ڂbffaXa@Vabxb!Xa$+lQ Qو8iIf (a.ydᓡ1aF hJd3tqi@hXfHhymdkvi n1 frZp;oVl*d@kd&d.db:eH`~U@#`*,*%H6HDX haR`x<@>  1 30)|2> `8J /fQEqT\DO=|Mz?z%Cid"`hJA.B3D` (`l)CLAME3.99.3UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU0#e1?2M7`4ŧ2kp6p<5H!K1x01D50n@0%190^00@C1E4 F1PÚ0ₔ0*i01^0G@0P03]*5|7pc 8T NN"]vj _ Ƞ\R,s'G#H#Qjd(chgxixiP)#dNL(|paQ`2@ d p #\6 1& HpX"1 a `pB=DmBRf0C4E8w4m>2#z1/3'?@2!@00x0z0FEa0j1M1 v3,h0.S00R0#a0 @4/EYgP b&t< Q8 Ah  a`'%a0f'cFbf 1ȼh&ya.a1FbF0f*e C!TtGaixdz&Vb03+rp x &5ɄɊ2"1PTh yά4f>⺿ÿ~[Y~_s˘k\y<.U|ݕ6,أzi]ɧeXMֈ>Rh/LAME3.99.3UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU1l5ܺ6>F20%:_44U50m1 010݂0Iv1@23T_2i3.e10'y8a PᓸɘxlQٍɉx?AX i Y@Q*b18H=@d{ @< ~J<`#A g T 0: (\ LP3IMf3Hʄ1,"F2 x6x(eacBP@sfd8FbyBQj.8@q)048$-޺.r_s37]o9۹r>~48o#9U'&)KmX:(ݓLћp0ԑԿHaMgYո q-𙛌2"с)4l*Y'/YB!QE٘q\¬ P Z[Eͼ@ <Ŵ!|x$LTF)Ba.F!*`h< bB @bf.aVdg8cf6dD`v`|d &d:f|ƒgc~Դh&ؗ&ipg!`Prg($1Ҍ3 &  ^1 )E!>aq1Ȇ2&`f I(Lo L@XDMTDK_e{|s j~]a?./ǻ|ÿ[}̰_1yjSSߗ>u֙RC!R]I+jZl*\x (yx9u[LAME3.99.3UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU0E;!:o3h55G0J247t3j 27B11Fl0 0×1C=3N9~d9$qd1KKC3.c<23;/0221{(3;4p?Hb3J3 0Y0V0 #08 c iu Aa`X tfYiK; ~ )QY#$dOi0̋}-OC2Hfj  P# C  $1)iqؙ aXڙ(IWUY5:3i@oR92 :LGfl-2a7c3&F a,1s#q 2`0- !_1S;5U2* 15C-YDX>¤L &` a~`rf Z3ēfQs90]db3>HuX+6!#r*2CY "b)dc,ag\0b\0/2)cNl1B0폣x2 8 1N5 p2; 0G0#@36 1&1, ?016 3 A@K@ y6ʊ`A؍C %-dži0Hq%qiYIA8/4>v[LS AEEQ㹊Gqy`#Ynd `Qaba`pbcraja Ӵ |4P&1BP`aJzګwykPc冹Zlǿ?8/lU>*T߱rkJWԲؤFhrG%!XF䬍ȋ9M}]|bjC ^2hp3R*5?3D46F#Kt5dh`3ȓ«3.H72 10 C"7=Q&0AssF21 0Sv2c06 0e29!F1 # *(”WD~8R„hl! d4A @`Gd`f 0`&AXJ\v%K&G&EH^r V< qc@hFKdRq;je2 10O0$A0a735f1a7Y83"bsB788J5X:"Q8d1,91l=6k7An66y9W2\{6PL7 317*5`:(<1v5>:,4tD=6<5%%>>&K6 x5{s:&3N)h3C;4*c(t2s$F0.01R>+2#1 !a70s S0-!0 `y0 1e0^Q1$Ad0}1C1u&s1C0 pR/P  P D<1D @@"u0 တ|B``#pǚXy o.vw\kg{[;eErzlO’6:,/VW-ȝelȗ7~ Cr˝%'/>Ο8/wâQC$c( CT#EVc"s,£2# 1 0C 0C+0 0 0&P) D?5}ـL /!La0Ƙ*у9D(4مxDt]3AыiᎉHW fIT0ъØɑd8`4 '|O߂胾w;O\Q8lb|O/_|9Gw'D'+_'_͇5Z; Z sw/ A D!`Abw'x|O>'U|N>'O'N!s(O|x;§#'L;G;yD7);;m([)w?u߄f_;z1آc(pG=cBK O¢p ApK. `A#"0 Q0y a%A>!`S f8A@Q8a \L3 Z yzNa;;^:+DQDx2AE;FNjT!<^^]{uruj䤷C[վ -W^e=NbܛMש3_x@ew9חկ [gw¾&L%[Z^!\l I]_wn}{\ԟ'Z׫}z-Wkzz~jyy1I͏:ŶwWp0ﻵP'WBWO}{W}]_W_x5|W{0}^2hNx*uWw_;xywr;]z;zN&!lBAx뿯}j^Cw<տ\¸s35ujW[ UU^O}_U{սԧ|tJQ4q鿭}~ZW^u߆=%uBǭWW}{WZ][ڿu5׾uQ+OUnW.MW_v]^Np,~XlmwWDzuVzz'׵wtR3dO-j_V/I,`i,o^oIo{^ 魒'.j|uok__][VU!^"P/Yv_kP*N0,MB~KnL~cc Z=8n^j5j>ww֨O+ሎ4~ 5J9jWO`u]^z#,:~. qn?W?Zux>1^}'R F w[IW;.w}{PŻ~殠_}z;+]zGsiz)סn^Ż^-Fס85.A ϫ}[_W\_{W0sK׾"uUڽz}{.|wP~ W.Zo}׾(\pzX__VQW^Я=E5łm)>UO_\`4# ?Vׯ~ j ]tZǒ3EFgJa~y;KO[SE}k^ɭ}[P_ j_9}C]ra-_v@շ7׽]MRۿ8][X[Pl__ZUu@N}oNk^׻z^[ {jgo M5OU%k ]`"U\U5׾U|4_6~#e. 4.+w3\wݿ]V־Z׋y@6}Vmxx ֭FΫW%u=~{~կꀝRRַ?A-&jn&վx_5GݨzwHMjw~͌jvh mtO7%kq֫^Wk?di nj tL|{߸Pp5oA/'w_87_WX sI7}G7WLz4{} h*7.$z>ru8}X_WWy:WW)_V~%+\9Wಯ};{׸?dZW[oU׫z}{^uo־Г+}|Mo_׾Q?^׾XHotP;W~"W"ֹ'n_^Fow}1+T~}oU%}_~*wWf_}o_׵U~o~=≒|B_hI]{/~?_Zտ}jOrl}zOBw}{^y5?^w׫!u}{^/Z]{^/[B_^w_x*^y}צ[}}{Z_.4b=o|}z~{}zA9 n+.í?WgݭYZSi˗'WM."|߫dw#v'$zuʿ>}_u]m)[0(I-צ9Ll[|> x?puͮ)E]>{ǯ}DUv得?ޟ}oʊ .q8)s}z׎[Q +] wnZuյ^PC]xn6׸52,ڮ3}.gއPrѴ,^Ӿy-f^Z>uW8z=ݤ~pTJǰW¿Ѻ{ݾ&ȵ1cjL 6BdϞ|YOOj =[Mn c[ڭ?_O˖P_IOQ6t˟ݿA:WcAlZ`.5G)2z\ * }n}U+|_^?p˗Lɖ;h-3C3ѪkM'芊̉~5n[Oi;-+;n؛CĶ4w_pCuoղj뙇)#tgIj5Ws8+Ɉz'yqzۿhG}4ukd~~jv_ur۞Mo񿺓VlћnL#u^1-UP : =W@[^S׾_~V{}_Zut 5UB>3PwEݶpLMMI!WE; %͒fj$ %Z 6pۿf:^XTLƴ ]_\,fó^f!< 2NZEΔg:ĸTk(dE[xϝ7'I{ٿ6]-{wv|I WbV߻pS =m4L]WqhOnrcx-VڮpẺX!{Ѕ}儯/㍌!ƛ^UyZ_V+#oJTi"AS3 |LqL-ikʽkN'5w]j^}z%.ܘԟbEО|)k7~_U[I˷׌0"zZ[pW 5t^u}_nw{?_&l|~'56 W|j͛?{rU>§WzZʁ+vsM־[|lxDl{^f bQuzG{_ֺ1o_W%}Yozy:su~ u&螯?־,Pt [~*`o7ĝpZ7^ oox 2u𑺨ﯣ?;Zx \F7 A3;ոm(vJ.ҤLa~ 4>^ZpV,g{ޙ2@Ji]KzWj&|ouv tNvXP]ELG z4shjt v<IOοo'lV:l]2>El<Xb:7F aNy{\$X=C}'uN h$޴t %DM D~ :M{jҘ`_S>(ϵ8!J&:@"Xou7Ǟ&x%vO/MSFz-1Uɭ3sf7ŗu@\xh#h4JךLWyTY"/(u^)Ʃ{H֢\+X4w[Sa)_.%v}߅[v] {ߟ!CచEP6V> 亂N6wn( -V*~p^#hpA0Uy:{v>XOҭcq*'I$@JL٤e-XD~MOeN )ZhrI}''P';7{۞7I@M+~1k,s04RmH*2]7u{Ek+3_|U9q#t<=}OxK1 '-ԡݻ}Cj.PGm_I_o e.2Y@Se.tڋ/8,; Q&d6 ?Ћm7Ad!5i39S7ڒ )- +׷[ze}{ u3'/g'#~P&i8L;+Ƃ}&Y),WN3)iK.$ vJ5WAOcC ,3Zל%}kT%iuقXR CeIU. 6; n:~X':}➕񠔱Nc0m#\pM?k67W Εlj?59ܾ7*.ܷƌtp@M#sg,_=KsC}$#y\lv*6iA{Ud>L`kѴB\o30?| aZ似GP^% bV8'L Z>>Wc͍9U,1o$C SiߚkuGv7.yLfENwX|, 53O9؉rĥxx!MʚP.c RvN6dJӵf7\˔h)+q%IFwh[&S\+|YN%-j[A*壱;]n? T>KYӖxʖxBn3zZjaks_k 2>n;r %? Ix7t&`kƫl3 "h]6Wrh߄Mˆm $2'8u/ <:&ؚOiLu;ot䃟/cNjǎ7,*hiwl3Am{l977ze7?w: .jKWsSV6"AVW"ጳ=">^贜a zI{ACȡom98x'F4 ̽[<{o ctۏuv3 Gz|MJ|NV]Vs"_8:nZ ߧ?'U^ +-U܃tTWW_{Ϳ_p N^"+KE캋[EL?Q<^q󼏥87&}{g q#'ww֮/ꫮ3_^ت$GzY`/y!\q!g]un /+bl1H2?/*o$w~uץ.!G|?WzO/]t |nmux';owU|& KwP 0WO/'>-UoҚK:_?_ sA-3cpMm GukZpN|f`ItF |_CS@p22S=unͲ_IfU=jƛu<@Cwn6OOM p 6yG+"D[iii,(,fJY?v|y}=?RAnTםY6w}uTnB{ךmh`})*CD4A!a}zU{t70W?_#~'TS$:$٥QgPEJEԋ/iߏʪ,$6Ct~\G;y[p'pQr%o gxlGMҒ}ZsvsnaNRpR7{o_ƒz-_pJO`m4"%J",+d\=1Љ.dn &=]>'/Uk<قQ EbXoǏ5w$жߦ(NZRTf&׉t 7;h~ ;ag.>f\xMFr,jZ t.pT|q$)t Ng=aoa }tA7UfVZ9 ?Bn~\%JSxJ-G ?&,r폮`#]sAg/;M?첓ysyl.wK͜;neԊ|u[xPt4޻ ~ 2f=xh* 1v BZz}4A)4HEh  0ʞ]ɗ-x#d]޴`UOI'%e/ Y CZ xNFpR%[ٷP"Z7e 3i-7BAewt: mzGDqE{wn&wpӌ$'1%c ]if`~I7)[6sX* n~ݳ/GF%d0cxlڙԟgCoZյby{`vcd@J|p7HF { ,p"d~(=<(}F4qEt=耏xW9ܚfg۽sM,c2MRt:l2}k%wNuVu5=fZsຉ}'mC;X$'/x{M*n(M"蠆l'bpVg(_.7&~:mozDi4@>?-`4 c7&5i~cuRHb$8|stI~Y1dݧldeնكPhp`ۭ+& @hh(E7lcG 7Yq m6on .ݹ×f.bϰ4Pƒ\8݆Yh]Ni_a]KLLuIEu,λ"4F~H)ֹhGͷ|wU[w ɗfiU ֝u Ӷ$'/au? eߢ'J5qg)NC9$vk_(eM V'GgwD_Z.[uhn7Erkl/zj!-BU1ʬ3 㠷WX@+DH S`hAa1kP5&h%; :n~ :%WU2ڮm捘$ɘGNU(&+>H#]7MmKGAq+z*C7GM7<3[ⵓ/U~׶Z38-aiW '\Gg9:&{to˻_5+Z 0[pET6AwcM]={ q&Vh$~ӭ~ ϻ(uFVw\`_K8X^rwrS'>ns:KpWm׼X^cџ{&jz@NkS3p {[=[m)0=cn{mCUߌZ8{~}-BR]68 zify ;o^s#4/+ ~ڡ+.}{@p1ֻ {=ڿˤ"m׽&o۽q[|ۧX}pB?B#xz[LR [dV>[JmMc}ޯeY5)g=Es.e.W[b!>|x;:Z򁔹߆p?;9rAlvUGcmPgͶNa&lۯY7sc-uoux[qX涿H荕Hs*Jmp N|c5%7;.uQtLsvM;R.۝o؟ۆ?Y$:ٿ1z`N_o}u_K{I!Γs&QK5?"P\{C%&&qav 2d^_Dɥ"z&9~NOw;~[*HGDo5-&Wzj8Jrt[}L)͍F=ފ&Uu@qV'1}z/֡/:N>'ɤO+nX.կ[}u? /G7ZOh zЯ/s:OZP+APQ %Amק@&o.0:m"aiRI "y UQ[ē5^V U#Ze0G__iO:Byr< S߸5;O~8uDd p#+z7$܀rq`RR\\^/ E*+J~'s7mls'Nf8=|gsݼŜ\ }'ѓ 92]G֎<sTkw𐗟/jdy῁L׶vȽn.ݰVV eޚ Y~AHH9cU$:[F]P\r= La#4*46yjS y1 M;pQ@nHF7+(2 $ F]pv߭<6|-;_PQDʋB(`-k@s|FK%kQ@qd+K)Us3X'NqV\s%$wٴJ#K|K'Ogf4r.W,Ϛ׷.P˕~S4$VwynK x37wVH?~)Hza;ivT3Br24>n*6=^ G>?n=JAyF+}ۄJl)\Ԛ@~M(ZTog<5`H g{0MFo 7֫ǯ_qwu0dGpBO*.w%-%EA7vc~ P$^d97Z %Nce61 ~ rM[d৻[oR[Xޔsif L8F^l.Kjn1U~,pKyQhh*ȸ3Xе2w [7Y5?i  >ޚ {s[p #woZfvt+ga@0IGopEYk - f\ |ή4q5h8";$.sڿ \*m/w/ cslW\Hlgs懪!Cq^{ei{ayCVm_kfyВz B)|pT_ @fUZ]e/;P-{w8 n\ƒ j|{h'u6j]K%m1f$7&ΐͩ-8!hY_4rO݂@7cnOz+ݣdǑJNnx4# {!z閝&(>N5CC4mf{ RCQۃ%7bStrwn7zB_D+o’Ekctt{+AiJzY\oXPDBM0OsFXIy™\V+Ih?d{ \ԫ[ ~C9 Pn.ƒwNeMOdv]"${؅3@$kPQ|oHS^d>l>ђdZSҶ\3vJYom:eW^[LoMNFXGPGqRs!`JO"6[ `hf!*͚6M3Rc! cP8݄e ;c(Gf"v(雒pd(l sSEC,~?dM?'ZRc/֤⩿Z[`:u训b~+w> ;xt./ZDa ^'?DW@|'O W)b&!P A F3HRAL#+8,֐`U+^7g om ]k4]|T5gت3§[@I|e??I~.Bca#b=/ͪ4?5mdN {kD][ͮ=[^qB2/lv ijg Qiȡkk3fE|+Q(>3'\d4c)0`fhcvl|a±)d4L,zbYv6<ı'AEKDN (ܟ >|ҭvV /.M>A:t6O)~b!FOگl+-Ϊ.g`Ҷ9])쭗ߍ(^/yJK CM?H*Ykͬs{s6 x43Pd.t%m6ǖ]Jlk cB4@f$.4DOOP, {ʭB'hf wnc)Fb^0uX}upt; "{*df4,]5*,yAC2 .r/ϗwt  v]&ކaٻr@] ej(:rOl j4۶)B{Iuk'sB/7MP50Pn!r%^`,*FikF]U:MLjVPK!&5^0/AX+zGxr1۴RV^ ݛkɛ~ BrcP@MHX ;OuQE*}VpٯB$.utʿi<!Fo><w68+N+p%V6 Jj)LحI~l`-2^VoATnNˁw2vk7v0K_9dy(apMڱISb `J_pYw,80φK#8/s' +a# /1_GDړ'B@z^$efsrdNt(c/}gQceQDM+3k 8'frx\p)a^ 6Rߋ tv`4ϖ:0A6avK+1@%֝2vBV/@{؃ dYBv4Q,|&@Y/(`,,ͅaIJ&nڑ&3~j;Us88Td9SmBAV܂>d鰊}]+CCjpKc7'u] iݚ5%t6󻮫`t 6ώ8 5pc-t|hRVE@9CG|qd ЍݪF>(3^pX(oocl3 x oT(yrѾSKHu*fwyWwA#)Ls&PW"G3Pi]1T99 169cƁgUs ܌ap. kQ25$d ٟqxoVlF&>xM8E@r.uV&Si ^΂|ɔ?7uKcb'[3%:KNxS]px!B9qX(.Ꝍh@Nj]6zgbD^D6<ȓ!45n٩/ 7lC॓ |F3Yx,i3ja fI2wff/h&Mb$GLu4hO9_c, Ru]Xz:,Y&?K߼PZ0<ݜe?Cv2ko;u{ rҖ9)R;yc?rE+Mfc5Qؒ KԮ!g=ra;Z1Z%SoQ /7`+"woV& -ɵ 6e?ùLhDoj<$ȉ6p AF\Jn"jN-&ExWV$rk{ ؝oc| 9\E?`s{MX种8C6U G|j-3LvS͒KU3Gm"zK[d e3%g0o+'?ȯwД|sw8;D7{ۉ1KfPdJjm؀}&N.A-$Xj]\ws]t[bKhQ4PWf2c,2~PaK}Yn+F'c]t 6FsC5D$sGA`Y̏ST7FHk)ylSqGw~&3sf:ϖ7%IUpʒ=@#*Hd􅓁jmJ}x }QaČ=>@Pu3Rw|h?hhj4އ2 #hJ]Sn̖uRqwɾܨS.Lv/+#{Ӡ‘uK@cdTYhGS7U#¡-e_߱jܘ꺵ɟ!>$NڀN!PrH-Tj\7X >9[/7SB\82U +]:+3-xJP:EY\CJh!XF62'^i&ǝԐ^'}}6kV =ui#rQ>𿋔 DxeЉ=p*DB Z% u APbh {ynH?sf|؅p$rE~I7pim+\-{hc nE2h}H C{kwlwZSz`D~~sP썞}(_!\Nkl} }:OƩZ@%k qe~D K6hׯ%^VʳpGr4H(' F?EаWO;)mn^|4He#CeM{6ӷO7GZ? WU~Kk^99z']ToSf\I{+ $  "Gz Y'#)jUؤBzX)޸ rG~s   0g|k@鰰PZOX@JVG{ Z=c N]+ϖl  7{+\3L v(Px-+zy%aq-t!04S}mPrgA3OZWyoE¸ ߈ 2j /~ ([^n0dj2<没l7/XP˘~{ނCUUFj \ av'B@ 3% | Sfly=f2qldU͝hJ;dk2E,6fU3n >[{apQ2XXnW/S^`n OUfM7 + yy˹ǛoQdfEX^3vzHu¿P`R ki6IlH!xgf [ݏ.mKy e SۍbI&h9 S>0XefXaj_X7 N󯈓AeXz. %!4ҳ]AFfK_5j\^9dAy*$Δ%YSJz[ݷO&tll> .\#L''@\k ^:A*,2~A!Jxhf!fQOylوw:,sl 7]t ߍWr*\*v7OO Vk.*^^@BIhJ֯],l $++pasI6,2nT ff|yYN(l%z$n 2fN;+lfPtrdžp=xO͉ťE͘t~c ١?MvW] ]yTsQ9Be=ϦϷ @UrVгٷm];qM+zmBq蕞3Z^l@4){ ؿ%ʥbJut V pZ"m2d,G:ЀF JK Wm)c3Y+,chr5M5Q7hѥ\K d[=Rߟ! Zc3W 0`RxKO/ .q3^ #]31mc4uJi7PGp'VFRijxzN3J$'R#\H *vh@:2\g>s==[AzX%C7;{Yw=.\g+쨈,.P~ Hs ̈́qyFWTCFָ PϷ_sۅ7VEExJ\3:JO9M6F%:ŕ3Ӯ@¸LX F*2  شi2!\>g KWO8kN. 65}/Nu E|-o1.t *|Qڮ<*3We~ xS汹bnt`._q;ľ7wq;a8/֪ A> 합0Ħ1iUi^ּ׃xRɔ"%CI7 n)2~ OHVB5+rin~/?0X٬yXAݧƶ<Ë侼\&] &kwwD`I +Z>?EZtW|AC^%Ǿ"Up,aԸP,^|g0_GsX֫[h p̊аI]]p^[oOFPa(OjN?\̾Wom`dޞC[\,8&hW 󨷪>I%ψw!۴ܤP$'uOcZ`jV<( ".q[K{ޞ!}'n+ۧ"YϜgBJ*oFrysaBoJ~}qlgY#Wn,nvwHWַʉ,Ha/Ggi^3 ?6 Zz! t~¦+&ɏc|%S-1a;5UǵvlȭxC4&ow x?K,aXI'Q\BXڙ'K!\컳o|7V9wzշӯɫkBͪ)'UCۚXM6y)5*) 'U~u>lUCv3\X]{v[4v?STx.݆_Mfẘu\S`xg9n԰\56COl>ݛ;}[V6_uк1w-R\y?6x蹺}3`mS Lu7gWu&]d= /+ bm*LkoTn7Qk~[,'&`K횝]1U­R]}josGuxVxC/h-љߕרGZ֯}cC^@hx*=G힪WSߨܕh[+jp#b~&\^_/2a9d@/O׸G]z{g(S.}x=j5p|:C| xOVa8_VHA9A8OUJƹ~[M.[sM}U@rKtԬy wx?pd,^dA Ia EA_X۶p ZnjWԹ8tdq &jmK׸GS?]I+\M%K&-Z8X,I 1gWiWz.n` ~iLKu1Kp {y1I:bB@HN%4A~4MBAimVV~QĹ:LX5vEͪK^I7!.L7^Vi?.N ɩ ,ͽ/no~ѷ}噣X e;~`IjDn ݞcnvn$N&WJ譽E-pU<˪-JqFMg{e54?wYR;;֕c6_ѷlno('+i317,&b1.]6υMpN|ORdaTTO~ Na0'{װەa]O0t!ɚM9?&o%Fv(}YfwdIHXo,H@Ltzijbφs/b|M)yxd(ˌw/cR(zuoR?|v+J*?, A)5|Yx L|.L?k+}pW/YX[|3[n$,+uV8.1r5E}<#rMmj2yqnf/CN&];٣2YZ?aU~)>p 63ILl_&;!wC4Ѿro J!{椾qC^Xm-p!.?V}%Aw+ZɑOg9<ԹYXKϒXiJ 9A/)TDyh4 '3," %lA*fq̋`I7VOL#'8`;'" f`w&K&?;ԅ^4^nSiCnIr}^׬fTvP{{r_.\](./vʽɄz/'7 $w 8+ק-$7Lz/PAס@Z]z ?A ^X [/".G |…FmŠQGVMI  DxׄBua'TkaV:iǓ 1x(x1q52DFV8}Vp&O Sim"꽩) aƉz U". B jRWłX[vpP i=I}p#LV6|հe:d2=F͉{'S~^m^;KT51YP٦,)&lp#4VjFgxxɡhҬ%^\+38;Kڪw .򄉚Z fbF֕dy-+mi]w¬z7C}%nSOW"~ޕC :I=x'D @JaCl.$]5X1'A/Az)npr2ZG6@o~5T"C$wMxQ}z#y)/.]oך&yr~ ,(a` ȑ5фte +=RU!gQ<krXKE8j:{P@6? r B*F{yQRB9go6p? IzґFa6L]m4 4!BH6lϞ"֧w0 VX_WmT۹-B=Sn2 6 u^} 6>+wpWz LDzp4#/9aX vx!>= KslbΩ)}zyF ,ݟ׸YNqµ#5L<HPYm4FgdFteݑ# {Ϲ`TW9(g;7Ovj{</DY}G Gʆ+'i#y^'CB2J|l}ws]?, 4ƃDۺKϡ׾|tqֆњa0쿛pΘs 馬m-Ɋ^c/ZMBy(Sufe9~q"e^{AXJ&9r^⻊Ff(X Ƌ0E7v6[NC- дĔiYUbAg}1ÖQ^&u#KL~&d̈;~{tPwOmEg`ml\0Ԋe|$ܐ%}+Zm )3(.6x^eo }wWw{GK'gj~+1bb:׸/jn៯DuԸezN\LPr AhzJbZ֞Csj\W׫]4aA;wភ }B|7|U^z~%˄?W/B:.Q?kU;U_WܞQn nɺG }^ /_ |y:W7Ups!uU\WAjDx+x\W,/ 7Azrո!DcHHHkr{5F|t3+{r!w}߀ )k\,1k%**sl#:@Q'~o JLl[ Bw&o8: W_AMs1ۺ,IpߥHg^%Q+ HX]v uAg}<)+R3Њw˖E%Il4?ۀ(ijp.E|ikK8&ޑ7V/ذdB upCWC7[ٽS>a\=9>Fs=f^9DbLe+0[%#Zi ysېjۍ8!%:m:X,_O4Z,HxEVƝvr(Jf߀bǒ+_cGo$-=n7 8y4G4xI=݅87I3&d%A&th'haG2$iZ[ ᤁK̒۱% 6[ znjO> .g{p8U+ctkµg2mioFMfXAmQ^46m3zk/8hpKnx.? r@]oqmyc77j}jtX7CU>S>TV{ =skq|>Z'͜ʺ\zpww?An .! ')K{>nP[_ -W]Bap %'NָWYnLcKQe;hSwxW0^dOh0lsl_]$ Jp44+pLɬg2G]bE:k#>tKdy.?7&BɘQI{gc;NUz|nZ8T|E۷T+^ iuẃgWM[L߽a>\e6ZX/( sw*J c!uUZ^wݻymnIOjAH|]!hNO%*,ԖgC8&i(W>ۅ5)q0ݫ{L5|l9jX^Xh"GjH]AiW#JRe 4߄fd3+l(iIc[yvj<0KgW.{iTP(sEUn`h?>͵G6x@OwwCO2~l"}l'+}2\q/ӭ3\P`[5˼ݴ !뎻k&8Xt 뱄Mp\]j`F3لŊ[oZ|$ֶ:~ &2v7Hy˓J`rSv2%톉6V>!S?E/?s~u-nYcA)woEK W .>s Σ|˔@P0Vo.a*\{OŖVq5Z4,O]{[-VcouKhDZF? U"+ۦ?/{Sd1.wVɓ\qEIpJu}& ]3n6Q&gָ#Ңr$LWzՋ[^;fQ'(|swhEԆ 8@f?O;EcL֦a/Ѯ ԤSN3l84\ZZMxo>>Lu׹ozJ]7KDS)t@2PjN,_2=r2!_ 6!Kxڬq2 ҅%Ho 7`HAeOr9}?~? k}}3&*>UZ'7W uk1ZԞbI]=_ιt+^\OPӛ}kuBbyz56>wvqDtZ0O~֫ y׫iY깥rDIQ\}qpԟՍskX}jw@'x)~!i$q0,߸cxj[_Zz#x讵ռI{зZj+ )h\.?-BuբPVA<. sP\nG}d_ haUkշ :ƭx%Xt&Q^n߶O jLw*P؛Ux`p[ͅ( +N\ti96ոNn%@MN$!bL>_pM}< hK m%|3eT'yz8X4HAjA*>73@)u]0!X.z߂pEnԗ|L/WL z\]ghFUZǹ8Fd2cxBg ?>c%dWQ;2'vxKaMw{ ٪0'ķܹAŷ\զK{+n}\D-a#M[xh~I]G{7b]7s 5"SPf_ HE/D8m;" _t~'A<ܽ~ZB0C I*upMSXW' ^l="cxcf nX4[ն$()ɣZVqED 40_o_WM%w'v/4y TaO椧ܟ?>~h ~A.gM߅3t@ؽ{ d>¹?F}o$ro'3yi47mz˶tUW6*5cS2p'H qÍvg*v_L3-/٦KNSPW@Y!٧ &F Ч{+/'-tgae̔u8i>ys~ 7o_-7<7)ظm&>mեk};` >Á  y6yR=pW⑦߆q<ɨ%pl5fbVU%kEm|d||//'7/ \66yrH}._魿v'/3 Dִ. |wR]d%wj/vZ;q]*fW7no:fca. Z>.yDT&HT]oUUW;~Z.KPݧm΃o O ik0% Swq%{&UbY,^Y1 #nE^i#˭x徖i) `w ޛ]U]$֩EҖ6&ZG$}—b~C[hvNvdg7je]Ya D{ߓ9/}M-=*Y|u{-ݫs~}Nd^gGQ,|dO.Zs֫I֯gyוMºR^CV}&->ͽPl3x;wzMozW #Ε*TNz^@{|˟ :wÆs[~:PNR'D֗ \OM?xrKt|Jsy pgͻ'{Ϟ0&/'`Yižg&mN >osPcۑ G\-~EtGseZ&ihnFˈqnOz9_$z;ƒEjw7q7d{*[Vpr_~Z\&OLk/9AoYm?4 [JLs]"kC8t# ۹$W]zZtס a݂@O9~fp!3<.ƃzpYvo_NPō *ͥ+ωzoxgkd5$!O|e3_.7e~dhݬث;Xpϟ֜̋>7 7+>Ql'׈a;} 7}ܹ#{2Ktꑷ{|Uߤy d^ %nԬ 3T^@Ov;[y3:Y|9wiieRO*N݅Xݲ2pޕ=Grw[*8gӍ5W}Hֿv+Awe}ٴ#]pYw{ͩqӽAq}i\pguϾ /gqN+~C&>++|;vm"\}"q싷ѼJ9Z~_Wi{@vD]պ("}N.\&ztF⯹sty;..UԹ3=ɴ=^G4{Zko.'xHwL4`^ Js(<ѦuRM&6=O 9Jjš#W]w.Oq;}@D̿m9ao(S1~JMt պiV]|VQ3o58)m{΢ِxUӾ oܬ7$$N+w3oh?׾qm{ hft ,3oB7!}w:W='GN'ߗx>7x}Q߈3m>E,6\{3;٢G8#zlGvo܀H( gZ+}p/o{P6;fT;(9ջj_v(-y ]^33b6,Ǫ߅7w\m_S..꿛0%1.V$x@ރ oL1Nﵼ4H!f6;tǏ׶WݘO/w{Ed^ pkLi_w@R'WF~[7w>o{.KOߟͽcjVPpƈv/q0_/I`Zl']U ~dS{Wi+`ڪIqv7J*=Uӿi̍PpceiL&ry(JEoijowfMGW'Zc^ /M_-u{WWɌ+AK^-W/Wh :7;ШGK<|&P-F ߀ p '󱦓p6=8hOO0Rn8#t}ǩ&7jI}ZkEX k-[Ţ!wMݩ^L'pQz 6P |wG9L'U cB@#j^M+ ɧ4v@|a:}68]BdESF7]-ua(|߅ ~`,F+S_hk: x P5$.8N+1.] ɣf ;nۧ?M7_@IOұAunDA~ +vmd kٽp@{v4]yT|x;Oڮׂ[L5֢š zg imq!< GetsPףoۼ0˗hivC8։HzK{cOw#>ĠV70?{ſ8ic4ccFBc#Shph~iFfc.sF%z;H,Gݶh"9fӱ0YK7; Yĭ@Y]vT oG:GW >v8߂,XO"KpWwƖfX|Stc;~Ԭ5A.jmF Ḱ,\zIߴa -MSw6jA^!Jϸ_c߅6)I3C#hfA6M4y-RI^l\!%MFF+JNtw~Ru}k}jNtϘW{C,vNcˏU߄GZu*xbԘ;u_śW^Dy2`suhqA&a צ<+rB= o=àw 7P_~#P`'^TK}^ |[{(*8_DE>mw9%JV¸m=z4f9)]#XG¡NN+bA?ViD$[|65z:{woO&JPtf]߅ &>TvꞖ3Ȁg.v y|34.<8MC-eK]|Wݻ 6E>iTa =iB!T%ξ#ze=F[]dzmC!_'vEgŝ%^swpX.;Y$ͯK'УU|CA7i/? =_fCXq_KH8IAԊ]{yժW+Zԧ7*X+mbGx]\Xz/w|e[!:hNkO{IϚ۰ IdnftW/L 7Xc#+׸!_h~Jbɜ*[>0"֭p+x1}$\m ?I}VK>M9KZfXo03ST$ '.u%Ndg'ߗp&0>?Xlws|5:z#_Tb=h#i1͞ۼvTWl+;FI&j}w'qN.BU{O%֢:kw[q,W"zygZ %<"{i>zZuRjֺZ10"#W{|҅:wsuM_}j?VʼGWsn/h17pt'Sx_a \}yt˛a'も5_g}D%pEm/h޲fV^+hc[[ 躤k}K[ex`Aro^zNnyb}r^_/SG{][,.ETwo3M wv"![6T>zpKhVR/?K]aL~VzZtɭ*M%IL,HCbVLvzMҷ$!}Y &V\qqN I + LKK e| )ݿI.S  S.^NFz򄭳$:y_a\O?M6͉k\⺰F6Ꟃmic}Y};;*JZ\ܕk 1kVF tMm{=}榿=o{V(3>|נf[bOI (#5YI!Kv&)Fݺy{;nށP"KW$2VZdɽU#4vkm@aukG/do4H~[$k{w*=+WRwޞ/iέA7/藇_~xMꙛ-uHm3fkVi{A7{ͮ]ip p"ܾ>j&޵Iaw56"[nvҭ> B{.>z\ rﴯdYMzk8ӃbM. Q2;gVn+HnQ~ %5v;h >O|]~ d'/kߓ_ODh~4~e|EKǬ+Ys }?wCQ]%W\3Jz7\7X;oҿ۴xg X'ۓ/C"\Lտ S%JnW'Un7?M2߅Ăw,8Vm_{qQ %U%OtMߖJ$ն$F{{} u&Dt ~K[ ֮&DD~\oq+Xʂ׋'u 9o.4cϯz~GǨu>L<xu /͋Loy=]t1=z#D֠V0 DAscNUheZXn?V(w}$)RhX}B+`[ZR۠2G _w~ BT 1_/+UMpwek@&$6M `W)} ,ew2#_d7*7I ebu磌/ ؝nX aF%4Rߴ> :4C-`cn XzVXR;M15/U`m$]x+[}Ǐ6a\GM@lVX {5ưwx Na_dž=#OMYF$m"qF^[SRh*ǜ^ nܽS~xނ0OI*i%`H})#G % )qm zOA.?kauFow& ,LʠJq rAG7"R^pm(tr  4D)Q]~QMcOnjl]W x;۱g Dpd5=oŅܸ4sqiXa?(TqPMKUn5>^F `ԤIZW|lzJj=\h]pQ l|<.a3eѼ؅*(c;=٩I:$lMKj30XӚ;W|*Z+Mq ٿ]m4H+u9hv:3QKv 1d.)/{Ĝ`JQ||mg8i0"4siݎ_ ͣ(I =@tɑaPtƕc]a2\[E,loE^b3*l; M,DHM2=(/Ml%e$.r C$c(lާ,$ܩ3Rg <=iM[4mX6X[0M3un@ ɩOAH?Bz|ᱝq>*yhg84;.3Cl١J\ SoCB;,kPi= X wYR$4HoGI4Е"59U? LGKӚAg< .6~;FWt2iˤ޲xE;(oF&䂋޸+ϕvLV/i(Ѕ 6D3kRIVM5u:r]9hfc~*2}Vsx?RC]'*Xq2ݶ݊`|bVGx-z}PSJꛝ[)+c(1 9B2_9`'>rKX uaznWn9>bV.mmۍ;?yx^x!͘W:Qޏɽ n~ 8L\&m+OTJV3Zߞ V W*I9M5|1D.~t?qlNsf|p4nB4 yRñVINѦ= DX5/4Z+mcF.;y nge2S+MmR77zA0r?wpEy|m^ X, 1ٟ qg8CFxEqpYR ZQg#RRqحn[PP_6p ,\4,D{|}C"o\O[uL~P^9/=ߑLͭ QBN@ 8*!)h[[|n 8w{תq!ICGU}{Uy[TOI1$ z/z}k?ı=WzױDßZ^ >N͞µ ~?/b< >ܒUek{^l]{eEbx7u]_4~}{"긋ߌѳnA.!Faj{yi\>Gw~֯NRivžtd/bwQE5Zׯ<@7֧W"a%Huy;;h/JxziCڌ [pxW>j AC@ |<lGv/d,TM(&LlYXOhy6$5o uM\\ߚ3T̆m0$4|:ph@M,)`C뢂.֯,rY^'cդllk܃݆l/C`"J/*TߢGzj/ $"!4h6kR"A|yOm$v(Ouȃ0+QYc+]tϞ]wڍީeZzҾj`:A6U 4-ՌCs\Bp;KPE$^ZppIrE݇-79 ;"a# &^n4:ߐpJ:183 A8~ qɺv%{&=EK.X$ե@|2!OrUDJja 2mHq1 2UdzdߔUUfCn[Ƃrq>+ϝouȬ Ry az32 &b 3e}[ஒi͍mP*Y\ .DƆ$'|Z]Xə$(TN؟,n9s#7yDdֈ#N3Wp3;/ 99Xo*U# MJaE{jw#*ge>ygn w^Zv4Ff:7;AecZϙB1avh\2ҙJ]&(б>) j"ߊޮ5@f{6ZuXam?|d?<0QI)lM q,EbW0%.w‰ KG0V4ePh #Yυ2ڗdBs詘1]ڶP>o->t.]VGD7vdbjMng5( |5 ͚7`#ߐ6j:<* /hnfÊIi@ |-s(CSE 1Gʑ?ɜӋyZo{\!xN~yq>0rIBxQэ\(\,HF` Rcj0P% eI( <Շ*"ێ\lPM Lz|T+-5UҦL}"1CU\ Y3GE`dV8+tyLM~ y rtuG~CCWGR,&-LҴL'{N}4oʕoME3I&X @x h]6_̺+pv@EKA>^D^_ł'tga$O{J8%EwnsT35QQNSn~sQa"o.Ћס3fa':Ǝ@ :xH귻1e5^Vi8 :r!.'%AulN]"1 >m`ufTJT C846XIkP6WZIZ{EfD1O=&p=l[Ș͵B1aȮN@q1V^hWt`EuI[R |q_1wsrѶs} C10X֛Dž͎٭;<9XLe(kNi2(墳9CL{°]j(&u#m@%۫G}t`[nq+z1|3OI7 [PfJ"mIKrKIkM@ d7i #)q0[e9XȦig_|~V.rTsCȝXIM6/Y/C!tDZ 3`܃ҶﰍtoԾx몹KTj>_O/ (")IomPn3ij DUܕNOF>q6M=r zUgͬZ~ {װ` 4i>s|8Xm=BG/F1g3 ?e90o۴29=UZis"UΕ{RcJ :$*GĊ4'v2(KhԮ {zZ 0%θ `hlA=5m3t"k@ʿ\v2S+AgjE2Dӊt4liq; 29X [h`%JGv]>|toW. a/pωגPΪS~ ނdO UX]r62+s{Zoފ|%dԊ:4~^nCv%VLۤb3tv Z?\>~:]l?':3WKאBr_p:aI~4"u;^/D}zy|nHg^׫rc^~y}[ Q֤rCqq"{߈UW;І*O z_ U^z/2AQUj;b8UFt_ZE9iņpݼ +awό{>'¸5NFs Oe\ Ly汊QCkY?Ă<+;*.7R/ |Jg]y*\,\3p?pgcp4K"V4yf[rd6Tv$g]ؚyݟ8ԯ-RyXM/u9/~]M4G U .7\?䅯>2u\xa{ /`aP59co yMpbr[_sgʴwVRU4]3..j_}&%ܿtWn=9wz{}}r g{p[hMR>hz*wM %Wr w4o 3z]L3WT_$fm^vp5ڪ]-"+CnZi7_wA W'wp+MkOcvcIޖ ¸A0iu,"@fcymmԇ&KK\=ݽG6u|mu {ߗi8W 8NmTB-+vy=[.6tV8ҹ4f>+nbr yS .)~MjwXy NVa?vziᴉd@jNW+V5O xh氱 .+J~>px xpD:}YhrW`ѷ *Zֹ\oA)ew[I&jvq_WOazKJ" r9u %80dZD ƉK},3 F2M鑯v9ƻ~[-/tOA FOZK/{p2ӆoj 4|jUu`('U\4ئ5n;UڭsXr@/ɐ{ymP(WzM [ 5=k>p] Z a3_ ṿʰd셵]pGmi'w^Cg7@I{{<,)7FLsz=QӮ Ǝf% Bwsu8uм!O7{*Jmͼnݜꚱ]hmr3K [bP/0.j`>n_@oN_շ{ E(!t7? ލU~Nm|'j9r^V"=)`ށk"F/ ˆ2ÁbWK9ZMpUsO{7:v]}IL].ӯR%cxİޟi!u$o'ij%kƖP XWfUg5 Ar׌jXɽeMZ""(|_wzK7w֪I#:䆡,WsE[wqݎ 7.}E$S.tx/&P}c8s9v_\fu9(lvom׾|/uk?^97Dub~w%zG߯rk|U@nk](G[Z7/Zy֭Q8~֢p\%C}j : R jjբ2 &AA/iXL" ;fz .΃T8F~&~Cny| 9(.hapvZVk u)MZe6x\ k|tM|+\KIס$ 4݈s@"e7t n~ZW0WAo1&\ ]B`]&kɻ)ݬ>Ꞹ%`hMp(PNIe53]m58AF2$NL_ 9Otakob/-(ɢvIЀ`+ q0Ui'O'6]HVCkP0CI;8 (y0I5rسX#/ 7O^/@Z׎0Kx%MMaKn9Jٽz)hZuRD">J%a]D?մ.NJ?f#`IpE{$p~(o}ANj$\AOzw4HGڪ]nHv$!qN5&O6f: eo@Ʋд++T#wA~smx `acg'jDZ #k$0y}p$)U'+ۿ nu8oP}+I| ?}ZB,O762d|Cz1kH"$$pyFQܠ%a >cuz;cV'0kCt,3G7~˅8/\P|Vǹا4Lft\4n} 1ү j23W;5ؐ 撥su-$љ 4Jr^hP!8Z ͞ҿ;bbc :?],<9 "gH5k`ʪOj/gBZQ6m0>uqL;֡`<5_AD5Ưxcmc(h֑cg(\B|J8XUY9v  wܚm*8T rK\rN۷xҵV3w8^ZӴZ+XCpA]UZZ!wDSn qVk>V` W1m! 0B5Y6}W6wM* ubS6p4V0h&>[rԟx@4, Fږ U',4#_ g+Iݓt_bКcegȖwm$k0ocwn iU~<_ 8 =] ߋƨ {CO -G/v )nVZA@-6R] ¼dV,;x*lĜ<>or8!TC(9 |"ZPi?@+D5;W"$ϊy'P:E,G9IJ9im,N|vFc/'Ny%fZ }h6 yCт/,Ȇzm47s(hSKClѦOXFށ(T &5!5N=Sf4>흂W}v F}tXdm<XPA?'=vX"O wmvxPB tORFmlZV2_ST|fhjpCٍ=`+{4 hr8?=WN$Wp{/oL+\tG1X>~y,LJo]X}ba,.C3`2b7/Ksl# tjZp5){%״iA¦CʼOdZ]>WU!|ȉ<(xgO [`sJ/-oQϯWv]a Cs?pPJ"*kA8 sUm!5w]+w;y.~)S,Fu׫k,[j* f -?_\o! :g{wUM.f: _y}j:˗[߹;Ӎ>2rkVҵ^.j>I)לcPbgp}ٛnwRU y*.O_RvDp}QWZIz,kZ 2˅ohow~Nb\oJwG/7wSKF|yopꯕ$(SS4>do2Mn\R/3?bDTqGȱ0o эD1M4zj\2l&\f;E&p~`OsqLu!}'1Fޞ`VW-crFjue]>x]ktk9? M#DyqP!+m%C#|pÊKM4/揄Zv\5_݊b INႄ IgoTi- VG9{AbV-K~>U 6d,g]OJ4;ׄEͨx@V(,~Rlq6:w=`y+{LhP`zny< 0MdeZߜ]v3L$,8: / s;Y-w@%Eeg/LV0'H{F%Ap[V7liBĄoV|0׻A;6eMhVR<{\"J6 yY%u#8A6ԙmdnވ\3t7-"J{?1Ky;&mUij^㇛.`m7K`OG_B3k f8I͗EaBgBvrخ=˷~ nk`SZf 8Kcd,c3%6]0@SY7ɽ*`C~~x%7[R>>SErZ1R 5aC Z/qm qID|A^ />vҵyБ/k[`$6Ѵ+[c2J(T@UU_zSg < .5 |Zw 3 qꫭ>%0Iap`󷳦.^ vk-;L\Xd 0f^X n7ոH>KdžpDY.dXg_ c27 {NE~H7E 7͞6wRǫ鶊O93n `q^FZEwkχ7%%0]Nvݴ E6tqHH~)͝W%ݷT&2^ s=!x9Lm dAGxDy,&N>OE+lǎ,}U=%JohQ9?PGUÉk7|@YAtO9MV[5u#ZTwlS3/-R{g̘ɏ1B2#9u!'p݊`ߜ&/3@wo`ٿɋ&W[~AD-&bVjm&? hyġ:/TM{RM}I'hATlh%E,Wac X:ri44A vt W.PBŒq[Ii>($ {hyA|_k׋}-%dEn4>]E!]x*db =YFPx?pB=r; wߛw|ϕRDD@5U X꥾a|kV&;}"  ѴЩ~ϱ ؝wQ#]A׽{{$#2ͮx/&A6¯t>'^wYR3^ -x8βxgyrTq9VJY*>}Ks{|_WRLU,SuZVgn7uN .TȺ $_<`W8eOew0|.ez~}[Ww~¼?#X~%;V>OnNݽ' o ]_"\fZ>$ËM-_4xLgIc\+JС\n.Ӷߜ.*N3`Ӧ=v/w¸&&msKݯ=W ap%Ү+ҽ&`ͿM?̍I_خyҧŅp"RA>p VayAu煭Sĩ%g$w_=USA^2~' O/sM2I}"%]3R8g--yZ?]» $e .]E (&f:&x#ܟ,\3t{ke\ VpZooR+/7b쬵W\oTAoB[DH+ RtU Ù#\VL\>zFpԦhޜ.7I32?hyEvw73@jzV~aW6|O %]Ofdԡn,OѳMBUG|Y24 .3'ֹfN_Gkoߠkfb׸ 1:o`-9%clh8,'I/2./# ˤ:*ى-vg*r/Mpiب#j̵фn|q7kaꆁK5|Kc{>aSD5o"h#[3{9NV =ݦuh>4Wn[d3xg|׳P0;:& w}+w͓1.&oFjy/&2n&߸$sAیܮ_c$df*eyȏ>?1 4H 6lr!] Y[T!8kI %{O£U.% n=r.cd~_drVHWPE\63P@ ߩ_eSw3}jg|4r_`bCoB$ڧ :r,Cw[+ UZHu?Tۈ8+TnDMZ6.|,Qmm^>᳂- H5a("4'|GUF[za1"7Ob(|[,y3pGRފiǒRCˉ"k.uO K*k]ߛާ'7aC?φvDB /j\ha=5Y=>(L˫?6ia, ӏgRC?/?mD7WύW=_7 Uoa($ +K!ΆL'{z$loǂ>} \ͪ~9CDAɫ^XH¸?+tDԵ~rZ^p0'p:ȽӢoۗW9xy\s^E]O1D뱪ܰO,(H[3I, PG5;mt^5'ԯ㕰 }N=.N"^ᅥ>Z`u~SMC0L5W5pTY=Tܙߊ]^&{58mhvsAkOѺMio?7 ' ƞ/Mr 6qJ<}{ yz*PGMv9ys j Uۥ y_{  [=&A$"߶}Wn?2 r,+_KE^r~8a'`p_a*}uϾp[ J 4¸M_KĆeNޟH.8unOt-Xk-SGKWweFz|hƪ?ﶻyr=۬+W눭W6O…k yPL/ռz yykF2Lo|HZf?n\op!jtչB|c֓kgU`?xw{: ᨻjaGUOi`!9.>%CQA0D藌0#nd lC EkϘ%JoNE#aB%1_ߨ6V]+b/yq]7 ZCt_`KG+ .'_/7\% HDei|!)Ꟙ.IhZ#vzwO;}k׾C|zIinZߣ?ɛ.g ZW\f rC.k \Vyam߀e**w33Y|3fnɟcf۞4s ͮǫA jq '\]޸t$j59s~KU ;SO6c.j+zu ii7?#]o%[W 㥗.l>E_Z'ͅ`f=+6$$-u]W%UPQ?U Bpã//U4g _*~|WIk _YhhE^^Kz튭USUp7TMVfZ{K$Cjaxhg^$ܟ+#Kt˞^`O 'jh IJBƻ.~Z#yB_O <.㯪$m4L'DU:gm5t޿.ayy& Z**|3p..{x ]'U_ek˺^tV}]eٔw@6'?Ar+ 2δǮ_O ú{d|7Gݳ|qIC9xg $?eʓ,'jq7u`ut~e2?˱1+ wGr?abB"1.OP%m@Ek[eA%kl4I=6M7XN2orHA!rpV{v;|qOOWo {+cA 햞~ QS„nE˞|m^5K&v׹ڝ˪+~ó|Q*xhU]B4h鿎nW#neۮ Ўs?%Ռ4HD(0w@wۜ/LEt'? mj)Y3 \w}Umm"*#~띒ߦ=/`vN8RZ[pk(OZֹ ꪛ|ݵď nf_}+xh]W?,Mx]8)YUN`D2s |؟%6g ~M>N4Im_-<.)ǧޯڪp03oZ¹4-ӽ¸7Ce ~}l3Am/|6֘{3Kߡ zTq^1_ Ovk+hB- 纟u@ [{M6޿~ן??9IxߗR7}7%& }UjLOטw,V{\5|ޡ: |X_^ 5A+}|541C;Ž寯_;ԇz;NO) !ߵY/yN׳w;;O[wގ|`T;7<'!҂ףy g~y7p {A@BP|4ŋ<3V$ID ͊^86m{\(-~H"mpGbZSl:?U_ P$YE`)R|P)[&Z4rs-0$cU:RV8kcJ+w0N 53c~ B +JTTHN `_Vx=.vT31A&sj+mQIY0yfX-,9,hyt @$:ݍ˟`H00 EOsP}'fG#}<@\0i_~ FM9lht}6Lo!g}\Q+,Hp#;n@Ⱦqu 8M6a,N3csn5isOas ѷ h7I:.b+,1V[y43W UV:>llGƯEBnzKe67wMEҟ1dH l7eAZw\.4lgc5iOwXjŊ﵄x% dU{~3x\z\;ܧ͞9jʤuQ¡): fEv -WZ +'wG-&HK-&A2$&{k`$ߑݖ%\Ͷ؀ʓjN>E`S56(a+uZNpC͢_"A$ ީRxwNn^aަq44뒈nP$w{S7궖}4rŪe3[J,DmPMR f(mE>1*S`oڛX͟/>fx@<<08WMvȰd~b|C۶Efdnx*rp2G$ʈn/ em;$S0c#AǬ8W+_ӏ{:r3v0ެ] -(n=-vGA0Lv<(\dq !O,nj-.8.pZ_1{Ҡ,O4fwW2['|bUiIf5Us(ę3P ׻0ce8OZZ lg$t#(,30ZhtP]ݴs=M}V~.=cc f8V !o@W,]U5|ſ@R] j[}reTFi= b| OmMq5 tG]BŜ+M7+M47p!1V~3bv7B8m^s_|DojJoT>QMP*~JYtۦTA[s6U=@N>k\h(!urQ1a#E7l^fmU_!oCz^mF2%! },[SC'tBHJGTɌ97`F =Gte@.Ww Ւ4[CcjJM\~{?9w\čWK\B51o|`]ݺp6o'b*w &FFg~9ރ>y_?U"4fxilUR3W{4zE8@~O\q0>(^o8<Ͼp(Ai*\;ܘ~r+z^_{8B;Zc0r+`C*؏~*6gOnXx_ %~gW/^O~7;uCaίr|-/ouո@=H8R=JRI}w7^q +7jX;̞غ7W{A&~T*ʯPp ڧ(gĜu^ s^n_$1shc8lFnn+U+tJp'Iؾ+iW׫?15U. -K]+޻׾h ~ +'I|), W^}nK}[MUz߮`;w_ׄwĠ9y,GYuiW<%# pX[&*' w~#ϵ' ? A(42HlwMy֪lyx%i07~@>Lӯ8`/(`_e}&oyp>7w3wMd2YR7f"yhǀL}z7rA5ٻG@ ?wc5@j6Fc&_;bK)*({AKi70WMx0Cp&$X"n r iHQo~wzگp(T  #G{^0"K@3 7k VO-|%UғN7fVh'nLhCߛ>|V;ɋYx8scљOZ`+O|XaUЭ5NY`}W>?wјkiiTx $T J7H3P 4%#m4_S5״h/~9#捪t'KF#Q.=։%~qv/D9زٳshZ&vpNT!: /CnT9-X5Rf.?X7Lh7K/FLg6K{.c"&c7&l_~N kRպ>1SؘO(wIZ8t )APV]X|r\x3ܮ7ےh'|N,Q/ #җom\Z̡?v*#N 1OBCMykQ͆0NI^xC8mO(h'a!uAf+ ߠ4h 8hTzo#M)aGiDZɿ;j;Klm<+ީӰMk7C:Cγɖ;7}30|F.MG%H.6z.[!p:N0K<dXj#NaUhQsWNS+a-[NvJ<"0ESop_ʧBSn4#K ZKx1wcuLsWb*LJ5rardx ҢB9q$q<:C!R^l3d7I!:m@pIsK8> #+;KSyEmg헺sыYFvc10lyǒ 7WWX 'K[px_Hg87NWG =n&ܬ/ bfw+U@ Wo˫2`F4m-vuČ%TٷW#:' l2ѮZ_FV9`(j\BWd58 ~35B)):NX~`Bly3lpfvJc&bLm+vcz!d<Ǒ0=|(Aoޒ`9('I?FV'i3`0GJb %5i,2 T:kNG{YkV FߍW%tK2VEU0ԋmXr@n6{I'ZAVs^+:;yY:eICFR8gYȱ;< b9KdT^ +Q ,q^٘٥Y\ѼFL'{Mdm]4FW2I'Mw=) _QEl"!@o 60)C*=֙3իmfaEA!4\j0Kc!}(>]L끀 =\hͰV+vx"|^>'-nѷPSk%Zr4H0Z#[ ˭7/A*f ķrV/-yz+aY`vp]$b{Zz"Yg:6: W._f@wn }L<_O("/=eRc_`Ke0+]+Avy=mюFqFU:KC0ċt|o93 2~BW`bZ# Ϯ׬RHY0i{{@Q_uW%LadR c!@dƅAw+ Lp8zs-ɯ!(({Sj3ߟKYn0紽W^׉3Sc wG7?p"$`= {xk7WZ#}MeGa7%hZzt./; ׸[aZ2а(a15*AķV߭ua8$„|x$ x\kꔜp˞q'GwA;F٬sUkV pP : ׫Xw|j^ׂD $;% -]G x _ꆻψyfn^MS}z+R|)J/*?UaƫEb:pv* mdatAErgfռtնw]>9Op$]In5s; -: _Kּ^wMT:q^("~« ]녉cm߉A^Rw|c:kw K #/&@Ԃx{u_Z05I{\H4F͟G@۬,HN.M'[^'/4dDzxW_v{p}[F 8p]` /e2כ9W4нsп[Uri]x>2Y~ʩ-e02(&뷜m*YDŽo|ľ!g:па-{CܠF%>xz6/J`E/{%kA٨'uɫ&\ႉa_.A$\#7cwwLq!X ./^?KGc GOju/Hqo`_Ftߟk^DnQ5t8j~ӖmXW&_5{y>? 5'Ƶn8OtޕY.Jh8 dL+Q#An?ڷlۥG _WWܿ~# {#P! {:p@w/٩&>A57Dm}7n >=뛦|ATlA?U>ăUv贇xMw8]Gd}kw|ߩumGxiUΉfq+4p oV~%cgW8w̓5{]}J ˖J鍟$o\`=wb9/KsE_+ wazc?L[N#0J2/Q`*nOy I}]ݼ Ŭ>iuaB}p*@[wf K]~I%wI &ml++wL*Qә ̂_RrKO7~',BZhCM]'utm ќ^ѵMN*#Lf bl/ߠ%!e;roLMQx\'|.+Bj[K;K֣`^и)VvXAjNW 1u=pF7qO >[:6ү)?H7g`#F$b$2:tt+k\MWw->E\*'L&k/Kתպ {䥽z|RₜGj𡫮Nǫ|:&)j/> S xI;xX_KN36Wv7wAE ȊՐ%suHܴ᠙IТ넯i %7UX:G5\۶׉]f&§e:`zٳ5L{zZ? FK.C%j) Q18Hwv]j2?7$+pGFV+Ӗni\'|'ZP(B0PJ-<": A[\?DVA asu ao)jg 6A;ql43ю U]^ICGg'\0)֯G7GY/Sy 4GUwd_lo$'.Ak^ր`˭p (<S":WeNI}iV; 0Q\UL~ƉI&Ucuvf7>Wm7^ӓ5_/b"1l4 _pCmvFE<ԡ~Rq] ǮC.iawE"miwg(&۶HA&uN%-yV ᖢi 8Z7EwnzNZHj׍sݍ$Z]W 5- .+`|߂!&9L!ݫF˞Ma%aE_l3^|l#0r\h3f'v #m ͒ dY© x\=#>V]3sԿ6?<;0zmciv[.,̊u|s$MEٍi6.ݺc8}i SEknfe)ӛ} \MnJ$ ]'E!njW0\%&/Zt£XI?$԰~5Ωo;d1Fi[1B2rKIDhx`_|к4]gk>rmv[y5~N^ :-*;Au"M_jmP @ѡ(6VXzATfpE~cpO=)S꽰 mf{OPKa,̯ b/^¸v*$tE&&ӆv ΗDjg?'i3SI&ZL76to.H)G5'^Gcszyl]4e5JxQ\CF$q0ŒeD5i9a^;}C-g|4~]4»d`K{ Qk>[}wvRNr5d_tMnt]. Nڒ `۰Hnb{>X OO>Y8r &F= >5-jȪ@mln'tj (k$pUzb`E @(e `>^T~ƌʺdֻjL.b jݙl3ߤe^5|%1{ 7f'9?7p vG, 0 zªtdV ؚuevnȆfߚaV#"B\kF.U a5Frw{WQTW?Oνu7;f"v+F`j@@.͡^x3;/uֵ)^5mFWw5^%ߣv:{_`>s@e5wދ}t}V!x93ya>@0Ăɨj=wj~:׮0fuI UVO:ߩV'zOrIp =b>n\4iO\lea^|}^zuRGu }3$#wd ^Z):!} xc@8`U/ꅹ[֢_|'^&+$0_hFB9aqn;#ak ouv $A(Dt| HC^ӫ=kNoDWwg@I]TtR-h˟ zmQccLP]}<, \j6̔0OsߛlP)a3O| [[m2CuN |ovrܴJٲ nhOXQTӳƄvQ\`G2PI|?%Ww0%Xŋ|q(bbJy|"&|=sSa*{Sm.s(UREld֚%ys覥|j.Jj-oָt s`F8Pߔ}@TkiPGB|'rǖua]Yg"_0!i|s1Ǫokǥ>tID`Y97[ `OBU¸٢SץA0Q% *>q>"S-W}<-jx0$j`6n ̑v_@Və9 QǐOZC(Xl#.6ԚDь(h>?a]KFkv7+v:cm0[36 /}\¾wj; ]enc 굫W$.Ǫ3AN$g5N-KwJ?ć<Ǚg+[׾ĸ5<Ɛn-aQ[vm{mqgq3]f}Kq,nXg3$|RkX#=K![߹=;ƒ|f-AvLqt_ǐ#}tAPC׷SJRګSj[8f죷$u/ңxVAڿ4QX͉S `U'ޟ75gW7GGGnHhQ;7ۄB.`Gf,?Bo;͋8G5dn`\{ G>ND5_Vf1w 9?to]a~g"?!Boz/Qj&DfcA %XUQ3a,n,|_mR_(7sJ+Uwx7ĪOtpVq֪CI/lM6rܓqCLQw%[ *W5r*M'xH#pCێ|"Y:dyn77hT7Tu_Q[vYo-_1?V9I(m4}4i >_˛{xn%E+Ow6 -j{奮` Zl ݽsjiz~M!^t$^[U1f 7 ҍwvg]]m|3ӵ_m Z ye}|lEPMBul*xP^ċߌ5ǪrۿG n>:yn.-`;Ot2_.-p|> 49 YC8N"(|6Knhy8%rgnDKHV%S ɦ%a`]TZ~5ܴ@ p Qۅtz6_&\Gn^3]A%'Xg(aEr-'ݺxjC=Wek޸#ӽ}[/뚯~\VLAot_ċ6iXM,=A&mV@-ܰkk1\eV-Va>J}0ig4RBv!7' ־|K%KϚruA\̧UM=$,ez^-ߛ8Ĥ'G5٣ZNiė0>n#.1&|Lg05Hh{.*h9r͛OI#/u/Z=)hZ4'pYLw+:/ N,]j~ JrGq8;ՐP'֣ D֣dؿ \zcA>M5S1C:ĽA>@g4ڷਚ%z,}n 4z L[k^I$46 ^&pJ$WqσoRi,A$.]=Wa Ǔ{ <.߰IgGD~wz~?+aOC e͏`m.¡שL{k_pn^sWgKѱ[qD.3B>mO\OɎp.{N|'(q4%K]pGN<^JP'jV07c_'s__$|'^;3`KOvfJv?g }YTq7k] 8"wmjaK8pK%~Y;~/Pne1N4#]VV&WA\A# LҮ@IJ ";(˞q֭ɛW%Oܹu?ODLui)أ B8|Xwk--FZ0g_z WԚDܑI&f2m7!gT _{CSRݣ=H[ŤgS":^U.]1̦ ֮JֺkUCѪ++MtH݈TIOZ]̾LWWD"NZ/נ |xL!  j% A`>p [ ȋ=Z^aS83 ڿ u_M}r @Q建(gVNz y={q|W΃JvZw ;k^0J2E\L n^I}I.lo5ZqY/W&c}c./)p\vlg|:"+Äy_!K-g&и,n# M(J[d͜ln|{"0=.I3)ؿ7v{hn>}xg hD>/EZ.ݬށ#-ϔY{ n|=̛HñOh,SYq-r7UbqU Z3X1\FnÍiSK&_rK |:<@E۹{x;/ߎfXڛrp |uݭ::" ^7VPT _f% 㸼#8% s0Mo~!VюH^:(B[,FBEY*T4HVvjm_nZga [C7`v|.?>,%q.?>lM۷b{JSw4 J $߿DyQ0AtŜpǙׂ{Y+ :@!4[Ďی(&SSϟ(UtL#+3ǩYcsbXhGvaR! ςKpTOHa@)ڻ0E`sS6K5^~tKG ]MIZ-RAD{o(V&3bK =nDim0 4~Jin~-1|xYc?}+OIc"f(r4n\/JYhf#@no6pv1a@W]=fa0C[6 'g3~{ab.saogA`]i}:pS<.vWf7`<IqM>R0yY7u<;FVy/Bz>4LirD3X.[l+vf(l[麵TO7~8[ e:$[ &*2hg`Kwy7e|3|K.#h9(#{{/w] [4WU[qH\mg{v T1x˃.y$ Ɇ8{"vQnj|qub8lr 3?fyXGxX Bb˙?هG;|ٲo5Ҧv1??q/:hD# d٤ YA %olY{mVjZmiJc5dye.Cn.c8B[e) OI%z.~쏪,1B U?E\w&"e7M$M:Si`+jW}-8CNh;[ZL8Fd.]-ᅪ=x'_8njI=F~pOxۀ$>F 7Tޗtǘ Z"ɦ5-Igʂ1ͫ UZahdS xg TRѺ}4Ֆ$|[VȽx>xMU&[tlF˝M~A@j G'{e\[px UЎ ./&P)oPXσm j0'd2tI, >ԏZ?q]mt`ge'ݓM?(Xi@ l^ðr %YlLwY:Erb7q_i&#v#~}{w2wrZRQ7F+-6Q:[)tATa!Tcc޵]{;JT(uF{>?ww>ط҅k‹V-ϘJ W|n^'|1ɽJ41&5׮L'>?]ׯdP$!&;$;|ng~+%kgQְ8%+L]{R*W= R'cq[@A-2'7? ϟټ oO !oOTqnu^+ vuO6ݯr)?;JnO;jWn}jTM޷[}{oB0 ¾zףz-R*?b<7URk~9?}oY;w4o;X=%o swI;w |A *·Y3 ݃N~ 7V@@*#+/Q J\RKihoF 潮D/y\YS{rv3@Z3-Xu2/\s(S{IU lQ{߶ `@ҿAQɮV5 !crQajѺCn@]a[tuR`#BjxF \ @4u;|%-$gcQC‰_Avn%ZJȬ06 ^@JOyȾpIZ67}J[@`y۩lN0dGltaOXhM_ #qYx,ʧQ\]DG lh{"a%݆8|T 6ͶTFߗXp‡<>01GOۉN~Š2Ӌ )]8볺W{M~<647Kk$3Ó>}c` r2e$@4M 7`|t'mߘ[zA$s;p x> &&+tP)^Lfmm4]ei0~gΙbG+2i:l־b@_iiRSBq pGR\0|w6B7qnȣQE4g"i$~+٦7o~0FF1mK@f}AhfDML7n4J{pKM5mِ,ҽ6TAM=VhԬA@]x7 Z g˷łԮ9w UjFvCb6K/->b fsɖ8`"XgD2=VXEG˚g#\`J$Mc 9N7aH@[4NsCaH\ʋޘOЍkK!d<^ǗW9CtHxlu}r * ܾ$7*6a^nE*`4;vc'Hxh-Ni9^J?`Օ{7x@fǚU v v%ot w,juˌܾ9i3gcci)\`p'o(hRgzyL}xRR ;DaZgd'e7&bώmkō--f3-() ~ =ߔ\''fD'r_Gh(%kI!:mc&>QDq.)yQ3\P'o*I_t["꿉o]M6¦xgN-Q9BjUpp ,;fյmvx-oͦԭFޝ GX|{^oWת>1mqz tH.3 /-%\@ơ=P]ά%Oє4w5T 3qv',/Z&U*\'Kz!_' W+Ͳ@KA]Tץ?>{vzTE'5Wg^:׸Gĕz 2xN_)yT?'ǿx =wO7}5w}"]|o2׸E7s1;]y$x]?` &Svs K^$gnQ&nK[}{z"{vI%֤H{>' Fz^WӘb/jY T@|_D A:;|_ j/&^-xTW(~]z#^&x"'; ^>;A"pF ˛0̫wJ]Y‹v>? (l읷 nwkZ0 j )ŚrӃryq|յ5+hR\!^iפ$_]ָh#~oY2ڐQl.-p&Zf넊n${%mCߺcU#̍z߄<o\)p 3m _}ơܕZ|?CQ]83 }kMg/W_rw,חy/Moo0ϒ\}O2#{ ˜ ol3-~Z43ab__R]pt߱We{pe|'}-SVX+=_T!HO7]| vP;MT{TJs]wmOPg+oEPu6ܹn.m ĖǔVuj%. K˗wuy_W¹x[!;^հ"R%q{o7Q ٽ5X]v U$RrTi밢?Y!9Zsr/rok ݕM?imW%ްIM;_SJ;kی!N| /KaXdWº_]YW_o=zx/U\VIezw0'#EIZjKn/}˧Oǜڿ!ֽWuJgс/Uv%Z)F$0 j.cI{]k`:k(}. $a|pת/!~>{~5+_Uz'x@pP제Cڤzj\b3A]|կo-_ v4H)6 u~ 6u&/uquָMFꪜ+fޚ{LMD~"O~~?7hiؽIאSXs?:#0g{?r?𮀑د樥1ZlM?7 'i$]d+F8[[.=#˛p?w/ZoߟXIbjjGmSz9Td_,~uU(AW9͟6Wblڟ/-YL'CV &fkw^M $>NG$bN$Rx^}_58'%֦h$fp׭p3$.c^ɏ!g;gZ?AxqXpgܝj(A8p!$ A4v٧M`\U}[*oیӻ-+/^!4h=%!ϛKu8]WEʸqTxWO^ t81*jecc'v`}rᝯ{;s,,I0{oַǛ2o&[ja$j\,_[aas}7,0[ /o[/Uחxu|IcVj=pIN_o; {}]W[Wg͗i:|ծ?>\U +?ְO?[}-{O'QOIVb~Oxg \G+2S#¸~hW i>/<]ɋ~]o Ć5Ma{xM4?y5K9hk%jWv *|]k(tq~o JaB}+^bqծSq __OLƂXQerCߧG7,i6ߐFke]6M6.sۅ=?̻y3?3wp8Ko}theι.T&#+FpChm`npF$Wsc^ o9b'Sȣr.$ 1$_TZ1>Rs=a;b5p&{_OLu[``͛}rŻ~ab9|| /7^m&\׎e~,~ 6c̝M/~0dBiz}0Lwlfl.~u?xs\.^9x9ON>+ysSxA9`#[Un4zSsr͕KR#w,R?7B `]0%񱯄+'C<W;V&+/I9*G AV'b>a 2wI_B\/(]F+9߁;pA>ꝮrH )˗tg ؼ,tQDؽng ')[W׸ʽV8_"|]$~:F.o$ޔ`[I$h߾+HFMV-< 5{{o_r׌/ C~ HXϟ\%n]?u#7u&v$o_U^>͔& eݮ=08Zi$B' OF UV$m:_D~R!c6e%45otv3 ˙M+CG.-^s_Mw\7sESITG ]/<=/5CCqT4ޏ~+G{یD q 7 Fxv׿?W=48CLs6YݯWgBgapYRn{@6Waw 1|_UMi}#|K}j,H'Cu}y|gw G桫.g*&#-&U˅ω`l:TQw.-dz-/CnC؅+)q7q$mVh&?(x%n^x?{ F5.>h lks2O*k4׾cWC¬N3 ya\eB.|Ov *ϏÆ鿖rZ}qM2i}[>!BX;V.CA{3wS~ji<F<3o `}1+aRn gƠrN{kĽߌ6{Ύ%w]r5/P`ҹ@B|ЯɽV_ k[p]{椲nD=Wxn۶%j{*g.xRv\{0%T߻(@}GWՂW-5OͶ6ō kƅ5ot[{N)H4Y/XʷcEݿ6|%`GWel1gRN˒gҌf\.WW!1ehG2zնLP%ԟyo 6*g]VڧNPwދ<3Ry1-'$Jͼ:KOߛmi!֭$>!}۹L&MϟXnx9vx"mlEIqဨ!U%J C2\Ng%(wU\="?b+RuL٣JMѣ| u[U/_WRR-gdE續x#??rI;d/Ixظİ?|#L5&_3?%>ns~($)פ1\uNJ(RA_ 5AP$ܰd:n cA0IpO{zw8J~ݎ b t.od`eV . * uzw9}jsРIjnp[TuM%\Hnɦ; ["( 0.7&#ӭceCt  Q}6Ƃ]7KM(cRQ%%3; 'LKv7t6ۯKYa9  e͎ca '>SöFpL M*GnjL+ T/ÈigctPYZOJe8;XF!|  J$pW&rnp[D;pH|9-pj&Ɯݒ&ƒlmޙ@ sd-l( M%TKUKT6hUz9J+:yfmΪK6ł7S"OpINmŋχ:8UCSM {zˡ.{v5FJKvpSgM=%[KR0H^hIW{[w0!WYkV8Bǩ-^'Hﷄx߽t'F閈y(2~ֶ a,%}6Oeo +L-B] ZREoak.mׂ {j )Gh96yPInZ-I+So[v?rM jw'fa>X(~֕(:6\z,B<^Lv$hND( ZjqHːIOҮ% h%~ sNr8 'T ȓLӆqy٧؇[FϒV1r>>McNyX9OItN >wVT2: 6‘wd1{;AT³Udpcm+¢A$f=D~|UҲ_VM :<"t cq Εc SUS V#Gat߼ d~ySvٰۿ mς˾VcIQtgh t5$/=CWT=βg^ -G+ 'NcÈ#[ͽ< Y'ᱞ7)tҾY$ݫ[ӿ3lPE,y{;{3̉HΆiOȹLZp-fᠥuҽ\Ҿ|Zv8OXhMbq*Ȱ=2_<#0ܬjݸPC+_ 0y~{D_AvU& {O//oCI G9/)EkM$V8>ӬD,@c?5pFn|c*[ʇu1(RwFe2È/}H eť|qBlefNj{NoR*ܬ}ժ%xdEܡ GA i" Esm({sD7 e6{;= 5-68ᠷ[cVEZs~q1&Uiw P)/%f+GbANqBg}a \cj600G1Ӭ>4i񱤦wClr8$04U( XO\+ψqR2M'|`. mө7qy w۔hFiZ.RiP]qZZMrj/z~&z;Й  ZlZs Ci"Nl`Ӭk !.lcyR <2.T۰N+XݓP䍆֎U ^ 8Efov9GWm I\c]murӰ۳U٠t| РEba٪knIl} Q08pfa\rA6b<^-|`@O\!}v j9Coo2-؛wF V!8O6&Ug ܋ v2eX1W{\6nz4Xz9&^rַ=姯hIMtf(Tn48;R ; 7 m'MxT.h'yqw g;yۇ A0|>[@60 @xC<`Ox.Ϙ떟 a<#@)t˞5J*7M]ev6Cnpr$o\c'%U[$2Sgon|\A,%ΪL6ˉڅp٢V^4Mz[x3%wE4:+{Zgm 﫫gn ;j-z9nx7HZ˕uoGH 5., vn;R5nu Bnץ;K4 '?}V!9n8>|j!|zyqb%%KL<owh#UfTX0`:¹,:Htow6yc~qߟU8*O._j}nҼoaD Y#3$36X-;rmݹ A4Gw'>uc ?n/yo}u/ 7UTOߎCfǂKIaw+;GHɬiB#_ʩk M^vOjUP ^|_~ów0/ej+p:B,1:֖_>Z]hGw-xH tp@lZV9r3+ٛ{\x6C8-W6OT @"8IR/$|u^V#~&O]+䂻#+̖eDWGTp#p G{ۅonPԹ1y+÷_z8R<"8Dbzr{I‚<+ҤA,~oSCMTbu|3,/7M@Xd&k..na7 ޛ˞͉Xh+ q4EIЦ=ce`l)RIOfViq%,.&)x%|T:'Ar\4;ܓiwwlc :?Qq58|g4ie+ {|HycvŲZv S}/a\jJ>Oˣe=1Ͳd#rװWO*,Ea:v|M(avֹ}%aͯq mݴix]-7@GOvcꤘRj 6p7*q%- j|I2|8W qq}1KWԐ\6ɏo=xBD_*?ɻqAoKea H=:FmS.Ӟagm(ipQoQ)/C4m}/^ٻ‚wm)"N~DFh 6WWV?=Mvhcۋ3&5{gc1`A lm5;xAfC+l :Ȃ>;H; Ilg|x7ӓca݊m>ci ƱL1CI²]rdFz9#SyV Zh9|'7T)v)/Mw`,3Ym-Y޲iMkܟ7(9K$D6#O^nD^6Q==dsu_/Rȴ9zW!""Qs$?Ap`Ud/kinдWZђ;_+]F 2dB3pºΐ6| w֤Z{s7{LH"}j{ RN$(E֮'pJ"+fCw] ! @0 Kc/\9O7%R.-k]YNTn|؊Ԡ1կpQ[U@x9vi`^#>nd{~_Y֫ oxJOVȾ/-H[H\N:~^ BA]GgZ^uXfmb |A> 9\ߐ*鿔(\V021{ ]i~ {p"nDJ 5[ Yx]'AAT= 0-?D Qw Tn$&>=doBUg(wD$z$ۯI=J3Kv'p<8/a?by.*wk^0vpã۱^qwv6}q#'v%xW.zo 6GVII{\0ۡQ!-m"}ɐaobGzb}o =+6{TW$Rź y4\o|.IS'{͆i]˨s1zo{ય96u^~|\ES6!{|>Mnn4! 2_Bg;cuռ&'7n'$$NY 9l9I\Ŀּ)]Җ.pxOMU#"g8^/=Ӽ] }4]Q.]vI67&㇞$Oz6T@ZMxp,t}r.=<2~#/nO4S}DXhQwihy K.tilGZ5AafvC 9lٚ{66\iSʍ]pF% ;>ADisayo|lbNl3X3~m̊_q_?(K-'vj/dn ^޸{˗4Hyczf0UI1quܖS+wN`^iIm| AM sZdMҹJB=gΊٿ`Wz)^6.>7ʙwvTQF&~,nYyDoF,HA(_4$hm~㙷^@q"Iw~1lUuK!|uew>W_)  3wm Ffh3CVӫ('XwC:7PiUSuIt; Z7dPo 5}k E¸&yN{ꎳC [yp=_y$+ulvjſޡm5~Xc͓f,?#nwܨ9Nk(gʧ8½zo@іFH> h '{Un9&E ϟrj^-9..&\ϟuLU{`ƴ, WZ V_,7aҫ{OܳgeN6-grⴢyOF(xwwm )!B@P˻Mrѷ֩4V U@7~g任W^T DZ/b}~J6Cx_0Z ';Byw^ DA; q^{^ROK=?5{/e?&^ !ޯkC|KKNzmQ%G"[_9W@`͹w[3{rPox{^ 1usj{ hmatV#ݶA.MNڪV M.?@tD7ܖEμ >h+\h̓X{uKQw+2 )WP57ͽW/~p|m|@TVDBo <|-j|]qK3+_-:ȡwcWeI/%'Nd]=Dw t)nU|U"vO_=Ga1~A|z IµW] y[:b /tܹ/$*w4O; \j.< ߏu5Ρ  C^ kE>`/~ @-E3gj܃T3Z$tp;˥ݶ W0\n51\N:O^~ ]rݤz2ӸJo¸i)-!?| }¹#d:jre[\"Y}7~p xѺ!]^jxq_ƭ}:}W{ /xpRuS1zljg 82z]qe } gnyXk;C oM7~ǘ% siYnk}>P< 6'/p\r$N (o]g$8hm~aWF ZV}an_M8{Fdk-;k$F,/e a" 6"<]s.J%ϗT^>N|Мv;.ɝ&Yf= L~ oZT3L;>W#f۱B9>L˂|;Z79Nk{A/{`qUO,G/͞0p;|/7 5m/_ `(rFd5vtmRs6c~+k#z]膦yDэ IrhS[r߄39 k} 0/%T,,ڶc,yM QÏD+C6pZ? 5rۚ/|)+v_{%:n,'8 ?*{[)ZAwJn_Å7}! CEW0t;6[kǹ?O3 nL8+KSpQ{'X'>5L-9??1;6dwUxX^>Q[%sm:K;I~c>jy: ;ۿLr|+6Ծ3݊`7#ku{ׂ/z :Dkhv1Qm0cUn%΢3?ͿYNwĂ$4l0`Cuo\7aO\w/ o֏6}O;^Q|H_Ews⪮\yJ-_xJ'ߵ%rjRKxOiB%yw]Uw޷GqXC=ܮ uD]ore=. 7MSc}^M7US'zXA3~?;E"T>__}ȠQ)C}>)H;z;u}zd fA ecyL F[x&@X tY]$^F5 ~aE=.OܬN&jaOӮ:|nt%[snI'rW~g/! cZ l_CR+8몈ݸ\ :'+d30GJ5I=4ߩ͸ 56N`@WtL9r5It$v]ii[w 6*Gp׃*Kn]绨M6ۧ:$\Рj>O``qIE{x/ItMČ} Lӟ;Cd6Σﷆ7'at6!H--<oa&>&*."-WHhk]eEc0su;sz젞[O[+P;T`1(I{.aJ3V V{B)d_.7+ONeML{tip] 4ߋb<'+S1o4';R`DiI'@U˾ 'i#`F@Xj"}rp% YL]}6硽7 ݭyGfӫѤiu@r >̨(ؓ0r+H;ֿY/?BrӷwҾHJa_mi $b`:g)fl!rg}d%i49'bnV٧dI Eqk+.-An.#}}N3O:9 a,ggLTd,3{Pս( 4ޛ{,Us1x : !R>(%cDC~Z?⠊L3 `Np oV?&>N;0y7\Y<0 6գ W°3S4WXhޮpR B-hX٧ᆦ 5:Mt|QxXWzq1ns݂?t@rB|mW]vۋ 'Ǭwm3F53¥YqMw' qɾ-,wCHvDHl&ahf[^K12Q^С֛8K24VrQsfrS߰B#/$ޘq 1(t|;h(pE:I^\w&Zaa;o=csEM98"bǣ8\k+ C<'a۸da'S``]/5v:a4&qFQJْ?6v,v7I* :]O9.5wv+nqk?@$ yCvcQ4o\Wf Jj,Ʈ?.jD6|X&˟e 2/s0ѣJA"#vSH҅Av4z Á,7ArcDt#Ә|YLpUfiQzxhy4 [mJQEy&E[:ERj :kb_t!BWdnQhB- -t\?贇%צ` $PG567ekVFL׼+BydLe8׻mL&vq{F нXݖ٤Γ2w-64:I{%l0tjɨ Bj,joal$Vlgfo 3X+˽: ܦBGI6niWH_%u4$F]{(9D~^4PiBWOw:fj~OnbqZCFhE}:li98XTwj7率_j^p`ָ"ϗ6ID̸0|]qѵޡ)7Wf_%W4Y%?X^h ׾lֵפKO4hTF?'L{ouX\l?o^Wb k '{xkSb/WE{{~+'^>[/^U_>_ `H+}~;P+n]qedHP;(_* `xmdateA8K{x 4\0ƤL%׻dl3eOB ᪷0A)ŇCNpRU~D_('X,qy3O[gdhoXݲ>/% {ެ9PX'}$nVa5mآ_}U$oOz/ڻ|WՌYfG [|]3; s i7_6}_I_ le8Xet^^%o1V¸WS~E{l<C&ys2_+iLe& eǵLO&T _l{-˺ۃ8լ4Hxgeƥc? uN#0O͆ŖvCN:4$f,K}>Bܵir 〇{e]/m#E"Y#CL_XW ui~kkߊ/' {vgnCwN`fP(+|gN_ qaodU5ӻڄͼOIWORZM.Y 3BiCu1:$URpU>,6^Hz]B_XA~Vԭ.XIZu“n͟^!aܙxNkR%.VmCD&矖$ "'+^qh?`i!N%9~ ݾHUTW?9Nk<&`;-С\l-+؛'6r曊5m\¾ŸlI=xgwzuʅ}KsO * a[sS~$-dgc?vnB(}{w|1 .$~k_Q?vH hrI_UmWv -݄Ee7k_'ɺ2GO->ָi#/=o;)Y1pCz?Ba#ޛa(i:uiA%-k_-C ٤?]ZVDP5}_&y/=|s1oKIoھ53Mi:Y(ݴ¸4mmZ6_I|[ L | M\@CnekI6˜~WU7>_[_?QEmE[ "'[iGߪ'gp[m/ 1kng/~\iZ9alEƐ%GM߅qL4V)}Xky~,MnZpn?CW, nv-W50P]?0-J3^ٳi~LGZ @Ĥ]캤x=^bW V]% WLnsS2(^5ڂc<\C6tU]sL[Qjw,O& "_7uػD?-9M#8b^}sy|{chk%. :S~qϺtݯ=r]7Q?ɼwV7j~wgx~/$4 g;w\=4A8,/?v=ZWix90am=?_Xg.Zm9&f _7d%\yկ81(۶n0BmZ_4H+pǝO T\n{>~?Ow{rwxg\؟ q)᣻KM)䟿H=jhj kx? BTk/{U*_@$h8cn|=sC><ĭ(]֟.gal lwĎpEƛ:zI~CD5ͺq6I>ho]-s/'NJzX^Z݇8';p%>yX"}2^Uj-Gk]c_%^^ãYfM7p ީ/zCwmw7kp3z7¸^{=f};VI.MN~W>٣BO<#ae}tRNϓn5݄5͏J *!;BSn51sdzaU'[n Zio8xQk|z DkM(S~n+!|R\uh_( ~of_ ]G{a DL+ lK{>ioXWb?w7 `,9"f&W3'V߰ G֞ uAwiݓ ^k0Ni rn67vywIfn\_r;nU[a]/*kYq c-zz&U|m|'twJ$ n]^ ;λA<cK_.\ǣjOF>J0))?YqLw" Wy^ ʅ͈Cic:ziU:TmժK]jTn'Hlx8m`QR^,M_~ab|YU׬ |F|< Z>KuOSa-(N;ֹTO~ID WoruSyL rVɁfA yq *_KNbu||;^K}a(Db:_z^GwxJswĝwx!xX^I`#@ 1AAKGpϙZ"[yz[8r397tC\ !-b&W#cA/6Z^Xڅ ^$^?qf.ٳi ճx_ݦMXɻC鳠MV4_zӮָtUˀPX{ ߃Q~;uwKYĂqmA+C M.)gĂ3\դx/Άf*wAm N|pDﲠGZ qIzA_aaV rx"vm# iA қXX]^Gf2P൹q,~ % k^D .]ҿىRAMǜmxwVPOjW1A%j#K7G=xNh-L6U/p/ Gl!dxLg2`GEV8{ݾT^sllbK|ԽJ7<>;u[}7}XDZ3ľϠ* ^M)5<6#3l3hgw QDW},|k~ IwR tuqmX'E?(Ќ= }s&JD nk 2vzgQ>d'+G ֩gw++d:0m޸+  "rWo/q.8g oӫ{{p`%XkB{߆G_T%ߔM9y.0lTG lgeq۽70#{~oHzn~x=x*3xgۊcs. b<)N /BURu,g̴() 1ym2~ 1=ʧ?eh61.3=tp#zk{.{C|bm2Il<~ZM 蝤|}F3O.iXW,E~KXg ]H?k()-Wp&y6 c%TbBOXl}_]U}Ѝ"|iþ+s_<86/ʹ؝1`WzmC12zq5p*f\Nmu5kZ5%-nםPemUCgW+'uN~-+(:y,:Ox^6~[wO Kx\g~Q}:j %< w_+KB;?Mִe|'pyýnrI@,L|5boZˊ7wxGW*ZP K"?L'}Zo7էGVרq~/8" ^(/_cp 5AwTioϚM>=-37$OY2}5JYc|2گu@7ߨ-rqTgӋ#WC9miO0w(Mnb3<[]#c5ݚ_6%G!z0_(*:l__ؠKvv(p@SȻqΌbK{]/{f$%Ot&cyGDO.p"$FB6V,a֞w|^+k\ i~ =8#BmԪm(GLmd/C,I8ۿ]Ͽ'Dwg/Tik;)$|Me|¸]/{k.v7TONEWp\-92(ݘKl%{ϟJ_jF4&Mπ*&$+fVF_ǂ,<{q"_k ճU$7e/&ŷpMĤ(/~)Tqt@CiPI? +RpUk@)0ϫ[tX.ot+GS۸0\33_lѬVF)[5'>UQmIx,fH[!APߕBE~J8ް!?47@5&U!hX)܉/23IpH_~II~0,. v/Wg ^f_jX.mTV30^ >Xӭ++9 ;;xUOۣf$^ɱ0I?l^f)kIq]\J}X> ֚f4ksaKV* ^.̵ƍ57CxgO5gZ&t&# ̻^7. (gI[᱙y.),i"R\qZHzRY> > 'MZs_l<4*;d {ګˊ]_A1T'؝3$x#D> IUBdoL `p&:B,L"sJwT><,}H:X\?c !ɨ%4W*|'E4cIEH.3#Hk {"OY&\x BU *tFӹ -Y31{UCK8N"M3\LuN& m\F7LWHŘ0&O[>pQOUPp%ǢV n}n\ L 3 2~V4Ȼ/=^5 rۺC@uϒsR+挭|@p!Mkt05IޒOB^A&c/oXorG(ڿ2=4]$xlǔv߷?Bı^~X7ys8tۆޙ:gq`IB6kF%mtY>gL#sKf{n5ꆢ^"۫ zdu`:K;+3{K[ ~L%MsCx# m]Rk߂p$boА K -4\h{ C#4S%2,=lkM {AKtc!G@;Xr$`&fv׻M4αp]7#8{m {ahJ$M y ǂ|j?|md싼LOpR(otw*IKz;դjn80oPZw2Ʉc{kXV2Px89Jb"6F?w{J~ysȽ[j 9wO,m| !R箠̙0ͼhfaMKw>{b0)kY P@q){A?6 W~ ,n߶kfAO`];ٌyKHf`|)w6n(!pvYvf J C4ٻZiټ4I{79P)FB ?0̢ >R@IBڠ僙+:5((VjJ=@}"͵^=4PXjֱu)aa]DgJlUkPUv$#Y2Z*ۓC.z $e M 8_8ܚch#6\ 1?X]P}9X}W3-@'@KqW{֥9CClױpeY58?Nk)AnX^8 Jr·&'aST=q1^lX7Im*Z˧qœ'Zs=kqSf&ntc:TI08Z\N6rJ2w! /OWZhvNMfftI Q%2S6T\Mm/^Oݼ9 8~O^iFipA |~\֥^1wMs H%eeғׯq%ɷrқ^*y?!^w_s~wZ wtw}fg)S1i6Nd}7&.*fzQn~}z}u}{rx/ W iFp"_5&0+5֢!ajhl|-A\/RUDu^7ky(N'+/r@[A:*߱Yۻ-~0=:<cKwpL/c|g(?2BdS?AjnO`KrWsgNX[i0Qf7rѴGєKU(lł* 7{ݰ!cEƟdE6ZK|uϴ m|;R]&<3ZM*\ W.]CvBZM|ayXN [MƄ>k}BxJ3m΅(ѭ30o{páR;¸f3- #ҿ{Xd[}WK-ãջ0KFn(huiD.zcs*ONeӱ+]y{ڞ+ꚹZ|\їF}GEwDͽ?w7t*[a~ Ǡ2'w?--WM=&\h4 㣬4q^0"wNj]^@2u}{ n G?Y&h1Nn-Y#M )=:lm]W>|l7b/BWˏnהI A;_lOZ6JuJ+a3%l>- M_G$A]#80y7TiɻPl#tٲ0Hg]:'[ ($֥ʕ?]}a^DlzDL/zX_5<%U]s!+jCFWa;͍ۣhU)][/mQ!=K?nA{K6h>Qytarษݴ> -oOn?4wGw׹ ]sn|^-Z#DEn[͕͵YLW]0Gj8 Q(k\Liw>^RP -9GzJ8m;3=5̂=3~*#z.O ڎGCR>w;bI"J.+Q4ݤ8ͨ/\#JO>v %`vf/Uܗ-KE&j[YKu4ьs lw({\|Emf2Xs5~`^9ŴKe^ dQ0"x'UgwhOǂ{^3WqU~ QV~@Y H`#p"ݟZQ >QÐѠM?΍ <m 9io}MsDid;A[A[}QV?>4c/ M|+V g>7 %U <66 o8,PM6 \}q#^\5TMpKݹA0{Ƃ ˉ AdrjC @^TjevUlNMiɯ^Ȗe^]4Ixj{ 8+p#j25\'t %%-49wlݧWjku){a-=44Ѷ5(/1|ۨK:J{-]) |1],SxgNa$TnMڮ 9\]dsA}x$֗?T6!ZdFV>WׯI˧Tw^z,}k-w|ϭH-jH;Awo&s jx?ZkO:ת ws讽&N u_^f&:O'Uj z-AсۏgZЫs#w }XƿpFaW"t< ATꪷۘh?a]DxW ? v'‹R'j} +IpoA~7USn87|+k*}3ߒZ8koGÆ󅉽FW~xzZ'Aߘ]OFUվmZ;ݒx4|g9+U O_O9Kb3>JzkK־jE֧(JS@EO<['b'_*]2D]5zAqw~k {޵a3M{z{5ݵֻZDP~akG(h'ߪjվ8jHbo{{sx$p)GxEY±wou5k1/k  d(ᝂW-WSSaEt0^U MUk2[涳1g]p{:ٶNXpʯz bvG9Kp/-յk_޼/&RT~Y06Vvۨ7SV_¸H-?k* 3*wH%`F2O\~Nw%[W (7qwk/9hZ{ Uy+r2 zȿLDk4O?~Y<@G*@|~ ؑOruZK֫z7Ѕx\'m$k][ˁg& >^m~;'CZFvshQ/C~ju}Q'w $;XDO~xOZ;>9@_~}z֪lA8KᰏM{^ !WML]6$A__߱Ablyxg^: 텉pmGMZgUӮ4[?g7-=_L_BǪM^߶E֫3^.!-Z]NsFMV9d߯}f9w oӫo$v.jj*Xeah }~@Xo|Wݬ5=]iO 0ceZgm~d)8ektդʈKtgTZx"»=a\#ۂ@;oO !oOMxny %ҧ {z./hs az$d,){~RF/XIʞ׋wsse~ rv^mNtSPî|-J3Zp4#Mm+Y}vW:߰p_7U<m=4!;ҎͯMabFޟvCPhLe>i`*^e6crsG/߉ S0&2 ]pQkw7K|&]Vi- _t>Q%W0*73%4F An4nHLu._WRk D[2Pr^q꺅y*l#[~?j&*.Smw۶VkY{ub?gB.UHe6>%[JV5Q=LUۄ].\1 k|TٶT[{oj,Sܡ0|>V r- M7~)co|O}ɟm :nW?ѿUMM4 g1Om"t껛mm]4߽MZy=y٭\Sz`^ոt{ۿ^3ގ3%Q.GV$º2c;]4ɫV7No+{.to'bd "h&}_,Oz7{^K$+"p:G!I'/kQܦ1 !!j"}q7/ORrƓ#ԯSi$^i+Z<_Z_?p+WD[Ԟ30P0w$#9F?%*7j'0G|Sx=A>.FMI]<[iþʱ2\WI7~m+¸H?тwy'FS1R\;;}E)KN6%ۢ d}wdZiǠ"zTJcc19<~Kj &IV]?$߆~k}w|ֵG],A.e[4|/WnXq7E&G8E7.^m{N0<;믡ָ¼CO4Ho0Mih6ӘaZw z~hR^#oZ# \o0mpwPU%~:br[{u'ޗ}p^A]D=>m>n^@mӑz"gvvVm5SEvJ* +=O٬LBI}߂J_K~,ߒ% ':".AݷK|KkO ԑ~Qވw< 9ڻ_$ۭyMumɞy'?w;|Z}"}4gnr!iV/y<xx3~V ;:Kgs1sO3`7~oKzI3La'm?? AstװEDۙ ezOI %-i9MtPVj_ Whw 㠢 ?f3~g]g-_( HNgC 7Mh~PL|~<4HFλ̊m'wmnmFw7XZ'ÀCU~JHZK< 5//,I{%7cPoF8|(sa|ýzW/ 8$nChݼ KiAՂ6m2T!Y6;n[\@Ěo,Kl3T+r'ЁӰeFKY LpM[wּMTm%Ɍ\#&ُ={/;' pMwvt|RI7ќGy?>^Yi>xe+U\ؖ@VZ(ymV^/~An]byۺ2 >a0OY=[M{dͿ`uUCoQ^өM jZJA-2{~_&1kzu䴊IJz#K[=[/ԗ,N/Ž^ׯ} f^+܊?!~|#;Ʉ!Ok-X G3x3j\P<^"*x)VkZ(8 \#{KHu~Jy; u|[>$8) =ڿlg|pE.76"9LZq|p1w填]1tj ́^liI7ˏ7/2c#pSز^/p^3]?.ybד~[^ #{EEǯfE?taBp^T*Rk|+kCo~={JONhCLZK_/_OSܴBmp!W(_^ W',pi/Nb}.p6=7Fŀtgitn`4Eu~#9RxNX+=%ȽW e߽7V/jI5=i?Wy̡:2IX{e\tDSXg qooʯ8sor;zOUۺ ~X j&Wpq7Պ%&e0㯹\S'wGC^,#n=W/~37潿Mϻ_{_yqnlۄjZ| j  |9_wν Aw+l6uL틴령7}Btvm¸vOMlkx]xl8?2Ww|/eɉ/0m/isoh [ń4|^bL֖]"{-SA_)t{J/~wo=om!u|+oUph_lZ%_I4+_nޢ~8'Gg)o\"muKc J wnHNfⴥdNI=.}wgk¸y&I{i{wA7cMAj Pi*.J'KL*p!FAXйdԡU1,]?l&^x]r'604%GNgϒӣov4h[R"Q`\ .^yHfaº?iq]zWO(C/wq\wY ?$"p' 0$j-+W/U$x7_lT-ݭh7U>5񽸏fCyovElG/-n/ϙPw,۶FAG{]孤7w$-MֳֿGQ6࣓sIy';iˬzHܾz[y)׾(w7߁bОH!ڡyG?jH/~+\|bQ_4ί/פ_%jy%vwyc}]~o3[ycOgyާ;걿;ͮ-TQsA;3}u|ƷAn I$L64K7v2_}~ K/LyKS} V4#{w50"턮QW ^HDwQ{A]_sYx$ݸ;+z'ۂ>Bb&M'{w߮8K\%^M׸5+w-8w|ew".vLN]<7'sM{8Vaj'O^+eѼx S]QQa rfo\y2f)wvu&_ůϥ} O vؒ3[]4'SI>6$D i]t- ͚ 2ܚv|Zf#?o%y*bβQbO'm`Faw$%̭ϚoKY t9R{k&tVi#bS k <4lt<#@(woj{_|~o|e|#URcb4 TKW3 )j*>1rULR$ OÅ{tDIZہYiqGzpE^BQ,n3ZS;ة+81^.;&\ &ZjNW#]>Az,xh|31df2c;9$/&HheކI"Ei_c3T9^O`j}ֳY߃|o7.\ܗXOץ5=3跹ωsL+ؿ>'ڟxOBb[~KP^!z AV hj7JS3.jT0`)ZtI?`&Ҕ@m#Wl؉h/Q*aoL 4p&ێ:G5 Nw? #8J7 ?X0 ?]L$€4mw[qP]nES`L˾poӿ0(ow@4Թp Z\h@^x<] -W ew‚B4[@FT5rݔ$l&#"pq-<&4Rv8OAt?K7O$`ͳpE~s8NZ iW:HIC~x>\X$e3 _|it4W4k~pdqY*o?.9X+iIB# m:Fuq~#aLNs7D8ci9˕$.h$3k1`ۏD~X[]p%r#Ⲱ ߎSyw$u /g,~[ .\CFϛYf](KOq c80S0կcB '8t;m4::~h%RYd`dU] `+m."쀎oЂsM. qωb<My3vE N w瞧OC>aE}0+~&W9!6,W]h胥cqG`I߇#Xn&Ҡ5f356RGUE{K%~!c^80`ϺqA>˝ ~+WwfG;[uhuo-/r^BwRWsW ʞݡHz9 o4͎Hr6~: ',B&_oa/sJ:l=+`D w'.g?a-;I^:!pe)]w-xL#{,2w^AYj|uO55bJ-%6.8p*, u)ppqyՂ=3oٗl)d&45 b{*bia%1ӽ􉪞Hlq_ /q`O-,'fQGl$5 E1rb3ŏܽS&Ǻ` .=s%\{_*o~ SLtb%/(-? |&ĹAAƿT[˟"\ m>5ͲYkiGCfq㇥ D\nxPePWIa;n!fv,g5/}gǂ w*?( -2 /q^Z}{hQJY7+ s35XN @UZĹ03_7Ctڨʪ#{õ"KH p|e0UU:#[o 8fgSLg |lD[6$ zҟ\_2뛿&nU|ݪ]HL~D Khiz'7>3ΟՏBox Xz"^AnxK. FW$dNj۾ /w/l^M\[vG޸;.מu aJ+XO_+PO^w;kjȂTּ{`N7yn}{ݯIy2UN+^~9=Z#^Zs_u%zkjUwzpo J7|iA> ri=|~jk!Z&1on\‚%ݿ+/߿u-(M߇& %؛o~ W "ogizo * B[ᝇ9|+^ռ+"l}:NoMmTIvbgDQ4 ;O._,e¢]KjsM4ꭠIu\+/Do ^{i7쥻By>;xt }$. <+KkAd uյO֦۫Q^ 佲k6."c~?ទzKd l'f六Ɗqigܹ|ؤ+~X7ʱf.b-ߍ5Kgt<{#&[J*X֯?3Zչ8;殮*:m0'eZ[\DGr0 ü4IS_]&喒:}'Di:|̊F dmmntNzwI ,6=Qe4Nmm"ۭq*Uv;Wk_/:*OӿqvBh/?߾u&2nT SFW"z6,~!y[(V8)H$x7ߖz3/n;*ExҺa\arVnZyPַX ɾu dxgU$M'Ze;]In>qG<\_sӮ~Ki`l!a4фXE-Nw7SY.Sf8b?-l}L^ }Qq?~>&w?Fc(:Z˿7]b>'_;:F`.Gq@_a/(3֠Duc Bю)VAVS*C\ ڌP:a\'/x |lqW}U$AXǺ$(.Ӆž7N]Wɭy>su6pՆ(uu,?a\>Ufﵔ֍Y}K/ pE#Vjݼg 0%wB[\om&kwo|+?ή~hxY8"ߌ׺=ȯ0xq[/+ p!^=/qVǰQsp#KV˜U-;~92~o*TS0y-ΒXg ;/D_yn<ռ2[/& +[a䶴~kXWu(_uQ3RMt#{M֚ *Hn}9~mv@&Vq_oq YR٘p װ#3!쎾+ḍy7Cw>2Ɲm.r7!qAh^'ּzPINbIUէOej.ǂ긭ߊpnJYQ_P@)VdžDiٽ|.,{¡~1]}NwL+,,H Uz&#?cߠOZ~+ -{ZW86$ԗO]5ȡ)e?&Ꜿmmx"|yrYwo\ !+GPb=? 3k]lzi鸬WCOjIka*kg˭p+fHlj s5Z4ꫥt<|6^ꧾqr%^;}6|vL jJmOU%C;?٦/AB|Wi x-W+:7ȑr} {88u"VoyNL&|!xK߹<]d~'vwIz [ M9!s_s^;?W(mY}x:K;慻ׅ?:"⽂uS/u;תO,|%E"a]jOKnhÙMwf|WSy#%w^/nj%nojzo߳:VTz߿]/Q̼w<1$1S %b:>^!bk/vu|v_^W u Oo*O^PpP A1ڮ{C.K: 5%Wu[ ˻'漑`[tmcI}Xdw2ٲd|}@|*9%e.P65<_f3e`Kg6[0%jny<% m4]* UZ6ZmIv6cMNNPib=+Ekcs.|YTYּ0v{]jTZ!ޕpm9XKm9l*(6 ~ï9MmMi27bTE7k11_:HSAOخ} ^h?_ܙep.58-Y6iX#aɺVmъw)EqAu^O )Tg5ؑW|CqqV) dUd23$jiWf8]yWCu&.fmkЫk g) oߊi7\Dߌɹ|#Uvs! ody4IpJw-ūq#Mˍg(YF~O(%wI(߸l^| q_)B٭`J^$>rvEז 7˛ OwJۥ rm*?.?eKr޹@P T^~jhh,ӹ:q7UԌ51C/\<wJf3ZߘW{+;nR8^8bFv>[ x,f g!t0DO&gӿA 0XBZ=mfL8a.PJN.Zy)Fzqŭv6Q'6}*.ϏE} oXɄW V xncooeBg}x| G:Vɚ]'vT* #. q]C~zKC*ḭUk#q+u>Һ'#b1ܻD3O;GC&p멩U:v A9Vc/~|LFI.K-w¸@u\d+fR͖c4>E+2bUOZ{A/?܁džbk.v@MXp8Z(gM5ziu)aߛ86PQCkPdB M… O2N|Mxݴ(wS}_ra/E؝R%d_ UƂJW&¢K u~*w<ӷUaED8Y(d(tg+U; 7,8Wս/~߀Eef,Z܏u>ҦH,g5 q&@lo~s-z njpЄ0%R3E2!~[9|,"mi2" U279cr#Kȋm26]qA!v a~L @d@*k 5$Iy {,ht}F& %!2{z&+"zx)5L;9tϙ(zvtl `_Q'X$i 3}_yVNlgǩ]Iʢ=/ȿ \'P\8ՍEeƉ_mQRo44m+ sF Ur9Ѷp^`IA^U?=#[l|5owJA!RudnʒԆ+6lߒ Yoa.̼΃MX ֆSa#rT+m)T.\L0JP8*&Kiv\G1ۆ2~6&._ *υM#vyw,EU5?M:͂Jш3YaL~*<LRQ֕Ӂxy ]F Po~%_ gACdʹRFZy!}qC-t@O^I3H{9@u$^߳NCw.#_&|O{"0.Q?{C˓(fKߞ[SW}IJu(YrDW&:tXS]sH aIFS.Ev ˬ5}zY@ KS~1W5vhc\GcZ&~pЯ{ NQ v~[T \ZZb߫|#j*wSVo}0Ԟ\(y??tZkr}A`4|K^x`ꡯ q xwi^]Xzy1^{Zvwqpؠ#@G^ʋP?%z;¾xcwJBD-xKadN;Dh.7, L% eۨ AR%\,}|8&w7' { %G#ϗdW;`^ G.ڷt76fp Yx8 c]M焿uacF!'` [BLgllnH;ߝbV&rp%7OAx `mw '2KG<IV!$d+3 rRM0/m߱S!='ռ,6Y iz}cNl)U+F$O`3ipxP$m^lJ*gTU+o%[OZ^yV%~ 'k-q_*Ke*ͭ ;+An)qr l[sҷ6LOS*HUW\$.6Yh ՁXSr'ˉ~W2M\0ҚJ)t'i ËC5ڮ8O*F:k ,ւ# ؠ`9٩#p}?8!ھŮbkZRC$> ڕL'!cW],.U56g{ P#PBzVV@^'@H!Io_SDZS2:*\L PVC=pnuga`"gc k0JpIࠠm= >&Y7Jyw і`8/o]y9B@N+~(*f+ziZ K֘ǁ` YD'"5e{s⩕p0C͗4pKv2@Ys_5c[HF5X*ݯsJX^b+4L ѥfs纪w} rȶp0蛰Ie )0 -tSYy<IxAL[ wQî ʎ= Jq>xܽ|$TߡF@KqxD_J~&lsqiA=$GU:i8Z J k%2*j*\/9qq| OP/,ެ <2>R\Xt_sm TFEtq `2A4Vl +H>bSovcBDd8[A"G& =^xE@H59Gf͔pP`)((ťћP(p:8 c\&;m[i4s CW\{WU9n:&ɔS<U:Vnwd<>MٳCSf٩m8 ܐzxLx.ѲZo\|+-?䟖GT~1٩|sr2׶/"j 2I+k^~1ɊF2J B;ϙJ<,_ʀ˗}[I{w) ݈-5%J`vѹD2ar2mPAlR}. FJZ'Mmru#!1>|NCѓ~Js;s96jO0=# KN|A=G!:_W Pj {Fh7vd=^ OHh):[l8l?wb:tg)xF f2t Y_)یR|L!}52GB=]fet\Dk!Ai6w}m%Ih-4Uy\E,!jWq 4Gt`yj>Gz{dW/Kw2\4:$`]mhKMרNx]M{2\8lN҉Pa46Fzan#^|! "6{)%'8o=^תNT\]ABxU<9*>1)&B ;p=F9'n+FuJlu#Z CmdatAA<`%2>{}5_ŰG_gZ9R?m׸W8 fr>IvsIw:_]m{ɴ?Z͔8;+pbKpͽ [b{Qن:G;~hJ{u!eqCS?v'eު^iq~^L'vρ4utb_ćݘYB U)sc ys``;n?6{ U΁7P7?;/|]ٳJIAnիs68Kov46=8T6R!0ʓxg "WL|ԛNLwsB7cgɗĜ]J%~CgD1^3sg~`8wP!W>>3OhR^fx ];`tn_Z T-G; ;{ 0Z0mSX>voZx#0h[~0 wii8qW4WYiy\Qpy/^#1Vcwvr\{w~vyϛ|F^)wX셽Ha%m"9c{Y4H!9? sIP0pm5v'oZ4&.G7ߚ+㬽D}7g|V MOi "[CFYd[޸Daj_s-ْ.L&rgRߠєny!:a_%[2]kaymi]G^rej1-۳ K յ~lS&Xg1X`$U^ PEf" uâ3[_,VMY)6ׅ>>zNw4$ 6m]_TY1.3{wׄ$ڬ2C pUҿ j7n0qk ?wI3(tbK0cԟl!;, ,&ӌ~􏞂I܊g2 5bol:>Zhq~@#O-5/pyB4u׶%nٯ(u4H 2Jۛe쁶43[mM4>Zlsˮ-\l%j,E CX7{y.hj7ztXhJ}W;UͶU҂ćw:+ 4wV&IDd4YI/^NJW$brǖ}Ya<.pC|~@j_x /K{@v za՗&|%jK 6dXSFS|t IƟA yUpCێ`3 wwK>P4KTߩߐ{~N9+0.hZhKm#|Ub%})^\R7"T~ooTFw}<ߓɚ\ϴjŽH8<  xN/\zy s?k\s8N[oˀKBB?k T+wBv1_ǘkU +q>^ W B2BV!֓xO<` w릟bFo.cJo{pJ<[cI}kŪmqpA]V>oen j($?W)=GF:u8)ָ )-jS\XW9o5&.4H?"ލY7'vrz"3Ulm ѥmwz/FW˗xhc-!:Egh!q;Vk*N /Nړw~;Y'skOcoI\:oUո;~:uiɭp_VomEl,IB sP{d69s$۾r_6O>#g"(ZDWon@޸$r ~ jdPW9Y% }A{KÕq]S%~5޼{6a]/lo [/aE_diמɭxUQ ^^g_OP%w؏u~Pn;D.a\*c?ͽm׊x(jU_1 {?_i՜7U f[M+MOz<msQ.6gܣEQ=g'w E, -t8; Pq :?ꪽ B'Hz{ڮB^J_%sE_eޔ::ʭq?Oo'c~s,(݆=UC}x6Gjf-3W#9#0>To ĉ'wW_ؓy Fj:<.{\\ +-;\2G|z{x^ hi!FӒ%> ro{l'ZmUZMU}Tu -kZ}zyO{pߦmo͗b94e89sܝj+i\;Q~[" C֤6i֯F`uD3sh]ޟ¿!xB-aTĨ$A8,u>\0|]j/VםWK\AVA+랭8\ 1{|+kT V.SwJ4Yì$z]AK 5np }v'^q~Q1R0?O jr4><\^!.[Stˌbɽ^CNhII`:l|^ 1g %h%{]sHK%qӽj~rAg0y5tB yRB 6͟VJ¸|e}4JZնNIχ3`ov4Unsupdy~_\\eozOEw/N>-vm.Abi_e ,kt߀/ C_w>|Mߕo}0Cq) 7{VO6^H(bm:~bŢ_4X\umA_1ָ(Rvۓr{4]ȏyf{w?vm-2ӣΗ2@eq^wĄVn6JhlEMAt(>+_`pH -mf $Q'7 º?"Pڝ9DSxh oeȜ_QI}oBMiX'}WjUZ|T aeKGl+L T&OGhGht#y /ːiw5+ПMT:W8 9#Hoz xwvMQ ezWe}Xhi{} M]oh%'vۊ@ns+}.^Cq}z2^^T8%qiVW{Ez| J]CZa!ikV¸prO<P0Ulm >|f_mk3 |Yb˕4妖tܸϗ4_. BZ&}4蒿 $\"PI}߅b7\-ۗ¹(!,#OM?Z8`h`fea\.-P-y  ^iQC8$~y0f{L,PJyոo,/?Z@qXʡN!h*{xLݭažai' D_*KE[o@'P7D&1ٮwqw <4Bꯝ6,,;65v.h+3~f|ص#e˓ݥIQ_vY0T߾.S~eH0OgwunЫx.m{^:ЌL+W^QMMߨ8q7]IR)ﻄq!ZWVbxK3xGDh"+\K6+ l/PqֵT/пV AJ9cm˄8HAlN=[6dBeRl_| 쀎.ծ fy>:wzm4RnIo?TAwwF5l;3=T2ݔՕ~\l,sq;$4"kO*lI'n0$qt2=ڋk\ a@CEgg[w c/Oߟ8nx ł]DHwp][0-a;ocwk L =[,g . ٍ[vK6֯h2J^( a &FaU}rMV+rIԛg2^f? /iq[r¬%99 ƛ+ p!k&P_^)d9X%;;2 +NmX:A+ZX1zo;Nfp0> ]CK~1iTwɬoO(n€Œ]ݾ 'h_h^܄=m/p}xgM&tQojOc+JANv`K}wk݆U70b*v[[ ixYe¸ZqZFosvpsXQE Q[!X'M˼ZC/ tlEF>hNƬ߇"T#@ʷ"dNl#(g[mQY'P3\^nV8I-zJ'U}hy{/Un+G^ʑA g/vhX!8IP'H]lg?dϞnV;>}݀!K4XNVO=%y!>Qo@G,e蠛nȫ 7M kUp*V`@oM*0J N-<g !z.϶!o|-*loa5c>DddIJe {̉l|Q[]r7BČ$=`.7[HZ)ׇ dVU[WL-aʞ6ЍH]Rnٱc ?ϊ @`ְ!מa>rKZ+iFܿK4sT`.jZ€c8]M„f.7B>ᘢկ{`$sR>wJ+@.ϾF`:߄̽XW .߳Jd<ݿy;mǁ x` |6jg|#jc\ 9@h!84cTclE9AOD-jgXWC- 'yVoo=P;|(T۴Yw|48&y@ ƚx{aT= {qZML;s~,'D|I|>pO]me!RSJWcNn5FD_L 2{O~j K͏v&# Cn`MFb.j)(!\LVϐ!\ܴ95Mp]Kڽ SB@+y-6PzOF&mOpT~0Yse`c^x#n:!Wm]m!OZ@8p]os4:f*~^8U q|:^Dp!Y[s>Q͖,Gz ZVp@pIV$hv`[bFo=,"y27I'tXL𝒷BV( ۘH-ޝ;a{sLp^qL&^oI֡NJWu'QRka6=G􄃀M U<'Ӆ%d_._(>$"#.t/Iw>>] ^; }?\#>ЏS7BAr L^^O1D8/[}z\|Y/KWu$]uv&4}֪KT7Qiwrtύood|W4"[WVgŚ#'ժU 8 o^!֣oW֣Gзwq 8Ko7ߛ'|//3 gAt A23ړ<F]vewymYByu5򓡿 kP߄ \qmqt8#{CuCm`]+ð[?[(.Ji)u6I jjxz?8Q )ej9nܷc øpX~@M`J { c!|,Hdԋ0P 6"F,Z ZCtܙ2Ûhs{KEYO[4'h:C;h, ˧g t4#i߲)G?ʕ~Brk%"K:Cw mWx#ߦkj +(d1~@ AF}ٱΐ-l߅"kfk*JΡE7 nRk~(cPGFy~`ɯxѬ_F B^C/5^VPXvcuv0|}2 `] ,.?+ Lx0B1*LEWq$%Ky(-LQl4zK`MA1!;[  eS>r,uVi`I@ȥ`0-ʖfܑO¸цmUMK=`>͙Ӷ[ͫR-+ & 2KwD֌o$T$ ?W`X.hA/ߔp.]A]=q_>;J]o 턪- *v *&K~<!])ϴs`MSr RyQpR3PODWsoqx+$Tm.8Z#-,Zޠ?B28W>$ 'tA]fU.fGȯD\xtGl.Ac +^ZnZG!T߆ň4;V8Q#m\:`(ʞOQ1]Tg [uOS(뇆J\@!;h)0v `W82e zn>آ 4դW- Ǵ:4U)O5 >OV(?zh ^ h?+L3fSM4=e_j눂:vJh#6x uU߃q&qA Kn}۰V-W6(&˄mSQaP]M7t-k*I~g{;mcWOIn;2[pV^Z7Xq/4w#SW~:MJMzXh{6b!mna`Fߊw(@wPP.l!]D`Cÿ@V;C1z"! Ua73 ǃ1ejh`Ő@mJ¡8-UpQn@2Y a>]ߤNsL|5o¡ \ i%)>8ql)ޘqV#1l[Yhjȉ3h.Vly~ҲlsKrm{!5׫׾ĺ"uPU:4CPrGT1J%v?֘~ :Nj)f Ksznldž?Rk_ Y&>z$;DhdbH?2o!?Hu t+daq}1mDk'}{loVo{jy; %nZOq^ O%Nd W"N}1K"lI7/׭۟B[HG{Kת |ܟ;=Ywͮ*oP]_Wq~#ceV>on6o7Zk7^xFD [ס\LcA>NOǰCktW ^?\qv6+#CME]]F["t^opI1{wUj5̼ |etlu~pGE|NTh/}e͏+8X:h9SK8Gx/|3Ga}Ծ˷*O/޸FvA@go~H$vp7f t^PZW$y='9e ݷɨ3_Kab⥟?8L%_L~^e~~R[cm٘Ƒ5*nJ_ZBwbr;*Ǒ1^3pu֛~! u3~anpE7?O"<ɟ\UMma@O^?Uq~jQ +) e$&u U/gہ 'l3W'3rVMۿg?66[M?)o -,*(Tf׏wwMmX`O {ro A%S߱BmVe߄I{_cD`-w}/< M1ZZCZ@$h,ݬ^y'{  6nV wy=LzDQ0t_&!IHe>.;:*[{;Z]옍 :-*.լcveQƟ@ N {@>|pZ}p֍ Ooq%.vڽ{>j%SW/mWoCI'wSn xܬ vW8n}1i+roڂMt 'nX$a@@"}v ҳk I5spD*K]nX@p!^g!)D&c˗~O.mq~G. 3]_Ix!ƈFa Qk_qQ:E"=(&姽AN  f_MJTN^Im3d.M&+#$&ìBvI+b՝JUkF usu.Eg8p:X%9|Bl݇;MtuZkaѕJJ8dߎ t=_ 3^M& eWN=s oԹw'N9`+N]o> &M5jJ=n=srs1hgűWO6 tK:H0UxWZs;BD=z'$877ay 5y"r? Yg w7ύK-F}{޵^R#L_O}~LuW]+.w=_k!xJN fN "}nf?>w{ߊ&> ۂ$8NnnPanNNb0%o5+`@khXh@_m`V\rewpQ :# MʵL7}Aճi><֟F w{kD3>&* :ע,40ϰ/O(S/êݎ=tys kgA79W7MԿz8f"z(}, 꺯r,1ܲzҗ'T5}l2c*׌Z8Ֆ+mn˜*^ֿZ>* ֗O{}xw i#ڮ^t}5n+΃|Ei_zNo'Hw$K̝+v0E}-|~Yg6!/AbXu?6MA֨`O+}U} Ϝ`8/a7 K$f &TXK'Me.ݮ Ut{mA $Kh-:Z+{}&~];~Y|'IPS>LzKߠʶ7vo>9Å^ :o 7`g`g3boWĹJE"Nobۢs2rk<̒;Q/D*d+lLhl4Gnݓg}Dsaհc¸=g9NQV݅z0-o۹}{79m 97Bvz:] {ro 6tmGV~@B"iO, ۰'v=>~Zm%5p^仿U鑞1x߃"ul\Zx!;'6O R/KW+>O8Nٶ޿I&9X/޹#_w>V-}{K+g㾜,HQcNʶD侻_]Tdu,|X{3ͅpouk(i+?_osև+.v[730V5Vcݾ ˞ Fvo~LXKEIL&]vΡO-S%/m:n_yԙg/9(uKV6.x?cBU>s獊OmI.\{I7ޕJ2\zWGvIп]|٩;6}M rܸȶCZ4R7J?]}x:mqD4b.XxYf,+^ Sgk=WMv$#};A3XǔrsZ*Au!4Le1pV -2L-Ɯhi+#}s|[]::xgC=m6EN͓|O]TMP'~%}Qj^MVZ@Rh!w<\j7nz5i'_p\%u̿'!&Xy>{{gy[;dw6 Q+GA_Z:O'|!ܕ\j D @A8I7ސ4DzKLPފ/ )2%kƂ'8'i_ {.fC"%*%wԑ^>޿K}{;~!:ݧip2Gor\!Včwq`~|G<ބfSo7w -nK+1|[Vo/q)_'I*0[?| [;|ڷym{"?]+־}MHl'{tO/ݾd-a\(/ 7 >ߪ_E4HIeELbF9 4O׿]Rnl k\,HI/?ZJXW ³ؿ9}ARw|oz94 }lDIq_#ݶ|wB+tMo"c{5Aeqz&=XXarn,W7\x v8h?\&RW{[.-<җwpJͫ"C}ݬAy"X]U 2+˗zpkGMI~!GRI.cum"@ӾurNBB]U]BV5)q=jP\뮫 "m^'{[6+3O\O]_[To }Z+_W@!T4A68  3}{e;sj,¸GA4}4Z[u"w<;Tn]kg ^|~'tOKZO5Rd¸OÝqU7+xꔅ^qjy1"`_ߜAa.$ qװM76y  !]}|$(.O Xgg} [7J^"ڲIvKun:/`ْ[~`\:;ͫ> m?\ׯjr%~7oޟj/B~}N$#m6|P+ɚazҶ7-0@[o &ύtۿOm/ 7k[tݴѧ#N_-iWǩõfߒ"gc4ve` IcWi< 5UZu}s~ jbk8)me?V5M/4)^ZBkIGbLAb\VCgۻhcx;|<~cWrj 3c_0(XQ@E(<]5ٮY+ B-_A\);Uw !ǎWm6v\y(SmM#H7 .w0[_UvNOVCDfAy3VwMZ\FYrV͗?{{ >|_[$i4FsSp0شʓy.V<\F#_eVP4md2s1Euy'E$k`i $"?餑 c. W Ƥ=$I"aRBA.a-ׇo,p]Tmz<j0Qu{w ]1gIX>4M87tk$i+ֽ_㯻zyF=RւHJY)'Orh#!]nvj{,^p2TP/DjOlj~%Y^8}XV+ [q!2\i u)ﶛ)}roX%_ ݸ#3ʞN ^+{I3cj.+ۗ2jo s䝽j[_D<?55AG|*y~s'?8-=MW:%Jv ඩܤZϏ6_,.&{$vO9{j[_;5 MWMgiwnեU饾 h7t^z>&ZºgOz U[Rw%r]^f5}+,-[5-\oPD5 b cw+`lM5+/ֿquZ;d /v;;tSZwc>mLX|^ [eM >tKe=߇$ljTD^iGk_7oJ_;faLreҼb~ *:tIuPhWD>dPIܴVX[24vQU65 ԗqrYT Sgo ^]w !<:lXy z2.D:g/Mv 7u~A`73ec}Hu.=KqYQ[iY`EVqMuCn.\nOO7uXhk%5-l!BƖ]h3e<ƏsbǷCLղJKWK@Xc8tT/[CK`p'iq?ñ9 !x~/^O}e^;FDf^^[jn.D|_.ۜ,`.`eakcJ[^o6o|fYiyi.gFuuba4gJ﯅cx14ނToyz[X)+p^!rBz!ć= GW v†_׃ 9wMUq}}xƊM^al9 $1Z+ OQз5BaK_C$ A` ?<uRڀf@k~ `.+,cu*AE3|To~M2Al+&k(a_ʮFݤky[w6=bVjvǂH@ r2vwQMLo]ԙ0J'_'a_afuZV/El'^o8֣]*X~U]w<aǙ.߰MYy3vpWj{5:%xb'N\Zg*3Y=b1l͓5]xiMi)nD .`i#OIL='oOt5R鈂pM,;qo }$NĄۭk Y ӰJ)VJ8^݃ ߴoӚOk[Im eN77v;\)$w!kCQ8eсvԇ-ˀV/`\Bw—hl'*?&Xhwhq& HUd%}2xnDv=2[#58^0.i{(鯢 W,.5!76;NA//AN\;4mX-;ոHpފh^~dzfg` [d+Oˌ4wF-NF+i;#־h(CKQۀ7 GPwDEO/+AZw՚uT{6($һ{9!&B'Y v{L({%4JU}6/UK c/rsOiq/4#-p4 ^Q6~&tZںs;xMܸ }N)e،.qmռBrZa\:Gރb`7[o_j(ܣxu_=]Vž; Mk+ApÑFG g>~ƋW'%Λ'5m0W)|f*~^j{'\RU\5W$|3K9־T&V>΋WUZvz^SW'kg{Eb?}ː>د}4M&!߱9}(ԎutMjWapE|E:ګ a_V孾^3v8,wwf7շܰ]ILjB YɈ{+(SW&T[vF톉>m}m<m6ۮ Ɉ’/]'m6C`;KIid7-%NVܷyijeE;o6ױDW0wl15 gcux53/TahP P2IA6c ތIiPM4w;_1wKq]7:w(鉻D_ϟ-}=ujԙ삶g +7"nP[RS_nr t"w!I #>Cɗ($䑄/O-;{ o1|тWj}[Ӯߴ,-7i򸀀e]foKM^(O6y)8S~5Ը͆X>ޯ,.rՋvrf?^ṲI&7o׸'p3w\wʛOu䊱/|b7}-$zkE~8 `zg 㱽my3~"1lh2۾`6 t- e#J_Yw2KFmN>Iz/};7w7oA:TCS:O E._*KDŽ?x{mƖ|%X]Z}$Gn[]4_bA&gE[4hCFj_]<;l}rfCSU~Y'Vk"V/մ_xW_~M&)ON&_҂J&}Oɺ]SS_d6=Vv .dJ&IS>'  +2x͟ vUZ|ERFǖ T)v/BSXX4u'yU" g^Xg 3b͂FO/f00BƂlIFr%8mC} C^oຂ'zKVh8 ҂V.nsE.N>9ޛݿ SdpGWO:7.^ypXKK*nם%VWMW:`^BW}k-Rۂ^CL־miocpgvpm$csc'-.966kjv~gA B>5|)qi{JB 74.F'78В^$۹:p#yzobD^];F"sww#L CMmv!yr/nWZWfu}޸ 4H_>uNCm:'"p:ٍ.F -&X@v &N[L cFi?5} ֌%]h'Oݦcm>IpRQU^ۻ2ޜMO2%2KGכ/🟺7.v>MGٲ~e;p齳 gՄToN<#aע``%XHApxU$c"ӝڍ\uw~ǂ`َ^ógC\`uY՛Ȓ^K~ϭGn}YnCW`#\Eoȴ#b1n/ W|(;(In4N+ WOTUw!^(9uW7W^7wGs] ½j#BQ/jᮯ )_A8yZD=AZOIOz 4i;9C?%=9XX[qpY 'tۗ07sJw .u%L`.HSy  SixziڹenRmϘO8^uVMvhd㇂۾w&1DW6vs)oY-Ǚ dއ+{qwۘߓ XgJhґ~(O|%Ce$Q4չCdnEm@tuk'i9e쿽L`s.st~ {u4Iu͓|oN]AQ> A?w)&)~9Nf¤{ucέp٧vT.yu~-(߫|r{| NSn֟&|"{jh=Pg t[zW8$r{͙ WHwK }q8'v.hPWmZs {?U _6r.Ȼ#TL\ %-oɯWWnA|B'TVj|e|!ڮ| MW99.{oֻ3MjgBum8^{\0K-*/qeQ(hY<݄h2LT~$~/~ïo]7K84WoHg A]_ёGb}¸eMYJ7m`hW2 >|eM`{}|9f¸ u F8n7_$b;O?BwI-; /kх>ꑑp!(Weky`W3>H++"!n%IzD;i𻀳FEg5]'Iyir7xm +orF3'v޽>¹[}g9omBݧVv7R3$jgOkUZE=ϏZm'ng!wLfg|%+~)pkZi4#.@Ӻol+e 0`]ɛ|%ai abAc<+]6Arur5-Ԃu1uӮpFNj^jsxK;ۮJנj&n`b놼8SZpFx׵̧ r-WBw?⾇bq84C_mo2_+bmO{64Ӥ2[+G+L55 |Y8%f#G-^S ~o~g?ZEvNoxW"_]Bjq:a:A\@c!p ߬JƃD-n7[@B*{7r康a\7-O5N"\8N7^`s2ykݪKwpG.; Tg1<͖Ê Kgf;pm}&m?xƺRt|ˡ{}]kZZNx")-k;mռ_4YtB5oW,]Z' gk7"_u!<3=ޯq1Y;?y~6oK޹%ļ?NE$lx%&1o^z"ī&cpw[d^?&u *|W}7IoHy;T\ߚn('|U fbHݶwv$~xKjꟄBVGM^Guv,[qa]bʦo!¸+OAt:'˽UKy8$$ߦ|jr{JpzZXp$>^ҝ͒5d@N ' ֤_ h=.a];[{Ud.۩yz¸GNY R۪v#D 눬ޔn|(|ڄ&4^4n-e_Z&>% Vi !Ru4hɌnUNlsve>_6jld׾~*M^0<͖MJf$ž@CIuh!N+/W~ݜea\I-kxX>U]W 43!햇C\.jYa&-w habA oӻvgQ=[;4\,Hj ?1FNM>!׶ݶ.-u =wIE@Tɝh&y2B[7asr4 ^j;yEě(h6<_|8Kin'xhc>~kcv^뀵&Rܬ]upT 7t[x+ _DdJ %*<AbN uիq[m|ꩶ#}<宰ƅ|ߠ`C;f]vę[PP#Qku־n7vo|_nC~r0-TE4ڸsaʹdC ;w{^Np]Z4:$O5[st5󯊓H~lݍ¸IyX.V?Yi׶ [~JKkڻx;~JK_vayOi'&6EStaEWKmtwT(%֫_|mxW A Go(`(]5£8 -.^.~vO\W b_[f]{C}puicH&Bn0 PŢcs#ƾ`OV槒d ࢥ$9'6{4ER3uD AG3G7.9TP&'RZQlM: m߭_ l_VVZCӳ~Dq`3,%:De!tXShmqMhwp]&I?ĂjPoJcb.$syτBzeV[؀WM͜M~>vt6; J\C3nM#G8|)CkJ˴̐,6NtU Ehz)a Qä2e& gQovLu:)s3ZmϾ(y+]̝ UȹV],e} -?vAsbjE /5`2cKYGXzvX`x(c5Rc" 80 .\73o4 MFZip#@E?!+{a3`)V1`X@9PJo>&[؂du]wv2ӣa MQڡ^̎x"b> ._q]ݹ%f0,Da:\A(aΗCrRo1qcC@ow$ڏZU ɝ7↯p~PI2 .Хbt>p-| n]Ӌ 9r$T+BT* 1 ]ϜW¥3 WMV6ۋm#X8%Ew<ǸЏA0F{ӠZczH'rӘ?)ixr1ß5_e78l#-3 H/W4;{IrNոkEd -ڷ knj7i+D7- uQ>qCU`tLzE}p!F/%pUt9:Q-ӛY#.\f2hRKQ[ATռ@'e͋*9#қ> gݷjYRe#\"@Em~ݶs0/>Ȝ]HIĂ_p q˞nKְ@i}J8lFU}v{BAU 2~ͼ\YlRZUXР>8;&^m؏7 f[~p\Rҹ3cլ`mG_ Ci_kSPWO{ ;BrѹpWL=9s%Ց0G|:1o  R:!34^` 'oR𻕢C%ߺ!ɶ fiy ;Z̽sPO)' :m#e]PB9}dž(횤H{Rx]ÎW& # acMOMl%g-O(ӾhY[0.jAo 0#DQNDXWA C)˸'jmں2c5 iel]W0]Oԙ,5{37*a8`35dnJCɕKxY/Qy`~Pg`%zQU(o\!$#D NƳ1oT~{_/]K|Do@D;,i򎠞~KO~Eٳ46M-bccFdQqz^;ר"7 [szm_281`lA|M=BOSu^'|6a@L^1qe2z]ccQa]ubI^e+6m5m?uTs+Myf yI"Wp_;8_|~@8+;W>elUܴw7wO_/ieCa-Mѧ*SI%o8K>7Y^|^[䊅A/jVo {}ߗ/'- Cu7עG!DwxyNaXɌaLo y*?ñ;r* 'ּ߬=c|Z5:mdat MA|4 30.(HB`4DS)I{ ĜT;ɧ-^yvLgkw;U1";}q2fPLiy-$@M͝E.aFM@]> A%[ݼX[Ià~2 +JX'~+?ZA?~VQzJnVVڥvMt:@^\b+o꽄6 5DJҞ <ɵ*p&^_U훵Ӫ<"?|8#io/ψɞ X4$ RQ=V6&p--d-\oGBM$?64HZ-Z<0e.6ѓi`Ca.BdA JSfm.*.xx,8hJ FEf5Dk~M b[ҴjP#NS$[ԦYpL%/k& h9C`t׊u@ޤ4gjPGCA4dW$K4#p VCN$s}/9~KCg2T5Ow6?M>EN ਚawɫwOҷ,HLLhGyTomX GCOwpZ]f ΆZKi6n G:T%)z$4?j$`St۝ŧl;U}0MX~ވ #i]ZݙO-z\FWX'O;RΒseWQRKv.lE(B*6厧lz2d4Iv,շAOp%vod)L'U.e'N6GxEP+荕+[ x=3rT{f$e.` JBGS|.C+p.γE9~$B;{#{xD_k>1GZ4 c0 #y ixh,"U P! .Ư  aĮh4Nc %qiTK}"Sۍk^Qeo q(]4! ;͜l%ݓh!^k6 *|UƖUv #52mSc'Lb<†ݶxyBp[k|&f'x΍/ð7{ ` . -?C;᜶\3`BvqIO;[l~` <7a ۧIX~vXN|-seVWIWWEBsU ֨NljTłLL-;r]7/7Dyt3r>&wKc#ie8oFS,XHNfNu+D@$y1׿a1S1Q)Xp( 6joe O̓*3j ]Zg1%* bQZ^x|2/-fD9n gݴkef^Jid_ p8MW&w^C,yLiV]o+d]%Sۧv,*l "`BB Fs7]~waHI2z,pU{}޾͇-:4j'vhϲ nZa"qs=*0LT :>nڏ' LWM7^YXQ*WgaAo+:tGnm],H~0+FΒ)X | pYXCV=r +&N] C+SrH*$ HUsZD#ұ]ʾ #3Lr K7t{jΕË:@)N=w>`Er*VQ {k<,pA0 io,pl30,a(̙]T +ypmCdxwԁ0"h|hdPH@h7;dvb(y3MH Cc9Ĺ M%33H=`և*$..D eWJ]XJ{DV dh%zGF0IѰ[M R̸Ktc׍=WhF4|xٷݬWe4pfiZD3kA'&}q qo&2@Vw~Z>=bjgmUU F ȖJ;z{ڒ5K};DŽZkSfީx<iUC_Z uUjqx}j\G׷RT >{Zx$޽A&˃Dby DFӽQ8;+a3/'^ROKܛn#%*iz~el?}^H;ao>L WI= e**W׬pư] F^-BO^s0G^]`Z- zA+Wչ"]S7ȯ-b6"<N_B8@"{qU| n C޾K TKY?>BĿ+\̾_/SM;~t^<$.4qq{-&vl+`_i<m #,\׍+A>zj? G&t)žakţr$+aeC~ /<](\My}A]n,v$q<7vK|8׉ DAzUt ԗo}[ ]"DkM+Dvwp%-{ UH> 7Jp7y$di4v 5._h/?Muڨ~8taE7^ ,+_'Ks:X!^  eָb 7bW ~ mB?05 t}zoUkϝ¹Ö-h`vgAQT}% 2-'|KGY3%W[?Zw{&KoUT03G$v%?Cz[WgG߮߅qVdmk0~d!NZ'.{!k B6VlGZVz+}[0Jwe?ǩ+˯o{HK'~=ig}72[VvNu' 轅so{tDE{5$mĝૄ0ի \Uy};%aq3Mz -z:԰h(M;YqOU <׽ZZJҨ[Ƒ_m?j-W6T[ 8}MDbٶR~'הܙ &R6+ԖK%r9)|wwipIv_&sJxr eNUpFDP$-KI/եQj~4/):ԋjͽ?.fy^+I>;]Z :&b#|ՠ?֡l Z+ AwZ`k$}A)a0ZwjzD܃Kͮ@uZp{t~@Y{t~ {{4rȯ* ҿZ %mvpQ݌,m?\4I~~q;\t~/s75giWtvtpA^5|-9aH^\_(I{3&M0oT#4_.wᜁ^#pb9Pwo34~W3}kŖ kQ p2op! mqh{M?%BZoN ov+{? |+O+ h j>(}J}T,;p}¸ٟ.{n^JU4v_v1? VB5٧2c?w^~O)x|3}ރ8e D+oK0X]imPegXOPX"W7 HO-;g$/-rzu|.ᛅ{[hV2\DFZcKֹA,LaU=VKl.52mYM7gu߉8GNݩՃ@EǼ4Nt )'VD|ADϰE74SĊ_)WRlW ~2r]Xnﳖ-ϒ$]Nx#xޯ3oz E?_xW0#%&)63ߔ/1y|pJ0٩]_i"!\y`D⊂%64ݵ}9te,M} ;V\H"H)2/;&mGq/0nC~M۲/g_ IUgUu4\Em9v}L4[]5 in\}χZ2z]tݾu ܰUצ:\`wа*ḻ\;oqffdFN[1rgus*ٿ )K0`0%%b]۽ςmRoXq-:yع Ll/O`1=Q3sxu]<" Mkte>yA᪶(d^iaѧX->y:g @Gض,V˦[¸AMGoZb[T"-0DDan9G0]x"wg1G&ctߚmzVK# /3bw/LR$r3uJ{M}6>=+tB^4', -˯oN^DRО[xuӯ_-|!SԌ:;j_ϻ=h\W|e}FK%ipKw߁ };< 4}U{Wn_6{pt JPLnqs)Z e  =]JcG৭Gi.$ꉮ$.죶Zw D62ӦV;7yR4jxotz^OOF=b3vf?Ʃ5<ǿ;{cllo !x |)0#ww/@arӷ>? jTRC %=$ {g?zS}w^ݴLo\61Awχ_i4wW~O]=MTyg!xoR!ksQ~xR}]{W^^/}b]z:8c#׾7Pcx_H@N[*Po^>6-n?Wya-X^ As j9n\6 2?8- L7"ڳ -Jӻ^sEݫ  rû 52k^~C/<],gWe?kjtļ\> Ab+\>=}.EqX QO.KQŒx-R@5%R=)57eA=թ {;zw)L4.68:>^1Hx"Wh/:ҷ 'n& `xa=eoDfaϚ훸<-M=/B I${rIl(Gc}O([;FM, ! Gw{0+՝R1W]+ 2֚TL& -/nh(յmN^Lؼv8y41 ;º!nIGA:i :o[ ĒOF߀m=m\>{& .,)aWv6қ3t_pV;\{];6Zp1z!wRѩo*?ǂzod\Q¸i|? 4m`O6,yGEkd8(԰qAʷAE{Zq,MVz@<,W2}:)kϛ'M.ߘ e#0+6.|0o<E{I 2ۻz6Mawch)NM ̐.}\ۄr^쁲ysLq+ )k 㴯YAӁ(P-6]3ŽG@oG!ŏmV@4u2Kʕ9 k|㱾+&.efҿv+n^9[oO{ vo<J 91Xw3ilDːi^l׺:?N`7f2Iܰj3Nj'&$)ݠv`ׇFe UH!͕n  Po7rßwpB m^0|]IDݹn+?}3v¥ 7vF'ފF 0HmBu3 ͦ9A@-_@ .nM7jjljI&oA.%I88`|+v|D] "CŃi+ӱiJ%%„lEx P ȞԱew^L\te_4jxo@Ӓ3ݹֹd]ůPHaϔQ.>6w CX>3gIv@I0u }" U;wa/s&跼Č\)U}^zH#\LK!Cc[@%YF6.]xnZ1&PL2IwЬ/vw 4k ;r"{l=8V\22 :__Nz x/X yޠwub"Z B i@wRtwzp󒫴 ovN{C)d"1gZ߉ؿXvN?Z>(*3[x{_=u˯v|?L.o7b_0)^uxn9wԼ#'7' q2j!JXcrEYjQ^Y-}[gUj|ZBԲ.P2VdXP?ź߈WW^8שwn']z֧ؗo]B>'Z [x\/¿?߽'k}Bb|)?Ͽ7:?]YBu8Qxf1#aπB-7h{䈶OFx o}3^KN+k2wxW]XW&~pœjǦлM_'b9DA¿i[Oݯn/my73oشz͊5kq?@o{a\ n$hl& iǛr簈{ho"eestKijgFJn M[\6M f!epJW耏y}abEk/uj A>e|3"ǵm,3h?<}o X'09.[;ߔ*$ ri_"!|.~"Z/jo'w~B[ŅA =`QņMo6a[O񐞕O|K>ck D߇n|SG9ּ0D& :N}fI ϭW=PC\'zn^ޗCtާ˵z Bg(V0i>Hx fϒx'97DRw}IiXɄ yEYlAik槯.ఽϿEpx%xmViO|Ӧ0OްBMɤh߇VKeN0΋<-._l\Am}/U$BtO\)P˟@4uIe/wo)mfa=mCZ2nCkf9&O-iB=Mx";^ywT%{Dq}E:Kn۷ aetҾ'?r/]t+0UV 5ZUBZmoIyIYX^- :+0`<< .l6uOsizi'CW?(#vҤ˜%ںu ~v&&{ "{_h~a/}Xw'G+.y-t Է,O)|%C-٠S_kk~8G_eW4=ᜲ+u?^Wl{+Dz 7Iz)[/.2񛖖V-~{|8xΥB(Yzyoǟ9#0'teHY"(i];K.FR?AE tL(]ʿ%S nܨ|?K.V쌴 -`_)4/F&f^B^LFG'ZYzlNlx(|O^ݶc^Eq&zg L :֣{6 !q:Qj,Q"NgwA1|Wg{wnk˻v'7{Y> Gˆ?uZ [6%+'P2Rw|^@CK~wm| ,ۧӧ@Imx+.:V~(p&|ov13e,C5|<:\4Hy~JAvo{J? O~tD.16oA~}/͜co^㌽ܠ\9vPEovmUt7 5^&oZ~};fy㒷/"6ٯ1jo.z_ ,H|oa0I/Qm`ς!gbFz-/~7%onoTfGG?゚"1p5/Z~ [ϞDy|N|ϻ3WMc|1U^5ߋ}l,m}& w->oE?ѻRp/o9kD8"=$p!~;S??{ZO8AyQ1>d:`Ua_q7V5S8te*l#Z9ݮ]{|=⋅wOmxD]&bp;K kjO{a{eOM eƺkT%_(Gn?:gPЋȯLjW6-A)(%KoD/'MJ漿\%ɵ_6X_U[_*\]W{ƋڿݪZga#jwjf^K&|~4ʔV-޴ޖӒm3}]ׇ~#;~=A x-~m% U䧾úUUR&x'֤"wuU;&*T=o`-zQonUYϓM:7>|J;y; u E7X.+!- TBo>p bAaUѨs΄uN]wM*U"paGnN9pgQs-`;<LD+vb훵3o|8O꥔nq|" Aj'VYmM<<z.o6hf ;){‹][g--aurScpjiXoP|V}-`mR&cu^D 6Sf` ;=pF u'n~X'{\xJZ?$`Pԯ=N\7A0U[|scea/^5+CiN# =p5,4Hʼn*5C#`&0Q~5ZQFGj(NCwcjySy KMxa@>drA7NbJof Dp`t6l :h'][+iYW QnNvOznx.vBM) Qp%ioظW.ҔevTBS5r)YMwwOE\A jpcj)ڇf`hͧ56 D^'WR\p$qŷiB*c#12!yRO> ׶S ˽ 7= Ęd'_^m.͆xHx#=|vե\-;|MXzdžɲ6d$Zgb]G{`Z^lpJ}!Ca:WVYHc>xX+oK <ܽ%٘ 7ij;w&d-K}_`vʧl FV#4he_T4zTv͹=3 "kw{Ֆ.\:ueM \V ɻR2M2œt1,HAF5{.jJRH|i7-/6?5H_–:3-wA;nçjoݾ}n .պ%ȯVԌ2+K,KbA>w~O<[Cl?^8']n}[%;D 34˸L '7c™i݆fQl|u;6rҭtIrӂQ{!|F\T߂,]|EW]K+a9uzFkA=C3TvFUMK Q=S 7xH#Ӎp uXTP'{~-%mi&\+CI&XLcc.v s厘Ay ԡjnAE>ۖdWig(+[j.(Fq_{k(nbb P~mc9㯷> y!jpqZfP6~.>DB$dp5HlA^ d=}>N>IW+q\ުALx?(Dv;/b*QYmzn|c3p@Pפ=~*ڳc{؅@5^J Kh}9R"Sf BYlf.L94ņc!,/ &ҳ vN?$'RBKN-`\ޕ lJb*ߢ꿨A:u~/o*f^vKW Kn>>~?Z;^>G.Q΂H;'͞ZK'^rWV\\̽q$ֆȸ9U,w9Wcq\4z~nY!(w>QS|OE+A/bW/pּ_pCJ#g~ju'J*q&2~] ^!x2?sUrov;\w4Vd>N]jO, _?_־G|}ZC~j3Ry^⌞Rpǩ>wu Ñ~&u(Y_-k\Pb/eRuC}V+^* A#Dإ<ݾPE>>,}pxev{rA6ssQo`\A]!2ks Wn %z 2pU@DZcKz/͚JθAȺg.G[ F[Lb ZAl_pD! 'O8Я 7smFpVM f!1<wZ''1(rZNT!Ml +lWZϵ_53T邐da_㵯3 8|BNWJ|lZq#ˡ %q 65;-߇@Jf+r ǭh`مTU\|ָEma_C08GoDŽ V$ (m"mm'MNd@k<Ŭ-"k]Z[۾odnjK r^o)m|]Zc >.{=tNI׋u&8;'?0g/+Ec ?+ Y4*ނb_Kkj6I0|]x,Hr޲\ I5S&]``T՛;ڷi5f ΚmIX3SfAol TaA0.X1[ߵj›0ۢ&n|R +[jɿ>hm"0[stiVUAaVzoiM'>j ]I''ik8'0ak]A1Vk!yp27-Vp=ҾauVb龨wKzGpn1Ww6MvF-IiU`@Q*Im07 >B~ :#:_S}{mG ƒnf5'I{v"l˥ oHK,J!. Oϒe%g{ kwz3y[߫VX[-MmA PgRXsŐ"t[MasCMhs}'z 2f}CchGw~ՀbNQ{VB\0 {ןfNF*cn,eDMt `o2} @ǂ3pX݄r |Z~ϐ"9m4;,U!i/ol'7 IvTU euK{Qѣ >|{S t4 c  }?Wd6ae[TK$B%n![טvn3 g\|#lҷ)—,n|̥\}[6%n'm:%p ;rXzcSyiRpSDI* :s5w>u (cX!.X5ӥ~ 7M߫W로FK$u WSf=Pcݬ?&ē|o *۾/hA_7K"]pSwmЂW=|3f`Fϙ] 0Gi(UٓKI6r0. #(0xC1 ?3גff7sCP;`Z}"{7D` /Ǝ zLA.u'Rbx!b=[T.ߘ"ܪ-]{U2e~5ֵxi& pI5kWfE#`8N>Iy :uBjx 'B!]kSd3D_/&v A&Vz'u'^o}{Rd#^;ZQ~/~*_'UUMBB XZAAy'8LL='Ve%/t4݇EOߴOVļ..|2&SM&UxN /j#W ɻ_O8g+7Ƃo\va*wsjA"놅Q{Q\Cڦhlo׷?}^'d~Rx͚q/.Է"گ8=4s_~pKStW.+y{hG]>#B+姢޾+/6(ֽN  + e/M k u$WU;*z`ow\%r^#4ahQӆxT_Ee 'zͿqV%RjWw~շ4> ɅGʵ *wʸD](H6Y\G n#I\0G{WC=SĖo-~ v?4H/Y߰4kG~٦DY/ɟ%Q-o^׫pLf3)o_%wn8=V!~WDˍ!--w;:wk8}{}GЛm^4[7e-U_G7"<%7($a&Z>b¸vz6/}>Jw*JqsC&^4%{}gjlZg,mw։cV*Lph|3[?~//%'{> C5"A{ۙ| IdCDuq Y<\r~m~@%wNgK\nQK@ZJ9֫M?՚7g`WNmН's LnҌU["am* .}z'xV79\:/XA>;+| 8al-[H P ڿ -_?†]7\>@gu.z^WO1 ~Kwzr36~m־&2elt򰾺ˌe«Gϙ O K}o|"V)hU_N/Z|@&JK SЙkd L o}ߘvKخX+lTB'2("/{{j *|Ejc&>})،q{c]־.twfgM[ S:򔝑sZr'@IjZծ$>zookLuw[O'dF;^կ|F[#;q:6[:w鿄_SgKeJWI]ͺ_/'_ 龓(""6U}Y_U'uX'sѭ?vZi_}_|.׉%eϚҦ$gKۇNX+~ۍ'U n+긹YQ?ҨWH=O8L+ jM·{\$t[W¹a&塢~-% p@a{:.'mi.Qg7p-JF h]{Ƴ HϛިCQ]KJ ꬟ +~jRpr)u̺i;y䃯, *%ɻjc;3>L+n6#? w$U7 OM-wL7|ؾ>Wgb&j2WcD~.r{1ۂjI$ PżxO>XaFsDz y|OP˗ f3)u6Nʬ }!o #,z~X)`g ½^3CgxCI\w^n&iwZXW W A"-<[l >yr] Cw{qS<|rhIi0o. gV\@[=w} eଵ~5n| `R-Ϟ!3MvpW{W/[ v *ۋBo{OnotKlWbiKNL[/` $/¡ : ^5=!zO\)mc ?4* ޯ#6/}7|h_1Mǘ9iw{]63d>]T^ABvj}KQ ,*P>Iݵ. ݼ>M+`m=wƋ5IEF7ǁߴۘ 7<FX~ڏOE=]B61Ɣw>)-^K ^w{O{ѱg閕v+0[[ꖶRڪi?}u:TVO NY&޵AU pvԴϋ>^WO)OWAt Zaܡa2M3Jq5;AOxӲ69Zm1W0Iu_+<߆3Q0Ec{ˀrqM{JɺOi)Uү v,c]^_<NHkV(m{`-w~ ~ ADws2Zdy{\'+ud1k.wuufdH( POlR|g}V~tR3䎲mY4]+f<Z/Ka@ߐ0o v}2݆2+T]}pIy/ n8B|Zo.´aAe1s$swY).ZW / Fۑrͦ& l֡ =CZଡ~CƓl7:)YY-Miwa+DR_Zo ]?~ӗkJE07uC}JmN,֨V*9+367LV)MK B+6LV~B(JZ|͉+wOUI#=$Y{cQ^+DE΁mD7/Bn~> .Mk@Cs~+0"_3^MvTc4$}9>p$&;SW<{Lȃ  [ "6"D߁L2>dlX+da:VAZ K![$o٤G =jI#&/ %֞Y:G; MN  3挭! uM/ DZx@niLMt&=G3  *Xb4i;/:rl ÉlG)U1[J"*B>~Bq(b4kNtF5m/8aOcI/I͏ fZZ~d_,߭tmp^[  ?Sg*~+e1t` 93ظd N OuSDR+_ICc`eJ$5 . sB9:!\b}v.+*Di׮.2HCXq_ }GoB~>D!;]w}^/͏kPx0޸1ռZ7;<Uv-7ysǏ_/l>&+^|g;{OoDj߄#z߽V^Z??eR(( Ap:]jۃnjclnw᳛ ,V8M{7r=v~X |d 7>c ߋ>T*g Eb3弦Qb Wvwk Vn9Uٳ!KO ߱`c&i<@ } q+߂Pv/-$ys\pE- cw$|]V#ړ;Əҗ/*ӷɗB>I4Ѝэ+x\i|W7m'hz҇Gq&PHEl\ߠ;FgW ;-EED_xšXBZ{قf8nqxfECB\ѹqo[o[ԖUa2c;0d۳}7lsm)R8QmLo /,ӽ<>4|Lڷ8v{8'meDus0pj wNw~\zw0SJo~nKž6$>F'[evg!˘W?=eEn`_Ta olW$_\g-Za!=2GW>3NJ~pkGLGq'rvO|o&f}ҽ>d..Z7@C 33ܷf|0Z?a.{("|ŌUoKݸ@R_4ơ\ nDO ؎N|ogb?@{JGxre { g+;OVR$bO5B K߷,_p )~vMA9/la1^=FBAiR5ܬݻm?X$܏$}Wvf[ؚIMɴu7wۑVkz(!n!zbYϳӆf'aO? m_MME˞6"J0e϶j'4Ag1b*P/\R44vK_x efv7Xq[.̀ -]q bо޺'6Z󜔪"f(j9%!F7I\=h !M i;Ӆqi6#yhl͊jjDǼ~(V޴9 Qj4ΖXAOsH'n%BgИzC`NoadV.cꚊIu:԰ (W_Xx^3[%v7AXj'0[Wio.|jIHx)_S=*n2h w.pKB!կT'qBui0?^$+ eQ_^M^vo˦R7[cJ@ģ{MdZ@4l(~ߞ㿎ПZ@wE߭&;U!:¶ײq—" <-q H0%A6,8{wU\:mE /'}>~QJV<$ Cw[ڽ׿ߨ(7WJf.A?3״ ~qaEt.zþ;K3lƧ-/nJnDo 52ɶ$ü4?0!aw0{m쥧"\ n]\"[]7zɘh3A٫ȽKw^UfduK 7k&$͒?i{x˩s HA!: uRXA0$eϔG|/p)$?d'Cd-%]~ܿ]MS~Qj| 7m )隉3&UzI[J$6lI 02/] C g{nJxʿ3;ߏxg9E0HárǜLW--^%Y@?78"}cE,F,~L^]o͜}G5:OcWiϞ/$o|2fvz TLV,,nCS7L Y]+g?;ߩ^$Y*D%KyX{jD_[SzmTrд?c؀%-8Bmbu<;a.\|: -n8Q̰ڱä'.sB^`gϋ5,FFʾ, Rr8W_oVVhx,8-sb-;A&߼AC8r<-׳@l־ ;j ¸kޞ﷫»ѿV \ƎA.t8g':h{5WLo:OlKUoOD'W~ JO?vW{UOJMкw־il3/ ~/.eKD<4Mhh"E;~Fcuk{uɤyx#ERzMi~nLb+ Em,̇\ѧVv=y2yd]`sîw;k -҉W4ZE±b>0KbG ƋPP> wO.|E]'pV@E1Ɩg'oy+ wQ~g V=[Z'z"?|G Yv6Gw*5o|+`e5HWgAn7}C]Z{B]ݍyPϓ~ b奿$5+b9v^xR}ΓsP/Q#Os0Òo+^$!@L^[IVˠ7@M](w>g}I/k=t7#.7Vt?/&EnFp6[wFxO)ң~׹sD,(1芓PE7aS]KD}xF_j ˕山lfZ0^!d82?R}T( |a-j uV6ǂ GLW^!o]U}2SLumf3z>ߖZ nxl/,]vWj ]*H,6'Vr_R[N6Njn`Lpw0.S{7=fb57DUSL~z׫+42+|}y]q\;$ 7[E'n *Jvo4MbGڵ6耋`ܜ,K{Dס.$I [^|nQ̦2I.Ɨ-M?Oj%~@dk+[GKx$4ݿ d+I ^ dn3 *ݖi x)W',o||7w65H.>;L LO$%|ҸX"?x[_ rT ̇U\'e$>>ߍLJ+yj>xމ~4&Bu{wvv1+^ ~Ӌs &s<ނ@h "tK2Ϩz7&;~hOG+w DV]@t;s ln$X-5z×&kCUej;KsWy^ b?}[T٦QG`H6N}{Z2M8l~Q׻~,w:ˏ ֻuЪ]VN"_īН_(q+cc7ܿWT nv,?^aYupDž'^|Tg<_'Uў&>A*8z:[ˏ ya]MvF+p/K^qUOpP?Ⱦ8$ i7B`8*Hn|Lm]w=E}QbFR ۳dRm8Btʂn i_uݼ#ټo͕޵ʉ%,|3/4x(f|կP[{ˌKo ћˏ3Yos=Tᜠ~קeW.} vWaB9iT3 6pa$z7/tl;}ÄZq`o|yQw;b,Lo3$t$h,-}6 ~w XeatWNsMKysEP w]?P hqnG%=B  a7) n욺KNi `"Re׮yԚA7WGk:pұ E'q-˻]م]E.uQr|Ek Cq^TL(53؞u"^bĂUݺm-%;\~{n#; $Oc;M~'O/1} A>jW~7v d+'K,}ow "Jf{{`$/u} kON9Rlԉ ;hrS.'UO /k4<$hzSFRp"'tߓ{Z;k|ܞc+/΋※jp6J4YΟ+^>Qyr6\ )| ?)>CZ9XOـ$KX+k:>\a[Nվ$ L\m?EjwK׸ ^%+/׺-ߒ&7wk>iW9_AH5(Jյe-\گUtw }ʾ7/^tK|,HpOD_{?q)6u5iVޗ!emοKg ,c6yɑ K]RKWպ 8-e*NC_Uxssn]5`mCj W0_ia: /mwUmo > >_7hi&U6k MHv o_¸]~-x_-i>Vg.kI$-m}Las.%oE_kz&ΛpY{> _MNnRQ^'[6C]k!VӮ}쭷CXW.j|D|+t+_տr!&'^[0_G}P_/'#֛8;=:0Toyp򚧧~q'gZҫ]9."֗qWmkXgdmVsoG+SN1)O\OtmG]$KWIxW_GW|ZM {z֭ڮ2Obm߽i%G_I*翗V*bWum~}|E'|/uK&զI) &-,;¸Mw7(׎im${ MTDfpENĄmP_J@пl6Ii+K?^_&P.33T+!Xg0 k55O=U@i2^Ц${*Rk_|R(dTuv%Bf/Om_~UW^O䏂+kAPS8ǾB>eTTopkKyY$49/W.X,c}/ uNs !sa'`Yގ췷|3"װ}1$7w>?Z^>ϟ,)}yJ G?KL Ѫ}zD;/t_7Ltzm3]$L.-~k60?^2WA%WW^nפ}B .jzѵ8uV\߈Q'STNܻ׷ $uW{zW&n^ǮXQ-~^^M_?gvK?&>0גt;Wע= UH[ |^:a8T[;b0+~ }Umdat A>'?f|;qVQ7vo t`ǖ^J6_6h"ưVҾjޠHKjjWi4Nj={MHqƫt4q]'x<{ z]Hq,Kh4Gq!;֩"7`եeP zA=I#cA!ܬuue^?*|uУj?tNka)o/x3&u]bJ8 njPGwZ#Z,N3[+Hr~:֦$x[%͟˭W*ԞL[/M?lqs\YjR6.o\(~PFKyUXMXvٹukwd| p(Bsվ'MjA[z]׾Mߘ[i \=j>].vO[l}sVy>W7D:'ɇ/sNgyMZ"˽&.jsEMsxX60ůZM_7O#$u7?u|߽[:kj.?GMI6} ,ںhZ8"Ϯۅ;(YؔM5fk{o{l+f¹B[zNШ -k roNj62w{+Tr.uuz*OڜH%v/޵N YM}c^ 1 7k¿nq~qwt:.PnY2\ky_nE~ŗJV,&k^¸1izi:z9U4=i[ ,tۮ;mp[=ga~}gfɞ U\')3jW*9mw/,ndln 0k vI4֒ A wbt{7UQ|{qÝVV^[z >X]효FhEU|Y+9sIHܲm?ۉ/Z撓 .l2:5RAQs5iψ ە4n9v b#{} 0]=|/,)fQ{@x\{▥QwUidɞ;{I2UT/hthׅƭy~n){@KY95ܤzzi^_&afQ^Aۆe'&1$H.j/oosAc9Jk Ο,gOiVr._kMfy7֫Yq INz%֣D+_7Tw]zH /&4$ӿ܏?ZüN/֢ UPN>* ~n ]j/Q^kQ^2 g|}BׅwA:6}ra\^ 7BkW˘Ewj`wl+_'Wu /~Ѻo#fmiз;0NUOcaCx}/꺧'^'Xee$v_=xF6^n0>[~J xc5Z׾6mqw^6q8nZ>;|"^K\w )+^~ aDkM)C K  \vN&wMtK_6#.O7^] r 렑.ֵ[bW<~'UnKoOGVX ``$Ik(ƼMo3<ͩXqOwCK !6*.n?NkXW5gi nrg\>վOlqa^-MpԼ 7$ةE>7¢ /s0EU߈_?(}[B 0^j|$*? iqNPUݴ6ۗ\Gnn'ʫ6ZXwD]?ܹ>[C熆fqx Q⻻ARkg,~߳[t]k&%Fx@ݟwdz3| 4MɓlښDž?t0p\C+(Ԕxy}7wmQ솾.( !uʧW m~Kp+@t]~X33O|brZ0!ϔ,$n9~[rA'yuMQuH?~Nmxۖ^iר}wh]U z0>[EX:u}x7iWO @_i52l+IM<^Q5u?q}-LLjn~Ԥ&Um~"?'3<婩>W)k7DrҙkU4=4zUͷ\`X|/Mr%c&4~x3HE_Rle`E5ii}u(1ܿq/d4jm=ޥY-'gym?ռ%7?/ NGCup@K֠FBx_p AM~I*ALY2?%li[ݪ>π#W?IX bX͊?yxdžNkec&1xFja\t@XjS{3RB66֗rBZ Jƛ_X56%gr353qrNG1Z9St߆C !}ؚ{4'{ͬ5X$lf]m' e]*g$tD4f uSJppIz/Qn&Dʫમj:bA;IsSYu7nIrQدcB[O} 9*Q2? Sn6ဎߔ( h7mV 'Mćo Ӻn%0q=]!^( ]+ON_}B{&Ԟ}9eSlH0%ۏXgAImOڤ63U.V]B0:3U..# a%&ѿ(#Hf1O ym4'f;CN^@l( m1m#͟yBwv1LXUE4h3`H gmHy)? J1Bnp!W?O: eqZ[e9iAzN4s"!hHEURr]0*a `->jŠM3s>폼vo.FI7޿cFi>sGF~Z"`)B]s|,0Ԓi]iH1Gu(Pdf~>nD qkoި=-_(FkU\ U8أj)4I|̈wjǽW˛d&G'^{UCM?64A]M] hO/ L qT< {uBA=iTŦ;Z[G%Wh+޲6CcmTRZvjMcpSRDޙro%|)/ o3qmM+o`OHgG? C{jM0!3!yHTM:aK;w,jѷ#~q9-%2p3|@!kJ$x-v ZsIH@.&(K2\pSq n/ߋ̫K+"ĠQEX(jn-Q=,p :>X3o"!lFDsaMpXOZ@eFmV0@#9_ mfߢJtѿ"Ma8!A&JxꡠE^`'bVчF:%6<9&s~߷ uR3#Z I(OHjyPr9[Oۀ Wna%0l3%g=6S$C` " 9imNJO&ܗ!:GMu,?ƛf ?MYKO$cu%-0'{v ;\vnZمĒ:!eq!Yw5|EqbM=΁q/$!;! XSvlAt>ۡ mm#+v^0ǠOW%LC"Hd3Xh N; ZOOyٺ.a ơ @4F|`Ž'@kL-,;˂LsW J?p(o!N<8"N^:%c 9XrfV8𐽷sDƖ8ah(obO-J*t mwA-p(RFS]콸}@U;Ϝ|2U.0][OcJBtV1mZd7%^cl^0YZq^(smj/[awwNoZO+]MJ7Dlۆ01C "#L)$ "kIlB 0 ÓI4dlb ~w+joyWmpV >c U"CJ`]k2I(`(C%aQ*j7K}`<}5џFm_p {l]߲l |2ic>q9/O FZ"'iXfO@o} 7,lh:q.9ҡ y%USr=|H)BfDi_8ZIܹ~ݙ>WluOH wv7DjI;XPqrC4\C YecņpL wtuGqW!TIpL(edi%APQ!FNfmw-#pepN -tx6}zNVen69OE Ic/? HcB߆}=Rʼn,A0w2.0]c+F:HlsQRV vR8lNZZ(|㮔n?Ƃ{ݸ!Pɔ7&ha&_mxduM Nq$Wc uvyP^J٣BKci ulPçCD44Q-7ҭ*PKeU$vnf;C{M8V"m]Z ]DxQ&&0:i5M'UJoE.>P' bչ.̴.kE#ahVǐ g٤x^aw :v/!ʼ%=Xxӿ$ʗch f_H\V+jahEȭ*IR6]ovN|0?e|§غ&ԙ3nV +Kn]zxzn^4,I;€ys.r뮕CK'4*ܩg8|x cB/ NK7E} hiWUI^+U[ͨejw3ѣod;SOsSWw6xB֡<]}Acskp#7I̵i&mv%wP,6ՍTV[m`̽ܬES 5q!(f3ԟ A9cvRnV .߰+Feщ͔%k}چqӑ =$)!2 C pӑʔbs7 7MuRH?W x8??gxo/^􇽸N|(Ӷ;٢ ktxjF\yp<1l l m2S=h2$}z׾Xye#]m7mX~"3+i%%e}/aϴ04$(ⴇMx%1(!Q[i>eTl,\X\~oP,(:F)6G\Е/`,jZv&~}zy:sx|j\ck 7غ= ^~khP<6I'byjSo"plGwoR;NnK]׆1&ZS)R?|!!7Z fMbZ?^^%aBT&W5ˬ;PE /ykDv~AzgN_ c5!V#}zUר[-'G^~oxY(<)|O!Up WJ@A 7…;3/ +p#eշ< ,]TmFý)_]ZYU+>"ޡì֏mlt'RO o~tr5n!`<θދnpD#ԕ\2Sw/0K}g+f߬w kOI'+)oz]peJOݿ@q¹F544~#n}ZĂ_oG*ep ZVmN+Z-y~3W~@ȯ=pJl3 /{a]:5R1Ͼmm3L~Y\!Cd: vx-xGTMnC|Jg 3FxIYm]d'w;AKFF@w& ]j#Ik 0Y%I`A+ Fa|23|BǢ|bVNۼ^ W/Z[D"zf:e_Qz^k޵gzRX{Ém~xF_A 6"Սpn2 {~'0-߀]^]p+K8&V.|ςN] ^+a|pۮ}Fy `T?G hSMVBs0p{Sg~{_pUs} $? OFlDowҪ^fW: OOq 8M!O~uP#0To_[wt 5 G/y JOyf~lPV0Q4a )w+CJZ rf|N{{r{!&o-Xү$MBm4_?yLebM8~?BSy:0^*zpAUX`>{@c,&oŊmhmMDe?1 s|Lp@&JN谉A#oJJ}1I51\[U'5=4WdH,#|]FoNDfnQ|jGU%EMcGti\GiMŹ-=!F`EcsGx@( Dhu ]j1‹/ 8=_& mU}C*qK&~O x%k;դSGb""G2;|]<4HPt7#va ԘM\mAS yOԑׯrCk*R&W5 qg Uΐ F\t)MOBm}޸d2,)s[/2KHRa2=Gn`'U=jmZ/\A {Ah+=w|ςj!NAP@sx_AGksI ֯VssEZ{/T'cI- d\xj|ץ\y+]PU4'/~\eW-auQwyG_ZZ!'Z,׿x 2cVϒS!@FVKxK)п^M -*ʵhޭ( } ¾߉uZ G| AE'k|CA m$%XPɽZlU0GI-AaIwzM\pmo {/prmvt 'Dx=Nh$Cmˍ";ypE5jTL_p;[|& 6lY $ޯ'Qw|+ n7S$i)O "IJ858sU~'BAø PT{I֋>30Wҿ vi* rW<:Q5O!Yx+ OwD(õFnby<8M}L 4rЇ\9Y嗛=ٺ^;7cM\!M0p'ȷѣYU@x*[-/E~g(S+ ͭlw]; ;:$ĶNJky#mp -ox!Ǫn ^85v.h~6u0s} b`z`þה=g 5%H`Oc|@IQ']Ѐ/[I#g o)=ΟFv:Ylܨ3ALХ^h#P烽rarfl~_yF׈P@@KwM"%F$6)㲭bkˏp$𧂀*8Q +o!/H*x7+A)?? ^I?{A~`x<XDʫ7ԾFm}=tԌʥLa _]]6뽌@p"QcGB(g{0_$?rI>gݼMg 7?вHD .na2VljִnT7O^,bA 땛h; =s iR :fa;߸1 )>/W -Lx>@'78C{.e N*axg ~Ho$`SV^ oE)[ = taN`g;P$ĉOLL 4cPh8:)V vڜ¸А˅M cj\ p6rVm$0 x vw.}Nj2wD!c"]'`G#:\Քq]|rD oo^QT;;?*mlTS^XG&17OnְL#Dh䓯ƌW@^ 9$ѻzkAGzh>#zM< EuO\{$tS+Ow?k@q;Lؽ˞S=%]FZEF xdž[HԷ b!Bgu[R.i^AɷN:ջOBnmE 1V)p  J9.rG;2(W$:4ۺ{`JZn *hZVV.MÐQ2/{aT!k~M}Z+|FB *dp qx Byz|$rޒ`4/Ǝt DnoF0MC9!ųmg /nA%vM!;5Mm ILmu 2c9߹"5%\* o8ZQsTtOW.+u1lfٙTNm)Z:>TYЅҿ?¹v_(J}%~\3ƴDpO}{8\a}FXwο }ߴ\v?1nӴ,}#p f^:=4Ɠy||Qֹ;޸"ҪeHn{_s+ lw.(I :~~ vNO䧾Dw U!_t¸`?oƗ3’o>(`MӡܙCV˭,"_yfuӭ :zWkҖm"wLԣ~k à׎/K˛[ /Xxڠ!~^xg@J.ek2u&X>O¢_VBuͪ6_Tnx.d'n}WdIrjS՛PQw}b^I a\9>M&&c3 8g 6нAAҭg[3Rt{ nR3Vza MUt6z=EMt$_uսx#+5 W}3o}yUW@@/ r9:b߄Q)]. -0ZZ{pM] OWwߘruY 槭jʂ n* gDD(mBfe+`'έw,wliW榕}Ոޮx)Hv~|D<[W nGtȵ ?SMտET'm6)2}<l -a\#btfB_FO'`fq>JKꦴ7>^~@25ϱꟻJn¤BH!ӌW `;v?OKOZo9w.Mum.Y![ 𑸀$֩.ּ#G\q]-Pg2V?}7\S^j[wM ?A உy̜q OWrv'B>IsuGդ>uޝbB/m~IxX^ Utvm'Uޜ?\y/ꚸ"xѧxܟ: T͟8g }=4K6 /!ҿ|LԣϚݷ4+I'qx49_x 7U1u}@ۂHBHlV_uB.tsuQj/7Xb7W1(Fp;`5נO|%,i`CA;8&Icr_?I燉v mׄ&w~W /e@%5S~UH񄮸ZY_/¸NտݿPN9 :U +mzku60"Ҿ4Ywo16Rҗg -:I<'m[k^+\~rsz<ZXW- _Xb5 $+3K@Q_r}KǾQE3U&LJg z@\t^ŮKV8.[Vw;>2U>VW셴|!upGUW&Y$M5ZHa"V= m^;38Hgp&SNN/mlk5 /Z+§&Po]I#R8Zɻo7O  L1"kߗ+(wY .= jJ3k#0h, /Vuɽᜡ7n ^;= Z[vno Vo>yٍa$6]>nK{v9Xim?gY+q a~~_'Tkۅp"li8w năl9T&]ȣ[{9"^t[$^\I:EvүA/ x7yeE#[q>4W=^4rA-n]@CҺ y3wvþ`-;¸V¸,ܕg2.9!^[r 5%[C6hB/H]zy5ե^uxa uƝA(a? w;h*jV A: ]fra`|'>/Hԏ(V6 0 p,Z;{Q\ޞS0 n֎*]>ҡ;FG>m1 >JT O"ZvLG-3[7%\fc/&Obm[1{'x͖!|ܵyA%Q)EҗV*BV~XPt$Zf,3:?s95`H#tn mEۿBA am-- W Mݍ6v<^ sQaa/rԑE*/ c Yh?<OAu ww/`[b,]`KQt=l7m4Uq hm41Z=ϔ_ר@jjo^kߚOA3)ׅY5 LpZc5E-%2դ2Sz1,&0ѻ0c?ETcݸ JSէSłjn|4H#xh7c`SGjNy(t [>0r%R ={ma}DX'=4pompd:€?p[68"v7+ڼX]IXfD :M->s௢4PT~﵎u麦[߰J#m]"-qZH˖6K.n6r͎[b4_vJS_ wqV;&`x=!N hm&;6 w@!\H@֥}/M0Lccho~qAIA}߸}q˝jX⢧O ?zaPXDom/bPM.xQ ,ޯƂGݜvV@~׉R`,Wk|PKǢh{b|·`i@k]0OZ X 0{5;:1+=h(vMda#ϘBr\ v7S\,MߎMq`$60oޝk3֩4Ҋx/~ԒG!mg^ [G#mfG#5sEor?H\UrbrO'@v=ZxZ~8_!{uP>aDjj̓ebtUm!" 3r||-.Ӡ7WsV&Ss)<(qԽ[C6`BBrҸt:].y/sG}¤jPFiA[d2~Ly]>ah7$45[˕pCz Ow˞JjfQA ƐkIh6KKw R"r(Xߦ.`U8𰱑z# Nu5jS46b%C!#wkJ=&V&mPtV &o9(ׇ`Z~쎒YY;lϗ >xih0 bN^R1YZp"˚m^zujh=:nBߑe^#c v<IhЋƇ&8#i>ODa'V: i-h q4FHC*'lhffw1BL(l9q671 f <9Ƅ8?ϗS S{KYJ*% T춈kVv)(}ōX鹚Q<-UÃXqK)As-#lm+mDivQ]X.=;UhP R{'yڽK-u컜ZqڪKw(לa"pq {I7rCo(]Dtwk$Mmb?'9Z55J=g&Kh(+6[όmBdAJk6fnAŏovl4>9wB(@Savgr OR6]h2t ϗ֠JUZBgnl*DObKT5k.גrTbos#ڼ7u_]˙I>|> 9+׵ #k P꽎$}]5ԣߥD4zc }zyg?'7G_LONҋ{~EƖ#ƚ#0&M{7Cayt aؼD#'aU״ -} A2̟'<$ E;_ߕ# SV,CStRȟwaPONzc$ /U{=ۮ(߅ K3W 4˅J -Y=y.\-ayrxHH#yqfݍ+$cn!!8L2(T!vDZN&&g%d1էaSN;C =;p6GAKobߔ@vxDrWv4lx"h7*Vp!-4_.1)rrg4ͅc<{)7hyH͝_t^" :fQ~`7^&/4 Ő^@jd Toi|ު׵3@T}t Fco-X\i=\ Z\ ^OrepUW 2\-ϚM՞+}f͒maE#rjS|(Я˲tRaEBf.v2K@D 0OϷW(8X^]?ڛ޹+sx3#h9*.=7RXDߝ(bm޴4(nm"-Mzۜanz!F3K`~ v {\ aRoo%j^9 jd̩1f 섫lɚ9ɖxvskG%u6K}=Wݼφq@N̊ _h3/L6!ohGm4Ү@I:~ TiKiJs]&uOc'cEǣxXCGj荜n;w{@mO)` + >;fJJ=Lc"'xa7lSO꽮Z\퓸GmD)ڐ-x/A# {SS{ %OֿUCA yr|Iͽ$ݪa|!K/^3ܽUijNXϫ)Vу /[oCZ>M Kݓjzǩ/'w|cHzRUmuf1^ Ru+2 7^uoCo+;(?;Fo\$B%שּׂbj$RH/}ccZ몶1^׮z;^'?;-]Z#|W7j*GxF{;xKw"u3bp7Beb_Aq~0$99wwg 7;v{SOP"{JYhl+Ӯ y\3|׎^W %Zr}:`_ ]n4n$A]q"ju ]҆Ӷ+f ܫ gwxNl2#λ|wu]Ʈ/XA[k5OసW k\E+iK ㋽z VV / D“ֶ ÷woUV+1˜5t;xGiGyo/ |ׯK>OGI'Im6]¹$@/WJ[ rk '1y;ǥe;4_E7c>|x7~SߔQodk]>$J gOg~Ǜ6b=*-5Sc{;UǾNs hĉN0"'?U HU4gsɗy,*i'ߣM߼x'7AŪFVJpcwpKmdJV~>i=XM/2f~4NKB[U|}n[k>^.ߓjX+Z$:L'0pؕ$EM⯵Ud\>(X+~O)U-sƴY(C{޹%j 9.QӾ7ripCU|Wh4HY;\ǻn`iЏc6^B䡙Hu<Km3FE V^5pƙ<~&m7K_50rS %䷺W旪⦖# wE͗2nLRc}mNYK🛻OTraD>'HG%8:}N3,̜! B ܴ;F}a;)ѴngZl\%_w:3b(AUwZA>_oo?\(մx1_wqo о/E.ڿ$KNݧ= /onjvoN~erߨ xkb~c(tz(\޾|NU?|zj e=۵q\ac/;QuKX#ybBSMm3ukAkz ZqO{`_^}>KTN iT.?,.@O/}T_/ ޡ'̾MX=S>޸#v n}ִ7 +_^/T5?DwޣcҗYƂj{cK {{˭+`3䭿$u՛kwZɧTE_53 NM_\=g EvpErߊ@sw|rl+:6ӛvxh w 詯^{akW)j˖lV^iW(l3"5lW]4e9u2}ݽCx;̳DRJAᜠ;- S_ӷI y~>]> +azN+Ku I} o Ni2gB_'t\ݵw{:4ݷ^۾rod$|&ͽ7/Dlh~e.f(C?Օ9bry;8>^lEI]䄪J%M?W_Lc_tӅeb}Xw4KWrF~ up|{ri7c}Fs]uGjzYSt_{|h'th?Fv9㻖*d^6sEIG\ZLw7ws_ muuon Ku o>vȧmC'*y?[to >e ;hNoo,sޯ>=[h/4[0[ei"{t͗P۲ !ec9H>)fkcLH$+%:XXp-@ޓnl̗!:34 쾎'IۃK\iVispOɚK$"]A'\߅bո&6|O  H#*^Jˀ\=ir(׸"{7 sfOI1dTsbt `EhJߠL.90U{8 w*7djqA<$=5dpqKM`PPP.6M{e=RKm=ߵVj@i[fN7x@Zl}]{{* d~⾻ixX+4q ۔1OS%0h~ʽH/J 'oZ BIm&}&MP(5j^wli'*\@_l~ssScksJv}?oLiU\:j^%=MV) zwVZy}{jm"c N"9Y =@W-+ښOg,WݵA?[kr%}K CϴSw.Gт>?@/-j1E"i~n:W[m1χZ-.FZP0YJ|EKs\W%+VfpIG3c`:J%і/("q̔3׶g1ejx;[jOA7Sc!46:{MhX8&4,/.2* .#ˇgov. v83EOp~R\:rf}kմ %޴& XO/C ]4x]ҋӛ\J0.0h"͋_ UM(Ɋդx@ %u0vv }xL¥!+N , ǣhszq`N$#VB$ߌ:p\tѧBmf(&"qDtI;[5#L"3 * ֲW#:M"rpC$J3,NJ"E!.& ޙT{s5~:+ͯpPm'O^_v WWCs'x+E$bkCM;'uK`..͛}rx54kة G;3/)2>{rA䗟)._FJLE\ #R nJY Z1׭*"QP1Te5ɨ]l}ǻ@T+ кht߄:}@u/SgNpLPh&Z&_O'X闒+v nx>wz5LE 33eEcu#o4Wn~{X֯W_LjӗWs^uׄV*jQgRuW/Z|!~$߄k+;xFaLЛxf ƾ rsx(` A;8 V׸")bCqۜ<7g\+w2 ft?[N̲/?ηnoW :~Cy8Rd+?jyX$`{ߒ>D{Yߑ^z._Wmq`]6,T0PM5"t0OBJg~SJ$ϖ5t OL+׼*];|LU$+inc MϜ46+% xP"Xn'>_ kβ!&U:F1ӅwT tuEb" $R;RY78-2ToZjw PNa 7/0@Q)\B9Cyn+9B 7]b۰ycu]ɗ X-z&A0K39{u6h-juI-H4ZF}0Nc: !&_-2+&|4'Ǟp.|`B]xJi8pUˑ}[^|m; FhĘ%n|AO܂nJwFmN]=`ח ge8p$^p.˷rXM{0?u珽ݍ+NfabB (c\/ hUy{o/r wW 6.e8+ϟށf KI=*yO"j?4@{]zf׌ufp-+ϞxV1Z}A]a!`C4iʓmc0N|,wɫC#icD5Ϟ8"dtG-/v0_I㱞# `nf O-C4z(.7e6&.1܂D]ͩoww+ 2gxV9DFZ \@6Tw0)6} O{ ܢ49YC|,muOy>-uWv",j#L+<].u\y-݆c ?|hF|NBXP ;c՝f`f2;Q91zf &et}-!ZƬGePWE0!y?A|ߜ6ㅂW]\u'UQ$Gokw|,H,I2{V '^P`ġK0svNäkS1A$r͗>9WǾva@{-Fd?NrVKn1.8)J.jp'7Vpm.o A$Zwl%o)5PMK3hYжB{.X")16իʛ0GiY124&=XMP\2 czc'QQIgL [5>o.X{p%5YgXx=QTtv[[62crϏchw!)XÓhp"ci<p5ҲIn'͛!pzQL(!;pD^d=̽Fz@tP'mPpb9Utw;E$ W E2Pnᐠf6& #Y2$<j_Fi]ߌ0iOoO8&%1j& 9ތפ߾edC^vv":g J;/pEzc1سZr)J`%zjqI"3CUW=W -j\fyFv6^H״ZL /•wUߴ'7{M@?r+:OW%W u侽|]eEb{ޠ]^|J;|WK_M ]n_sw^9X4^_~Nu _;^YF?^x[G{K;xI,iPn.  \? j A@mdatA>;)gS(X^j C bmIG"3}⇒\Իڪhۗ}zk"2^,ῂ9KރDϟKƊ}ixULNRk˺iɘݧY;5=2xP>pϜ(KA%sFfA  "G㕾$ 2_OjN;g#ZS3m g: qȷWD^ LϏ{8X:wh-m޹\M˞9{dx"}o=q[M?Jh,U_?KupIpeA/= 66߼7lUM]h%^@h_swW!+.\"LB̯;nxK{n#EZ8Łk-׸&w*" lupliOw{#Բ?a /K/vu@4*C]Y2;qSdP< oϔ,5uܹ&:_ovǂ*G{(.'$ +Rth]#0Uʦʯ]ӿӹ !_H7`ұ$guknT^s <.AI4_^40tQgut27|`RvU>x@$ɶF7syrҡ@o՟3 )u t%cJ{&i4Nco )UTbtЙ3B\VZ M3F\|V7hs(>k@VdR8X /yD]]7p@ UkN̎ h*/鿆HNǂ[c9łNXSih/MtɍU>4O)͋ *\߄D͍1_Y2nϚEaAcƙbJAjryɚ|J}n ~7*(ggă~ vQsIzp1,&j {d@"ֽY}2 wK_mF]ORS9o6e?#$j3tJ ~n밢 '79'J<="F١޺#N\kC>$4Ah<0#z|+n؝O@}M)E͆z%2Ӆt0͉/y .~[z]"D.j.|RsdupjXnM&So|UKS%YNI_ e=lTϜo/hYsavR7{|0+vx%i sP0]BX+~U+ Pjju$'~XWO%´1xd8wߛ浖3tXٗV~JI*>wwN'oiv`xD 5ݚ ]ެ 0дj$i%,Kzr~wb<3|nRnUFקI@P&}X,|‘u˜q .F`HϯA| ANGD_6V<˷h7$5X| $IZl3O*_X@!veJ7'>sH]WςM[5ÆVg`ae{qI}]G|F8Mro_=WXMTU]S/t?X}_֔[34욆ի௅p:.8[{+{!.~׀1 _(韦,o>7¸~Ta/JqO~{5:ְ':[V2Ge^]<'V:[K4WnRwJ`\pr$!qTkBFƎ%+%խaOF@[W >rJ0!J1`o,WVIw9Btl="Uֹis`ָ&:/2NIjL{ T]a_]R܋X"V+LfqyB7jSGlCe^LQME`͹;pNM;DŽn?!{p[dɟ/msmwOudw ] \.C콽y?7mW\ 3RMmkB΋ըgigu˪~OkND^nZpC$kp\uk ;X||Pr#v|t4 zBL%1 :,jua Z2 줒]Yf͋=d6nFZtK۵wi50IcV¸'!|x&VX+=6`R%"|ev>uL}CVG} I73g3se q#^ʈ׻a]L\B /%ӻjc;o/kS&&n]/oN~> Cyz IU /Co`b mɝ§ª@" uKzxwک|']$ >5Z)z {tYJk[PK|,5Um~j Dp>D\]ע6^А&-}[UTjJ>_ WyS"$Wɝ]/¸eN]moaRQ[;}z]l0vʒY=Չ3QM ګ}JĈ@ʧa0_%Zp ) W|~~]kr'"KeZ"^&-z>Y]WZQ.SzZN]e֫}Z[ruz[Sc􂉗S W^PyzWt'Q ^y:'פWUפ^#%iA9.CizTc ^ߴk%Z0Ow||O o׻WQmEE_O=U^EV8$8F/Io4Hlgਦ,-+ ]}l֭\DWvmX/'B} `KoְHs}($nDE.z!5zń%I) oSA6 `WN[%5zKk}zO6U*[ł+/yeZ<lٳ5C[NIdw( KN[U0nKPhӡvP['Mui)pcǮܢ8e1\ӽe`!? ]V I { ?stOrnS\LJ[ &E~Ӻ }=?y[󢮛¸Bѳtl|uWi - ߷EO._p}4a=c}Mh~KOEMtSwuzoifNshX|y-9s"}ࢭHbR\l&3;ȭiőas43q_7/(T [~o}M|M+⴮_WVp&F-]l-U{ Xs'с$ vprynP֣pEj W2t.|wƇp&зخAJ`([o 2NS5Rɉ7U'Mͩ?fbWFT(un\ fgQ HE߅oݲ$ OgTJ~0 T _6Mk}ڀOHnl_w>h\#glz{[uЉ.fF#NvYxFҫ{¸d G$$T}p2(PUnmrɟUIUUbuKGj_[51/RϿĪla[l Rã~E߾ݧ@6w=XOϗD@"3, JUk`L&o,jNKu]& VQ-`7$ \j/*Gr/ICj~7]bHWZ/ZR־/,pßMPK^^g#,GZ:C1%C:uuWZW-PA>6n.kZX@ ( [w&]ӥ8 @\K>.0"nIq! MZ}%H nzG>krީ4 xA{N\eh痞{ToUxr56̗uҾe ޽p\V N7~cP/!|]^W’0#M @9%W}EWwN +pd%4W8>^GZzu"6==,&5m>+:' i{|MOYr|M:+͵C_ AHWg<%!zi{ G|˓}JlbXmk̑ӷt_K *n@8A}to0$һ '&{ :a\< 7.#O}p~?&+s\//og=a[B^_R~0|~к>ᐗ&+TˎULj~n>%I˛*-]<4V¸~K%˟?+?>":/ݾ&vݝ{u\e!~_xKmϫto;jG*h+>ho3fݍB0si HB|3s 7d&A w{qC {j@-Z%+sQ|^ ,&OºJ}U8 1^c!v^P_p_!8߸W?fv1mq^BH"ܪgF$"sV 7s1f(yߧ9߂ ߘVOޝ}bm_%~LܠoB¸hh/b3¸3wʒrŗMڽ/a|$ %;\K~N=)w:.ti^ |S 'j¨?n<> J:| {~<]Lp#_~l+3T;lnXu,{ʸz^b;K0Q1k*}57_Ex}ao .g< ԑDS3n4u#cl̵I)n.$;dBO2,XŸM<|z"sXa_E"wd%vPOUUEu] UGu"3u+FaWIפ{ k^[~Yz(żOW*5֡|^:#נ.Hu蘚u| A',-AN.]:W8sv4& {sռ]6{,In4,nX療)w7]o|]-]|Son@{ E3V]Vε(0Ew@3߂r.3F/l)Sm/8il?IDfn^"+}oi}kMSf.f8p]z[})jn?tȬ([Uz0"iMU]߁2.]TU~"󎶓ժXd>>?MܬxYvWdc0f#{t+ uM6k(9,HFmNaJG5dM/&.h.ym8sZ :Ս-|]R{Tkܹ 'D96[ #vZ-Vp"F6h-=4a q@c=!. E72>R"qpFz'm`,A˔QwWwwEN+q>&Ǟ^7{<}Z3I2$x`F}BGսi6sŴV:DbЛf7(h2"3j}b,IA\:<B5tB} o) Znq>{O|8d4+VB~x-uLn8P7AOנOas\ %g|\>iw&L,fmQ-[a" l蠄, & +L/o2Kk TV`bM\c"'t_ӕm9Pq[DuExW%m&͕Lف<߸$dDKNf>wRjP~ZmjPO&1\jwmhhݩ`}rjq{ @ݫއ ܴ 5)<-[aBzg{_o'޴² O ?nJ(%8E{E3ܽ8o{3Ц Mف%7On*q[;[&OU/>r;{%Q/hVF<>+7S]w^7z}n=MHIAv^yח!W/;fL7ڿ:op2}պ|\w~֞ܐI[V1AAWd\$k u[6in$&K[₠{~ :/'#1xpq]ܬxl1k˻pr>@K_oS:f9q'Q YXv=rĦ=sRk|ۚi.0iul3 7do}6&e(Wblޝ`p "^xjE>ya'hE$8sr}&w\pC+ ii[w6A:lstu/v.c@wquw~{Y"f+CS2TWW pp_WxW A)xz˜4H[5FB 5m4;sn5' P ǮGV6"m(C&;g[oj=·|3?lI Ĩd4i^Rb¥4(E~,4hEߡi4^:QpE~ Mr˳F.XmZ#h5'fJJ3W>VvzZBfJ#JA8(Ҭ\T)r>+^_=d>4ϋ)pPOLn]|ýY>)0z=S׭9p3~9]yMUOX)%|͔A)۝8k40:_tGxa S m]+V I۱.(Fz W@c 9otmoR `%+ 3[K 8m+&Pzf!g̒'.xt xSVr`da6w9+Mjhh>uMJ3D_ L6!0لqX|-4uu׮B8鋾oyy?XkIBpט+rkM@yKeLWWZ7]^!-W{QhZ_םjj@1}r?6]}C%uuz;䃐\b:rTvw?/]_`o rbx< ||JZhX%滵yaI!kGQ[jL>1L9}^bz/'VS~/RRByAxDc" /pc01}œWZT}j*'P"Z^->n+Nh7z}e :Ծ7Ex_/z A7VZp IW8qϘ%!N55Ϳy8n%H9&+\?W-ÅH/O hMQXX 8ڷwW]t& /}y\Fa*ݼ B5}Ă?6 ;A|"Bf ?UVh5G{{k_L[o=G7^;pJmfOn U/o+AC+PB^uNOն o֞l~:^Zz~\5 Ep$ǯzݨUuOS,; |V/5[:t;G#'RSE]CjioIWrDH ۹z $=sQToM]r~qvH6/M=2 MkvqOj;FA~&N(=krv@M&ֶ+^7$|MiCꢁܹv~ y/Wkz{IZxWy~nׯÀz[m_aBMlVrKL57$m?ޟ{Yf\ HI&&8"tO_ ;"aG[_r<%Kcs<=d s=/n8Foi]EgyINu7^ZTO 1pBWi^ҷʃj#c[~y 8Yhհz<ժX{L&hsJ+T_ZSORgJnv[:o0$u$5E۩4QsLo A-v/ :?ћ$<|щ͂i!+? uz!rb^+} 7~`g!y[(|AIU}_ɾ-w5m(4C]&NWóxpm4l-ɈPwΩsrm ϏuSCrIW>isK?yzެrV:ԳsscPKj x_h,֡,fw]P?@A>:7MlikKr߅|+^gV-&47guvI ~ %|2'X |z'2W \qcrmw&{=]k k#|XEX o\k"[U|Ph{/ -ΰk߃׆8)/ m[q5&Qyͭu$Ļm5KyWxB}a/k4#0E?p]v\ƛnY t]t eKjl׻,eֺ.3ߥ_$qmUcd*3?/H[f[V+rkNK ϟ"ZI蝢 ~]kO>ާ9 >7~o6V%烐˧_8\=[ :}aF|33eV\9xHJ i?R96~*M=Ek?f¸L5im(vt _h^ =m2l)a`(}# 4%2C}W鋗9)Vr]9>x-¸YF{3 SK%[yKiZ4kk?L~S)Է-k8}O_e^˷|ҷwg(o`]̤_$+G)o͈3i%I&&~μտ˖;T>o řS׺tSTNM9)9G/?`=!\0$Om}BAlsPD fUCkVabEg]ְj-ay/{o~ +'墷'FK.G m:+~^Xh&૩"hOu/Tl~'n_5վ ]npƺZkr^`:٭O?ٱ߳ƛKoS|iͨ;Kg*%URip֕u⹃5 |et8N:eԹ-i?\АQ'$N# NmI0$o(2bL@>{͘43MɗHюbkrm c(+OɟFu ƄSf_ ^MnlOn͟_6P'q8#µ֫Crk͇E+":@':Ԙo7w [E3 A# _cdnndZD,. : ,'jZ`)ksg z5@mNnsZ{ \_o_{W1Ovwܚdcn.H \ <Q 4^|_q^!cͥ} 5pn\4; x}=tops[9"Zh`Hk;4{^.<4 Td^8p}HZ,AMIu[:ʓ:n D?O4Mŋ5kgTb {E V9 PIwJ8& 0Usō7igIm'{/E_繧aUnߜ8n*+协 `|vlM[iW3V{9n\lZl"CcqIl%Qˎw7gT{marr,DH#@m`W?cڣ׸l,3 ^^ +-3{25{%c-:CBiɰbtTL;anwΓ^0Β^Ǜ|KNJS}xQVҞMK!"I&'?NjiZ᲋z6P+V `-w¸@o߂ꝍt-:PD^6n5ȯނ^nmo#-{ Q x#:8ĢZaC$m[Fh`ʡu?0Mj;F 7o|x@όC<AKᆭȧsQ{fxz4dlkE"SFa7Jj,A8 ZVa.˚ - k{G~/ecI;Fh$6Tm'8T'<57?\yFA6I6:|FblN|]^m?m]y"ֵf Oh\|w]jc.\NЋi\/'&fo ]WD:$xG`OƄBN0oM݅Ab.MRQfiwwM}f'J-1/R8^ƳKAO͋SKH*xE}E(#}]5IcnKݮâ"ڭEGUhj<>4*x\F`qV]:Ow(X%Y-[[.;; Y118~lYլ{1z%;dX *1Bz&ao'4h#{ջ8" ]&m>!vK<΂t^wNj.0>کw= Gzg\qAE6h Y}f#- oBOii7)oImJ=/?M}0“_Oq[ҡt$^~-2)tup"j3xg6c}-.\&Zu7Z7kPG{RM't˅Ogd<nykN3yvcS*qDX`^Hs]/ܴ+>5%?,sBTpI{8JT8Hj;Gf?[w`# dXI9wϧ׿Z\sX=Ru"" MsPˈϫ=pC{ZFpc-ybfA;^-k;wx'!ea=*Ow}zk rhnq/"yF+־?.WXQk\?Ƀp&N־M 溼Fvm ~o- Q'uL ߀wHCCz0֢PUؒkj#T ^A݃ dA o0-ڇې&vq>8P+~ ͵ :']6"|$"*...m| JRRg܇gގu'O꽩Gz'ޛ9yv\Y_.r=x@? StZnPJOVhHmU f}л{r;X@|Wsx k,SoL߱ǢL׿-[)f8&|"c]ή0.?*5iv}X ̻hYHp9dbfK/-8g+7~ *Wi#]+ߚp Jj`-o8#ӓO$R'х :ې8FVϱ3Ed˘-&dwtڻes*RVxOrC.fɗgwU/fH1](E5F<6mׁ @|͗V߸4 +$1JJѾRӯnf-w FvwMz4f׆q&*t2їeb`whC4 A'HҘWdzk\$ 6$z$~9 4͍Ɔ!KbxFokE5;kJ[^ mfi:f&U~Lgzpv:]yLK4t9ZtY=n^8ga[ό&.[ ֘zz-x]|EZiwC Aw jӻ *&g¸~ ҕ$hF}-p.]H %&~HzY_핎;y Z{.TSƲ(ie͙6j/ QVO(͔?J[~iy_hnjv4gmnTV |.PSEn\( &(l.~,i`ئ 42_8|8wZ/'O8}&0Jf_pw%t/a,m#֤^Y2 |VZgXEZ;>͵bE6 *0 A_wXV̊V(0E[U>=;΋zO%~(,ltneɷ`ܴO8Rgxَ$ 2m65(|/ۨ&bA$Z1= ?L2oĒӸZWl+CͭӷLL8g@)`[*s)>CfCP3qme-/p9 4ևc +sy~Z 4_Nh& ڜ=.=RIxbnpTڿHFzӌ# oiW/!C}si@ .tY. #ܰQ n_;NCYU>&%{a{}3Pi}ӫ[a2D}MЯ--^EU CS>U*d}p [}aC7Bco|'Wd+'H`3W m%c'FA2kCsB,&r/_{>O-1m8[p68w!n[-FKǻ~77V@d 'u>,*NaLha~/.hjSEr _iLfW{О 9in(H"ņ !Mҷ~;+^kds1 vRk:,eeEI։cn(1#ޙpn u#H&=6̉ǰ뻟h.CӾǔ@y<G>}C /a6|a{ٳ&]RVM߅EKTLvK+:XҖUرd9o/3m U[wst>/iA8KzɺZ cEtPWw!\b%PteӣzSȏAaACF:CA2Wa;{m%,SE5<5wPZeWp)"Hm„j29\J -Ɍ'+4e~7HO/-FS2{>,V&C}nCi.aOܓr֫n50Qސ㇜+6^N\Q̬{6馥2U싚f;MxIZϞn~"C9$n:x8mJ^˓o{ԔݼW<mr 1_V#ú• +XϏ_i`yp_-:a:4+ᐯkqpftI[U*L-@ vLO*}g?p2'^?;UrpIԆ5{cz^ɸ{|MsKq~4+oV _ZʞB { ߁ބ(z#*yBX5XC,Y94wU 2mrUzj ]_1wcq[V/@7%]z~T+[yYoGxWߛ'$%<|~ ?ǹ?Z%T,F"A<( oI* V:oxW .y(1Β6N{W(n?J^!b$)>A-%f,?ğk\ZXxU^GuiV&ismݯuO){zumB mI\j8t>| G-8/κ$;^%)5>G/cy){WJ徾■j%oo bsQn 4O/ A_s!xֽ2#&{k2RWxhO-h#~g & ww/\>'I"ͽ`%O{ۼ:n^C'MgX#Tˡ\k7`D}A6B/꟦IZ{Z[i78qBWJӆkk-V63@vv\;rbm=.o^6ʭ `l\0U A%ص0Xv#IvO-WOSBnR~޾M >SUsSж+8}_#to7{쓌qO ̸ ͛9+^P^JU";|'wkw0i-.\G;#)$i!_pJZ$^^q{nZ?]5-buoX3'c|T x(k.v[k~-'K۽i$(Ot{c6ݞ暊ޞ]w헻Wk3z.bWU%k  X<.ɟ J9s_˭.8׿n6oo+ۊե_zĥ=r[¸/$3+h"쥭 ( /s͏ut,x@‹fZL4΅ 0G8uY%^\4'Iq蔙_+|&J[zX6I(q5q :w=cbz` "M\(n2po|?9~_'6|֚fIs\^-ptCdn V>$J~ o3n._ZF\s2Rq^x=w5'"4n5s&7qU65zeϞHV,UvYsj`#蛅\FR 7D{zw?DC&SFy;X"]BWTkniMKujpA%uXG{׳]zB%11k嫖[ kS1fN$d )!kAmV<}/YB?$F4ܸJ;^܅G%. XnlVx r]V@ׄ#zW`A>|iA7.on`-䬬|Ԟ_I>}m3\g4sEW# b_ӎ] Yu~nWY伾%V=!}yaG(?_W't3D%Ȗ>Tϟv›*&i pv}hy3 y6pOոNAD%Cj?ZI5wcj&"ZuU~oQ{nixluzhޫÛ? T > wY\nxOگtGm-/5pE_ب#'`vѽY;xOZɷԱ^T8gfGUmwC?ݠEFUJDm(aoҶz=F3~Eىz20X]@knȆt#WuQ-$i' ;˼G360!勽f݇G?f]W]_@}b5}ErjWGWtn|+*cEܿ\Mڙ pMRxE~V^;#Wofw{Zr]3O~ƂJom>@Y>?i1XWSy0[ε5-wy1 B?;w2[V,|w7!Cz3P^W- fA fn;Qݴ)ΐ~}sǟW G͕,4;18mݾtL{'fbx9貘. xN=%6yª-7~#U!/_DуixtgKu)w3|ggI2 ;1No|@ۀfF>ɽYƋOGe1 ';8a.jA vܹo~@N N N_up$-D3rtY.{yC[J]7 PE._nAmx02eG;6șlx(wv;ڭO"\xHjF06misyuɗ/7>u\pE|֥Mxp, *u_]*x9} G4359MUvd3G>t$!vt nr>O3}:jn*co|)N.9wwoͽԡSI8W(AѠ/𘰕VnD ZGH5JV= [d?]>p%o[FD4h!pD;v zUq*GO_v v߯,G=h ˗ѓ&42'{T~fҹW{}n,WƺHi¹T&Ff>N[Ĺѣy~ aw \EӦ>8jhe?A%r;{&ᲂ+vOdU;?LLa'78!}2])a8"9?ʓ/8n<3M~+nV.ӭQo4\p'e6bƷp3xrfX}_0A.M߹mW&Bn"4pwmHi-=|Uֻ\t|Оx-iqxD~:y۪ km2j}l0w#QM4O$Y@. n `kq'w GBB)@i9Op4* YG}p ۻ\}h- x7s4ed5 j%on rIX82`]!UP&ùnCy`|L)>@SMGqj6~d'Y8twj>0&sD|xg L]>K `4i~nt NLoxP^ӣfK¥ t4>e^3>jݺinÃOwsr'TR&\r胔%Zu~$>eK59/o}+(İQN27BXh^o ';; !k~iv8 :vr+l&R %>w0iKJ")!SkHXVmur6|/au_ww'1!Y}gU?E$m[`,: D{<0J4k&q|΃#4J>6OC&n{ȹϪw._{QD0W?75:ǭ~$bLœm8)B/z1_ ʿvUmMJx(R2`Y'j$:Ї*Ff#.\wўc{fg 1ɜ^;CV>{7꒢~J'oۦ #O2mӼی/[șQh;oi̸h~| ]^8 {+ xS_F Z خ`J/ kS/l4`EI&fwxkTu^:{@xhK:()n^ZAI|}վDE JWf~Ol;[9n{P9ڥ^.ƚUMbgWmE|}|B#׿_)w_`ƣ}3䭉x>V"6 ":?2x_;V檪oxPB P^ - A /FHNsE'_Fl܈:#~M)+p M|=R{c~UTXԀ;nj֒Owq{9 6#;;I(Xn?bSQ{tov .tZ a6oDӪrAW ^ŶpF-݅Z2`B;/kip!6^3Uo *of0!Kl~ ׹ l4HM:3$ag$}i[ ]GW}={CĉζNiT.a/uctv͘ yx¸&lUwcDcoh- Em,>#JfaQnf?bثlxP\ˈ4}E v c}wtI?][kI7'a C8 : |Vݰ cY9 "(8\@l%\4rme;Wtῂ+6\KA2jJw(6 &MM"|wpW$44 !] uct 2Z`d nމ Mx=޹;-N88 ;W%lt+>}"C WN=3)>ͦO 'gKx<ʈ,RNGݯ0)H[{8mr+վ[$䈾󶞐;εx hR,׾Myoڷ(^[Hd韛l-iRVT`{)hTnƋ]k0E{+$a˟pQ?}Omw{37 7nLK/ ݼ#{"gl<߸`,m p:$+ V Va?57{v 8-mNr~ MucIKjK6tސ+P\~|nSK;8g9?uy%ǥ}'4rэ}; ^3^T˓0Ё2ks&nG Нn#>Vo{E<.@Lw2ET_Aw 쏙"e!/  NS;]T\Jf+JW] w2MzA*$4kUB@lY)Qn|i˞lo(ҿ*6qcSgnH%Ÿcs)9Ӿn'Nm7Al,2x7Ujl2{n;&\eXi Soqa;:xgiXmDNBKg'(Mq"$䛦Y.^'\%F.yN'Z7 pNE/#iY])6eUpu'S%FWs$C,T'^} )$֪A?WB OBȹIRHXqD^|O+}k61 ſ߼+M_Qp~]eϰ|]uƎ8ƏW cYݾZMX^s^^l74?hqIptGRPpdVF=gy{ٛňtBVdq8j<{;,$m? j%|zX`\}^Ǯ=)]|$[աݛ?tеUǂߐI_|]PWbt a=^w{R+|Bw*x wtpAzhѳLB:D+ >FԪcXg :djz^&jmE^H8 \!6QLZ\Aߛq8T^x5{˫AkhK.]Op=6ԎYÀ&z Zs!// F׿w4fo +:sͻnW{&ϗ0> mt{x3ev/8eʑ,`}zl9eA}}xdZ+utm`Y|ooBg (gڞ9W^7h3q(daw54:iMQ+.꽓K xr?hܲcj:F:p̋$%{ )Ok6 䝹ץ_' aSQP"{K9WdqnhGq|{B]IL-'^*ZD,z&;)k|\D/rO}^z?\("3w3^+F׸ xwp[B|-7Ÿ;mA7 jyhh r|!ݷu~kM%4[x]ͻ =Uxya8EI.PS^B+D!_ݗ{.8X_>Z\y4S𻜏mέ]( M|9텁t4>;+u 3GI͙K"ݻ2_Zw.j7)0#OC'&*y /BN ?;kc1 hsIj߷\#nӶ+?~WowFmo쑟6N^"8ٕ%߆ 4/:_vur_lma\v+oohZ=9JH0_':٘ඕikWl?O,y'%%-|X ;KD{+ZC]/qP[w%1:NiRNդd޷/bCnELV+_y.'o{DNդqv.x^0뗫y;8}֫Ȯ?|'1|ۿ֗TOEk=8N*Fּ=E&zFm 2[:}¸EoւLmN(OXW${Eo7fa/Vo'u DN%I=3"YB5.+?i|U4m\EHY"a\`=W?\z]:I|%{YXDŽDy’H.)ZjɦK1*6əJѯv~ofߓV *n~1˚4!kz$nk_#&O '9aޛk, x|׵S}UG }}e/&^(RM?1a5%ۡvOһ\-/,5.߼ykL5YMOwIsW}G~qwzjxEnO.allGw_XV0!V[mpFڍw\D~8\;m; :7ki:#͋im>PEs^𻐮SY+L֫%IHb!Clvm;]-;B5m6q1NׯXw2hQ2xįgKe9 ='{2I׫fj60qd /׉kf4[n'% nDy~,~'w֧=u^焢+&>"_𿖀 x\ע|/O'Zj^vA7'X!ڕl3?A}r^a#rSZ/ST1n6pJ lIO}\%ֹB%7\+ۢpsšܤD#g}W~ e7J.ur2^{h;vT붋\s0v'{J.h{P$i㌪knh3Jї0k#R^i!}O6Kj_'\ߖ&jYw ϋQZIϻްV<>- \!p {xѷh?-MOII~D;C4:qh+}޶'xI|NWkap Y0_\vv4/ )z¸cF{c+|,}Xo/M$"3H\1ZiW־m[5W{}f^'\{1.[2oOFa)z[?x@5|.5A}}SD~S֝US{i\I9bV 9mA}YkwJIDwty=[D [-sk=*ڮ+$_sX _ Z^%t˟+K{?cJpq"~;_ _On5;s,Ic "n$W _}05?RV_r_v[Lk)s='\W߶rW៷?S/wdXlmO\$VRN.'r1UnXD=Q$VQjtO5xFc)wPw%7},HBۺɝAw" ʑBWM?VD+RB|~+ pEZI-j bZ ;uL!Zps\z' Czנ˯C9 \^(w =Z2A>|.'pcDw&ϋv ;~ N u0&{ kh|hQ{"!{W O}ߥ8|4H8(N\͏w'BS__^Uf'49ܴla07}  73y+ uhOjgP-ͽ&ξ1w8\8p ZHk}!7%v7ռvW1e˿i'yioMOCP]p ^P޼;< MN0GM7rˋM7W3V`%BsDm&sxFNI|>xzcxHPK-< sO]IgA~ o^ M0Gzgo˔=lhJe3xh W$/i]GL֋ӄ,%}[ +"JP۾$%Q}֗]HDXˏpO͑v*(Ϟ[>OnՒ}iYxh7Q0U_~n7m|ܬ6:џm೏R[ jE܎s㧷xg ]eБ)}+ag6i.+h1ԕl+g}y/"Cѷoy]_&ɑAw՚f<> _%CXg Vz۷2>M!-X#~@iv($0L~f|sD/E_ztw `]cկ "h?*,kE.>KhuBCi3~>lI㠊lO|+e+~#C~HJu/KwljwǓ8XOޟ UHkEgc\i͖[ݧ єzZbKL.J-wOcAwNS͆pS8$ON,8UUsT n:B"_|ː8-X?} ))rnhɊ|֐E-{sMnS7ZFNr㍆pDp H&2:}_#??V/>=} cDL׬ZT5}]ʓٹ}Z'6N~L{W-uת׾*N [~ xc.D>ubgC<%}'>a?2^Zf:xW|H"|Йc43sP,->.D/(MBn"3VO'\ K(;e^VQs3}k.;}[GkW( G9n=Ηx}ˤLA%c?!qʼnmD͎I_]~*A,ƒV>0ݭS~/Ŕh`W_ph6f,j;uL~9K ! UΔ6Zd'KMOGQ‘ڑcC#Sܴ5KOܨ@V0ro M5GrfK-Djf.=(蕶kdL %3}"J|8]JW⻋i3SQphlq&mvDDE$բяk 4"Dq8<NjX.abtxú(k6>0V6s͆}HT]oÔڵj 閝 zHq|0>l2Ն͉1m~J^Z(6 頣OIɲKJ\HDc>$;.Y]֣BIcIdStR0PYR& F;N/x5_59DĦ*Teoဦ_{TE2v<(:l Z,UU; ȋwfj¹)׏CE@YvɧFf'ְRtnܟ..&ulof 8GH ^Xm~,8bc,Вdҍlvɖɴ4_|mu@J;_F wpdL{p%PY1Uߝ8 ǖLzͩ;-;^ &A6}A,aM#>Ք,.% rfV6O gi3ݸꦪk:B%5&.-/Nfdž+|.W~FeiH2>O_`!1}n,#vj)\.oXl4HԤ 324؟.0.~r #`vgxٴ$L8*4j_,%?ng<6ٴ\I Tn碟 '$pxg91i r'|$Mlf_ -w#.1.?A]AC){URK8vt4](aX"vܤ@IݽlVo Amg8V5?*j* q7wİG?~(p!~a\ jP$^b,M'v_`Ӗ 0WndhQFaTW3/ˆ4Ay~ZFSҐM_Az@c 8P^}q˸):Z̹Gr#Af oE̥1gq%ۺp$ܬuqr׋ H1#mExK:~e76 i}E %ʡlF>OYrr^M_q4":8UngpB%56`SG.KGe)s%*Yn fc/?ݸ"CO+lBnT)v2#|eOh-q!o[7 ׹׮3t"/I^|h"K)H}_D>tXt&;iuc<gULay8Gb1MRprW ˍMb ɏieÀ!VnܬebكR%rF?ׯ^Kϫ1#V[Ů?>#_ý{Ѹ~5׻UI{['~^'|-c=<^Unuz˜g/>`_j1+^񯐞3'G |gקĄq>wiե__y^x |U޽~_zw>GTk]zN'V`7`OB>umdat@"X-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------/5-Lb(h`ILr)*TTP^5z|!fINQt*_R2w,CKjy񞨴Ui m]P۪i9>Q,W`_kdӂR!!FK"$riTNX߇7R5,3b _^J̪@*&E*`[D&Sc?"ș } f!#RP#>\ 6Tl\V&yk3I7g&mWa'=:VnZOm{B΃z#oѦ= FF(UUU`s d kFNl|Nnʁes6Q_hWY%u1YsSͥK5b@OŞ[e劢[hѬS wILekvU];*ҡsJNitXo${F[5)v<pu6(Us[^x/u?dgCׄ25cV4t]6З+Ruv=EMU]a˶F_GAB!)p0ќ ;e&|`|磷;bNG{QMn!2Y>ṟE1Ŋ1M"PIAc60`ٻ _ >%}'ڒ? +a$ڸ@4ɈΨ 42 K >40 Du*e>\мPb~G+ىw~&El0`e{r5s츪|}?F/ w _yjzCv+3IiSdM^Qll 5-DLnw9TT2_amgtFNdn^̷\7zl6t|JKS\i}I!!WQQ>A:dԿ )8\B}Wj9NHbŒ|3ńUj ҍ@Xϗ*e%).Bu2HuҹtܤZN [Dz&1b,3DU0 4Bre2Ia^oNd1q|o+]O}WWT߳ndgpUOc/3U ]8 5)d;=޸귛IT2E؟jd=ye_+ ;.wY57nMV׿(G/܊E$ը1'5& U(5>y!}2 uCsXA!*`$Y]aϺ@sk쿅l\ uaA?|;Jmo\͐XlK%"LQ=R$پyXO*씳v3ֳ:O  ܆'mֹc6Y#4a!Z; GH4䴈p5)TC.79 Ra lOCyM!Nj=f{[^C_~ BV/-(}I,SgGQkaϤ1gsԪ_uq!@􈽦sژT/,}ەkb^ڃ9l+ϱ~27jMkUGy﷎׍Rp@OF"09.?VL]nTFŤ'Ki|P-¨C9ʪB-Jb eJ;-*ȽTp5*($ joo* )Cx\5BXC} BYlo6 0"ԁ @@uVjЀf e<}&$rjB}^vkoKCC(^K9. lߜ`|YbvAIL.yKk%+E0Ø$bi%NH"cl("e%^Cw㳸!8ħy P"Xly6e k`8]4' )z h5)TC i+3SY.; DY-]~b{ U*|17s;Wc4b\jn"5)j x⪳**H A~tٹk85mHˎ_389O]Ӎ1 c֜GՁvBzxʈDp۝,˧t,OpJt-eλ:Y2zJ{ܤ8R%d>=W1KfDKog>uMϜUCLRY26,R1މW XW*bʇe'jotdn 8\د|8?=5)TScZjB OoFG[zvpZ[˚痞D {` F2|)Œq(E V1?+vNUT9cJGDO}Ս5EZ˘ <:NC۝s1%pcG)snTnWOrR+.(Bbkکła SyK쐑),肠j:'J$3KmNZ aZ^ [=;.5S5)DKB8/.\VVnRB;g;tS1_hOe;w>fI)4Q$0"7Q=I HD ' N^@0 ,֭j _V]I}5=.GNZxfq7f]zr#*MHݰ6^So0`!"p.D]V) kQ=VknP,`ԡ6\*ݶ? MC;Sݡ|Q5*<qqYPB*)RP|t?CuN5{f~xu ~*&,Mi^^[}?L/T@8tPV+ ɘHAx[^ KB f-9:m:'=UYv{}-v-mUH6.w–A#23jV )l _;ļ'Y# Š򑨰j,L#k !T2f cX WT(5)DKPw3e" R*`Xմ:bw|nUã^w ^ wf/p/5N0-B\r ;+ݒz`i^ `\}2xQJ/eվ*OT WZuSdyn~=})mQb/rٲWCfRYٻHp& 'aI)DoHq64AB'*#I@ .:05#ሬAN8nc$85*(Z ]7$Rd֮}k쯟9ys|[|.mَl B d48'=װ,.EQݔazS3=cW%LWk5!o,hزTI.wOT6vvfJ'jwJV ̣9dfsqQl#xЮQP1XZMI$-D:I-u[E|`TD%5-dK1WWisyA!R$Pޮ3$aЈM]!zDعsI, % THIB3,B\9o!%XbY1&~ ۔v( +Ne2%w(J.)&LkVOW&P^+lދeFT,:o$[JbP`iՠpN 5Twh0D5u._u%Ay8tJMN5-D[A81jUUVH)D՚/2좵˙'ZsC>RuҰCC2LbQi%4+~BspG -͒!*xuLnΆg&Fg/I*k2<䘺O~ tƬDՃĉЄ bu{1[|[ SU$ aRvjAI)R1F(F5FԊ$BW9T&<|t6vN6y5)4 UǛ2DŒK7nm=yUԼY̲r!&>+iI 7&bÜNRp`8gbV  rU%p;]́5/bEmiqG/auPҹ~ͪ(/@{,`Xa7P(Lm 6ZMrdb4YξdvuY}ଗ 5E V9MLETiy-R م-Sz<1"~C >mRvpy>FTu0XՐB9m}0x5)TC1S]3%'jV"TJQA@#꘎x؝|g]w41^ZZ,Hxui[ \I"(HFP+i3%|9k֓"fW:fGjNI*dtJ`Z雔;}9d?.%<ٞ;ݺ$y$}- #U $ %lDS@CR=BA%>2yůq 9R{W׵d<1Jw5)DK)8󛺕QJ (PN1E6|or}[Γ;SoZ|9nLe ciRJ =ZfKAuG]i䚆3kjtJt.(<9Jv_ d\zV*GrndYjǖ@wu9.orз[]0w?PeRz8*P;5)B̹Ih&+qr]l56΃9V]Tyb]]k_QYDǿok>{q/{.:"BΏBm?Q=eۮ jL+c=w}udY`u-$&qˆFVH βd},I[,_(q$e0(& ]2(+Ҵ1) |-cHwTuB9n:p!5)dCl7WPJ"Mqm詞9/nߥvǜ {V &Lunۻ!5O0ִS2vZN,Sӳ!ɭ 9"8g3Bؤ'Zy#*jCmeOlymeτtXK 3wIہVXK;bt6 P8#9x3'€`*:+t:?3 ~l%j5*2!z$VT *RE< MT=2*;~g!7NÒ;V5\PM 둪hC=+`;bR< IU׶R.ByJgLy`q.puEJ3C=X~}~Sό 1QCq,n5-tDsQ42+JC쎫iHzK8/c>\NG[nNMeJib]USꄫ^a/DUײ 49=&~ =P![~Sz`g)L:t% Qz="1¨5B'K7])ӕEFg15)TC) 㬭Mʫ.dC5D(wYSv?շy^Kl@V(A. j%JfTy`J 2,n5Fq;d=jFn?ׯ_bfF"CHqyHuЪKu\D6Uˈ#ֳqÜr8 3@JL42Im(ewA-vC0  q'҄j?|x\ :?ٚ~j1HC&)+DP5)t[ ƕ &UX aSÑx?m<ˊ'vTӿH]h,kØZᴘKcIV{2I uJ{֎:guX:*{~OJqN&Gǀ6,Op$u̒z6sU- :@]SZ[Cpܿi N+,?fT hs5* DSԺԺܹiHS%*QP|\w|es3>#g;i9WRk ÅP{IE0V#V'>)m@RGNG(/% 0 0_}dv1$gK\iַ`iZ'&o5o;iY>4ݿ4w҅ehZFk5{G1G_Yyޞ/nBU t hb_h5)D[%⭮+vL.F\coRGovOh5r:Nl]g\F+qOE8n HSƅdpD'@v8Ƹ,xKWߑ|hR/M&pVU,j;Dby4nyZ+JB e!!Pg P(sui2sGBXGhLt@Yym憀@6FC wM>teN5*:![b$(jJм(sί^O/=?~{Z2eX6-#d 'η+g ҜOҽ%= FBX8x &zb1BiF ^䛀sL$>~5)DcAhUE("CK:EC9{yAlW2zǛ!vS',tܺ1RFbi f) ,  ' w>}1l]z (uVr~p1Ŭ*Rꪵm@U[3Of( @gQܹ,豆x(7j U4?wZdd~Ӿ;-{kA9Mx~5)TC! !qyļWdܨ*lc|yWgihG >+1PPZR@zN,ZWnB"eҕ_>dJMy* b T]JRݽ\l )&H*X-!`|ץ/h)189xdd,.6[MY?8`o zRp'5ߨN#5)RW\%H |&JLLVuE @M6` 7mְyf zt'JBQS)@ϟ|.ى3E"4#)݉;pwS@O}'U;.bⶩ5h1*eE"84@mU98Zt`=q7+J5),^ij PTwV0??މk'ɁXq5k^S'eRhm$ԻKnȾ?Rɢl~8Dj"& Y`Z®+hBݪ- UP6a&U8<̚.5Uf&JYqu˜CƩB@tW(d π&f86v2|mym)xF^c5)tK{q2`Rv4K Ghb_;.Q^ɫqT[tL$G8׼4焛vȊO/2|*4ST*MibR&b@ l3e BM`Qia]X[0~{>oxbؙY5)+% ㌫\iTj ;v.'fW=O;0W鞏{ j^U7U3'̷4.ا+bMU;\>7/92V[oHHϗ\z>W<Ԙax+=hf3\N.d1 [J L֭ Zj^%F+RT{JBszK 콑p2KpT)#ӥ0A``fH;5-p = EkS̐B*))CvrA4ebɞ.];jAK aG1 9FvK\kk0XTŢc%€rc;a[:T\ `g  {kΦǶ'ZiylK2g0C\fJ!D9 T2K{L 7xJ=9 _c\:\+kWSw(ֳw`NB4pa=U\b5)/\;U@TPt IcuOs;>q|6b7y;c.bLBzXNYY|seJrM# -R}UQ^*IruF 5a}BYLR\6)%Sf:!WN"4'z캠 2` /{7lHtDShfC+d+ XISkʚ#Rp `u8;"5L5)M tV5;V`z/:|a> r6ҭdP9MpȮ5,>vLEj#55a &2*o |M+RQB i45*Q5E:@pɔElcPev eM",X{Bjg3C44iΦ|u66ayS ֔ K6,с`2aHk[h;*Fe[쨺IZ-nWJw5)48yqwDBD\0}ާW '0I4)Z;.V%r&ٰN\ҩwVYv2q:I陫pȳVOx)J5ZAwIi!'yJyh `xU*|"u+ՇpM sG 0-TX.ÚU-it:b9KGJG0"$jy)'Y7g\;PPHH紱#>a5)D;Tk<^`*:mW1{kT?3/u6oZ['Tm "Z!e) a[ɼm+8H(VoW\J0! 7t]I -E$0ut{斳6=WH;Ju[xkb`L:0p{LQfߚ) 3u\LCIi3N_Y&䆂G CwYo$++d\-z+}-V~5)T0UsUUAaeދxSOg+d.^ǍCH%sRIM.\ PiszNpeKGZh R'FxB+pI/ T:§0gw M*KAQ)j')*~J1]{6as[t-#\,APE ? MEыj4JuyN1O=>?p$bSXP>kkP5)D0֪JRTBR(c ~T CX#^뽃77?33װ\1ܜ351!e{ϼ{|\I(y2JGk3w ܳ1WLrwSX U##KT-]!D3I$pt_m8whH>an&,a.LH=OnN5*lT#%"k7Ȅ 0y=K2[F{Op{#ջtzW%`yj;8Cyl1D]+qPDU猹Oo*a:2 hͤZcsKc֤+I͂9ެk]=Sʵ3)oL+pj3o] k(4"PҤ 0vť ؃fDnyj~ .#W5)D0調ܡ@]W՘lMٽ4l޸=%q.xkӔ*tMp4Ol+qպzy'1y 80,ob r02pu sVt^ pbFQgeqI ƀAm]Z-<٧Lix|KsFN[@ *6D;[, 5* $q%HP% #vdp辍,ײU33Nƞ:Ƙ"@mU{T;ب;lZŇKlfk`8ROlD#7BذwCDLa <\o]ZFwő-9ز1CqsN-f2@J $*I]{xKM9MEs,,$ׂ_`"k\J,Yf(@B@p5)t*uFn%X !W:Gmt&I\';GD-26&jׇL^O'zoՔ{*6k*Ĭ#d3 JjsK5uR9\)U+jnil]mڅ[$z({mbE/'VI`l%)pX;ɏ#p5Ϊr1XU*\ `7Q1ܫz N?RirV5* Az9O )PJCՠ-|Wo̰Lff~Ws ; ㊻KN5YBq*k 9=5WjVRp6ZXF\)4UƢi5I"Uo՟Bޚ)pDT4Z9TRIsxpDoa.eؒ~5)P_2 D JM79Gu*]9ʰ c&%/Fk_uI];Ѕqd۷2u/UHaH$ Z5,T֬%I#$YXВ^Ǣv-f' ƅ>&>f=R}~O}Gp7~U3VzX I=w?ӔCCo^;~>U؟Y} Go5)D#ur72躺 fn0\'{_nvУ9&#=7ʉƞ[9o:ql{jNjfJSngzJ lvޮ=uavA,XA`WXS4t`jS0q*.bՖ"Ķ ss^b/^u0U}u>(7h+{w5C2y6XPc@BgMOEX[W5)Tg{ŷSx @)zowvc'/{a~J AW'5w-3[ Z%fg).Cd4sPei 956n[b0 XĂ^kAS&Zpt*½ R2TP9UMvwqux@]wx śZ b9oPI 5-tF회*IUbRj,b,;чZ~sG.7 Lo9xe+Rԗ]MbϷ!&(uD9nd^9Q=QyR[nHoёj)SP"#Y+&|(sJjUjASTQu3 cq[SPDHmm`cGT5XBM": ڥ8!DT䓓J,0SJ2E둣"0PclgD 5 5)uj(PhR6g3~#t0M>/й*ӧBurCU*&R]]/֬oIidZ'q<34$7e zRc ;p4b)1P*|-&ZFURV'C":h#j!maq o)n4W}%v5(oC/=x;΂"ocgL/UU~*Zw5)DB].U/%PN >+@S`C_7[I2D+\8c0cT6*cګ~-icpZP!jꈓ;r1iLzRYm𵭹”:`ne! j-J/FpRWLƻTd] (AW{Fq|| q"r}ZsMk~I5/9ha_E-45)$jIg% RZIWBϖ;z}&ǀ+,Qm1?^j+ cXXm{Wyfl'9_)RBedy*Zim7dً+ r@ZCӾ59M)4⒫։V$JM^jֵ =.>WNCQN{zWcI[{wYZCUTW<#֞wN(0Ee\ʫrJ󻂅]ZS؃]+f͓K] qR`P'#4\얕Tr,H+Eiy$|&nW6Yղu%E;@׬m j),,E,#T:yDLHBnwuq{`Gkqcʧ-ݕ@d?jn5* T YwYuAE"Ǒ}‡IZa{s-c~wVت 3ݏb. :MA&"L霸 l^Ѳ&Dti')Xk,[%B9G{ѕQV4CdQX!h+30Awk=<Ԓ6J(73KDOp@]=%,`9:DF >W' '>@ {t9͑y&1H"85)̈́#zWSե@(wY-k)gboh!ty*M"7dkoW*xUm\sM4-,^VUA>Kh^SBB 8Y"oI5)Ͳ ޑ2dDR%aѾє?yJ޼{;~q[\䍅_H^ot$̶-EMS<蝬L%<9ݡhFq6We()"A֤F4PF)"!+]I =Md ּrX_r §kyQNJQT*Rl"yqxzVg $GX`i,',NYą0 85)̀ u*I %YELW;%{!jߠ+-@:U!@6G|s>su >{5)T0JɓK! }([jo4pz NSL9onVGB #.ܢ/^aZ& e!^MWVOĩ쬙`5HIٞ L'fwjrBUPn Wt%z[lSQa\ VW!cL4CNMP BJ`TC1&f]՛o˶~i_|n*T܇?5*l  q7*u{u**P3r|uփ|ԩYftVry$PS1ZmuSl dmg#X=,,v<F JfNL%aKAWޕV#|XK]vDw\W,`)S\~rRƍ)B0;A8zQXh`{+v:J?!GJ^s¯5) /ljVy$ART wI]vE|7pNrowᢜ]3O^.֕p Y+55i (Ħ@ǫ$q%P0xTEhsKW1tJ1QGb5Jdf(YakK[wEr,OI..bsXeb൚$^U2[nsA$'JU$஑ ?];%s`~5)͠ %VvjQSM}u mCѹK<^Nþë"$^l߳rc%3h\v4lg1N RQ`< S9p 'MK._@+D #A[S-`SRGkuZKAb[22 s7Q*dG[rK₣9t5vcbQnJa{ 2ĪWUU -HiqS3rr ЀfK.X:05*Mq q޸U `ϾG'upzViyϟ~cDItsSi` 2b!:1xzXRMP:_rLh/U<l3=75ew5W\ńQi j08NUxO3viGk&DM)OEc%ipURJR!sN:ɩooE8g;mP^id; 7*P^a7Nރ5)T;8V$Y)*RUAH\sٹ9/mw5wrDDJ묯Cv3bKlҼ\:oR*_90WJQm'semL^) 4TsY^-h)p r FΪK +vfxThyARֺxJC! A(]ʂfNTJc4"A\l :7tS@T&rhʇ>U&W1xAAGϞ4~5)T 댼2r B:jtgr8ȵzƧl}/RweAnG;S]cvۢƔ{_/\Shq]D'*rJ rXE kpzXlSje󲉎f ]6/ d3Ta(d2p9=؊a5] >q ͭgFZ&,vs,aJFIFwT9Kz'VW=Dx֑<~lȗCzʶ+wQkp5* 4;\R *P0v[q SZ _}rͮNċY7Ȧ><$Q62R-{l9frY sDVYYڭ*\3F7R^GUu,s2ʕʦ*.+,vq%F#kJEjIP+ki\ kDˣAIkiS>:.#y|%_ֿܛZo_ҎQ$l!-փd&=N5)V}\3`Q )l< [j]U;׉ٟs;91`@4@ֻ%4we-S؇*HdD04Gw򡉞03Kjmyeqa FZu:t)Σ8g-qGH:2觰iIIBeViЊq^r\gs* 顈;Fu$G5) /o Ǒe> ~Otص~)6~sC=RFgnn.>WiܸcFTVDٌ5]fBLiֽ=1YEuV:*i@j!Uw eŎ@TL7r.N& u^TMsMDh2: (UR5Xp]S-'ȗYNV0*G2}(L jj$aK1rƕom8@mm;5)G A]8w2EDH)ݹ|;HcY5ʷ{6Uyn@{|feϴivYZ%\Ja9TP1o- M՟k HK A8mW!@VgEb:BD)3,&/ d0X5F}%9jg}2RJ!$LndR!hD8^Z]@!l^.j l LD q\Eve4s<\GfN$RV5)4B8og$!"JL 6*oX{뼣bFI{?DEP#ٸ\ςTIdE &N%cl˄ CI,T1٥býhdA i@O"Ȋ5֢ AyRd_[b%aA9RQ4+(T$"bJNۗoO3 <޷ݣo[vèetyBpgFMqY 3V2"AE1w5) 5+zNEMtvt3}G /^Y'4K(Ҥv4i$^70S]&*'T37zZ)v{[JEg%Sr_|p3E.ln;%O9y,d),JeeEc3cwX$J5pe,Aڈ@akhB+:?Z;N Dw@Y'Ep5)̈́#SWLtrTREDm,_%8-? gcܴA͟uӯ+Vv``%Q8< e)Md$kLJirOWpX#5܄S$ta$+Ӕʐ H d{hJn$e )NԓO8=H4CrgEp%t;$AH-d6V1I019 `5)T3]:j JC'RZBtZ C2nVZy`Cd PU4mWXWe,4cm5S#}%ؙ!0+^Иwa\[hptE==c5(]2V\L$U:-Ҕ9H2}=tju7&,h,ZT&oM AMJLi~mq!ðvjYn5)DCRoyJP,흃w:WYmcu]eW˾ovkEv) L =Gug*ʬC:3wj%kY$|&Ġ+P!IӬ"'okI-zX)¨M&FTYlT` is[F&ۂ|% HMt먫z9 JCm((Gk/ n"jl%ѡ2L)„n5)D;箯Y2;eH)CHa}O:b=h-s7-u++Q4L例( *髱ʛy֔$CqpJQ᳤FIa=qE4+EEv`@5S5sCHEaVt?`w*8LU{-5)\ons>wYB-^F)ZVǂda{,*0k]ט=q,ά_j K]g9/[j"$ esD^iNbR^+0rk /Mi^ҷBVT-!5 S R8:ZVT)R+b4;3WB1<3 DXb@C`*X55,t!.pۋTPLOHgzO ò,qngR^Tjc۾wl^&J%-_5.7ȐfmDepj#d%jOBuPr͸j.skE^WYEEJ㔗+)W+WG] y"!BG jJPJ+Yme_`HD) -Q>kZ&un{M(W8\lhgCǣDY΃ڬ5)͔|p4J$BTW_kV+6 }=lT25+j);)I+s╎8:ʜi**&IK @ׅVWV"UKޠ\yhLSul, Brޙ@/G9b=Q⳨0YY#+!#Ҙ:2v)xp7G)Vz PbwOk9B]6j"):5)D+z79%Y*%9 n}Q ֧zf>rF2\snWus~ksjqTf uSCȦZ1ĨF+SKL TYXFD.U„aH}z$).9@jZ*tA*hfnUNO!SlYSCX!lu[=oL鴁^ G x2]W B<:TV=I5) \J_R*TDdwS˭`9o3zjGwo:g8LkܶW 8rgJyZߘrU9}[#5L, MTnʚ/-D| 'Sް3KjԘꂺ;p _8dA,OL$Ɲ]K}7R]9^3ɾYA,m_NF/K 5ιðOq#}~ ނGp5)( /VW̬!)@9Ip nze{>ѽ&vV!W ǹ3۝I=SjhLyFMUjtsI jyӖ[*U0 :O4$4ew@/WupalY]]@)S$Fn (1NYSִhw2L3Eu .3]Le @/8zcξt[s8V7>J^e~ŤhAAw~5)8 kֹSĭ"" @`=MwjVe93Խ5c8ֽnSۅ*lj% ;ڊA޵ E_jLQ3{+,:)$%rI衾4;+Lg_*s3 sg]P`B.k: IK<"1)\h|JTuw Ш"aփb6.{ &}_}PJl:QW)R5-t(v֛#u%DR)j 8B[=EOG&|RЦGBZφ7aq,-Fk^^[Rf89̣d…+&cD\NBI 66,qa $ڒ/6UVVCf(D /J0זϱ3KIe' X*|pyWDj^,v/q d$WYKV+:xah85* 40L%At.xEf.۷gmIFd7F"׊*)r,ONkg'űy1 ,g[;TZ+5LIh̸dfr3 ](4JM]^jTH\VxI :pМm.Ak'u5l+K',+IKȞ|ZL -רe5z~4)4FRQlw G-ĴN5*-4WƳ+\TB 7om'qEx\Gux 5DCfܩD"q@VO(d4DeK^C鲘 XJ-~yEVAD18:4!3mR]RYD++^ÖdqX(n5IwHP CV4¢Er2(Rp;nQ?RtAþGd\RCo05)p ܻVVU@O&vn?WyvKh&].i;p;eS|TK^5:qNچ8SƦs!YfDŽ$X/4\b 5>3$%"[N|%x z n%sJՖoxDpz$/[/2UaF'XRazlJ5T.6fIry >J@0zߌeXp5-d;S#u9Z y_%y:AsN-3onUs7-,ZhD>WuoUL:H t;%'e!-m@.5T$*{NCn4ԉ#֓!;+Ko\LmpRɆK cMR:UGej{RV:YRL[sLXVV(D9#Fud?f/Ѱ]sLR϶zE)s\Xy̹(eR!e)5)#KW9u) BP.p|ˡv^N<^q]];+CzhI CR.Zm1aːٖoڗ +/?o/gxi8VL:p!SeQv2̶(=#!a -pG]nq[:l}<NarW@*\P%U0uʺa0QW0%rO~X̢4[}Ⱦl05#|e|JG$f]:4W4N5)Vz}y/2@I})թ{۹=ލ.n=wLqQ(IAo4qX6M:%I"Ԫ56$kW {qL9- )XpZ&تmARJΖZ(IS3j:eM*s9aΆU[ ,CGzH.Rc!Ynacu0Va;(M%vCZuqG5d'5)d izUU :j5'KA~HwQ읯-9& E}bOʫnya^ ,ᕜiddN&1Vd 3(zbI.e%q)Q8xb>kXWyCՈܸV}vZ\\Ӷ@vk԰6'਼{#0vzP$|ȆA1N]_VnXs4mj"P5cCDZۛ,J$U3;M4rNVgA#=IYdyMxvαaLgXVq}>-2ѓ򄲚'X5'z&;J{|rMF\'`Rie;T4+zj-IjpvMw9I*JQy7/7%#9&T[B7}D*IlqBZ3Ϫhf|/rZC΂).C aDb5)D;k[ߓ;U'5ڵǏ2'̬==C`Vsi5or3/ 'kᔖ 3f睬95:FD bC{cxB@PXk6wjJJH첔 JKz.#Rm)9B4㫬:VT!Ѐ#Rs/9f@?uqrn@RB>n gIBKy@=ι)}1:^27mY5,tjg7U˕ RP7j)_4rZo\we阺#m_otm4֣T#Dj2ƫ^(v"g)j_5N"AvU%J5:.nQr,-J֞y*1ĩa RQ& Mvb(&5SFBtL(+UK`"(`De8;][A@`^{ZPDWi"'\RE8 = ѣ Wwh$_0I5)"oٜqU APQCj(ܗIZt+'U仜 U<|#ҽdӢJ&ƛA|8Z͜vrUK'IʾA+b/ڍ'j!!05* $0yZ|!*IA,L"KK_s 讵/Puc6aW{hõry?_SR$dg.fY9AB{(XS0\rAZykd*(%"zF jڰ T "$Qe;;-㇣E0M+ O#pд,o9c~g軧n r # ~85)͐ BӪsֱI*B aڻ gl`GNkgn7ʌ@?3-`pk4N@BͱY lC .Fg(c&y.~,o;Q "ʚ@mZ'9bI$M%epTf';!2fURT QU|Ng7HsHJivÁcukzNK*b\qIV;Y\J`3,o<eƐ85)fz D+]+{:Oލqg&fb,ч(Z!ΜaZ٦3˛a8IbC*0aZ%D-Ud&(x$Dl{)yzUS>N,xE5P*jiZM9[b22 ψ+M{$D@Z*Y)2p@sdUJ{﵆]mW]?*-j 5) qZIMJJ:hyϏg,~7z9PyDTVKkKh]!Us4(ʩ8l/v.ѷv-d >-hw1yE)ma=W #eJ^Eu_ Q` f"V󽬠& ujG.eOeJ: z,32=p>?Ctw͟/yS a]7LnL B T5)41|Jԭ]sH *P;*o58V}e}E'fϔOGl N$65Mg|3{HIaf;K $i%g)*)B׃%I)=5QTip;y8MRҹ љAH*vKExCRBfÍFl&k YW-xt1MzA K_e:OZ,/OW/'0Ƣ .+8ɪx К5)H AZqXD RtҹOUUyR%2g<~RLuZ[\yiw/Iܶ_3PkWQ_Oщ]el8y (A )9KBY]cƇΡTrصOI,(ԥPî ;[=s <:! ֚K<Z=L,fJeI&{kPp^/d~qq XxgN5)T0i9εeT! ;l-M'/F|bCJ˼ /@IqHO׃,aD3ԯ pwliZ'{\Z;'r)61@V&־좲"qkHUf(jPRܥy(gNRKDzd%6&AIIR/rg,_peB1}!W4ތl۶0[`o5)VB^f7iPR w7ߍظ-:75hiW[y`ZQ >{llwz(ezJ8bSHl35 #{&qڃpbȐ"Mn|}$nЬ'!=vM?&L*ұk<$J3;/nblr=0tİ]Ac," UZ U.1}%of^v9zO{*5)d w+8rJTR- xWyw0׭~O#btExjjFHf3vi 7^3իKi,SA$l!NL%Om5ѲCEmH %JPSe$sԁ3;-wa>A(Q =%žxR ;m[i9Z]ʂN" .$l9Ǩ}[Dq]ZvXB%#5* T ִ39܂J@*%Z`$kLik\B> ?Y} vs*Wy:,idd+Smbd`y L d.L_k+*0 L#@ZK *˧2HJ默~_ ?}ɮ3=6 z6p5,Ųdq TPPy D\+f3Jjub l1XpR$ŴLB슀{([BZkۀmcQ5KZ΄CjҔY}7+fPYXF[Ff60'a3s ;6DjwΓu[`N {k BiŪm"*wf4ǚӟŊ ~5* $BH)tPV~'a4{ǟn<$ɋ|*Uyшz5]V8I+L\2M삻$sdbc{&s8GvT8%3ZǨ\0g,Y9X@ V&W"^ɄVYYG!0^֨WU+;QzڊMg"k#߽ۆx>tƋtyKm&5|E@< 񟨟ns"F{;5)TC3\FJBUԠ VKRBÛlݏ޻~+rlż6~Owh,̴E6/z wd*=OS^Bcm4Jr^UDW[QxKEDݢĠ#c*r2VV] #;cG$h@ XLgAtpb=rtcBXt؆=peTl0D9Vg45)tq\jy\H$TEw8{NCb+{~*=C2XmA,ʷ255YN['knM͘;݀TVdZ*V$R%7A[䰦^0AyTw#i%΄TI91JY K==" 4V7)΋ AʘtCW(Bqs;T)QMTV`_`+Ζ!2O(:Z~ (oP7E ˦5-6uU2bT 3?c_IS4ۉXq푦=\uIX8J,p=B0d#NuKki-fR +>2wώTDeJ9Dq{tAj4$ujgz1 @ZRkc8 * 6W^:DW&`;Ѐ8<.(9H[lQ;dVKq".ᱺu CA&7d N 5* tUZ[EhDU)=gh;]EG(v5}cyNP=- ZÕ))rajR^7KJ0vGekSR(x!nR9M9x$`UI!)q(ZLZ%se]5Y3 17}2L&4) W-JeV%hĪBĴB)Ȏ#44 Z9!I"VN* CAu`+_ Uy7w5)T;YU\TVq**(}cfQxD߸aO#~ͺK}>%fc)p)/re}>&eoد375!f3P ;3>^fDE@Ftf˜kz&ݫ eDJ98NSRN*_Āwf6ZN])BUSI+PLwу"iIMNB q/D2J{Hs=Uxt^8_Lef&S=8$䷖:⸷0)5-T q%HEH*TR9a~lzx|.a+[)7#6`}0ZF *:6)˹ ?jU,a;Kj59\yqӫlr YY(*^oeqBţu`SʖJ H.pqD޳]AۺUlCzIkh1YS 9P[0>swaQ5)D;SW7{3RH %Kos0w[yM]/6{lhM_ ]V$w ≠ $hQ!-t)H+샤b+R)z y^LaX/zNu◸V7u̴c1e=0!,\i"dl]"JAls&‡)IJNʰQW0B| )\̝D`@H"}K@0)ThhB̭zG$?EIvnv5)T0媕[۔Ј"P{Ҹӹ^)ǩJ}m䂞ٔ \D&-9C Aɘ&)(*9Xi!Ae]#u,_ ׂ(22Z3b3cno\Dp]ҫ\9E\C{jƴa2T%$:U]aSw9Ķ2#GxMNP+ͳ6?OXv3v}r^\7^F `Uf{}qC=5+hwMz]nFJ3"KINM!ƹ\HX*h5d5. OFLҕ:Y0r+=S W@uLE)ӬLU[mHfHzD3%#Gk?ěU.ɬ,yWе1/+V"!vV:R_7x5-t6 j˻eH BThdmDž8+!;Lz:FF%uun&Ir'tDTxQ (²8WS%#iST~[LmL%MC Eİ+wh5wWT%))(5=JA",@Rl:PIT-iU&rj8ڊ ,R*X"Q5$QVtս8@czw2m w5) SYwkJ*eԨP蹗djI-r*I<וleswjJn,|3 ,X7@ŗ.ٙMMT04^6+}*Q7:b7򺄻)X,;/LfR`Z&ZtN@唶B`}6 .pɋ,uA 7K/^@SWh>=UiFC5* p kS+1TFUB~ig=GӳP!KUA2{WWRpQc^ܪg;4qwFZ8.F$w'M#sd4`$gAHڐ\M-U 䯜qlJSȚ.j-,`]⪩bkY5OLL#W{U)F$صIҚeU0/ٗ Rb'5$2ht OI\a!ĥW]b bdIc;Ʀ)EHPELr-*8Hu5)t sk[d%*T7AEvvP"AscV4Vԕgb:z&9+O>2Vb:W;zFqb1v1v By0I/0Ō),z V><|24 T *S}SP P~ekbB37դ /a-ZW UwC+cH%""x8GK_3Ȥ Ͳx5)T0|kw^\ @}j)u}N݃x[]-{5;4ZPy AySWрgO7k7;K9UqdYLSw)8"VYbc-*Z2M2$oLlHL% Ճ3PA89CgiZ,@SD4 }ﭤS'Ha|$sHbMqv7۶>Z$G exϦu2H:U o5)4KSRw3Y`(qZ;vgYײ}v,xblR~E7mEJ3ObLI% w,fw)(fM hx$)WAWdW !wK萅2TgZ`DkaHKZMj@]h 'aGj(l!gvIIl_m@c]mZ\HHy!OnA~KGaw5)T3RV3P]R*MZM[hWێ 0Av/Xzvɢ7]y w*zh șC-lNի9[uEcA \xKIvt[pXZp(d+1;,m@KYZ˄R9, 4TeE~,$ 'uA22݃j)# {@Me!+V[-]0<ǀ|{F FeNUD `>ms$u;5)*/V֪d *|mٝnleOsP>or8sBc?i+H-VyM|k 2U]rG=nll32\e]"U Ң6jhG&*7 g)K+Pd7Uk''QpSԵ,;gPJ;oZ'$zɲ齭9Mn`)D u0 \69!;G?[7ew +S5)d(efVEDJ CWG=ַ_Vy}:C=qf%UnUqdشXOal.5AISR"O=3'd(.6 hHMbJx(8s AS5eiqI - *B8Xu #jA@!PX*zgBtPP *kw$vy^xZ@~8b=Uw(nA[HP5)DCKIuzY D63 ~X>sÞ[rC^'zkj,z!&6uZɣf#tVZÜ <4-C =cC_[$2u _&¤b@u&`K5)̈́#N2ܕ$BTaG;Zp]g~DtֱEr_>ljHgA -M"PSIE2$ "ج(ޫe2ʼnBFsI{MI߾LNnhѲh Gf4K#큹pd{@M5HH@H7ݳ:D^mL T[ĮpHRTYnSYaVqtFd~ l njB~~5)T0|֮o<(%}7ڶ^Ո )o7[הѭ.N!yMkK֮tM!U9jLH%*CgEжMߛKׄUS-$l#>4f۸%H;C G*&D 1#1l3*t*hӲ:nk"qe5MO㞂3{o-XaBēç5˗ s11H+5*MAk^e36]UQ((y͖߹2 _t?wN:":q DAtz ڦ `cVfyhƹh ,!\2n dNtq hDS7-;+"fDpܣ"SP!LNjH kIHy-Nmb21.ہWzHUl=fyvzVe}@JPASM7DNjI]jevWx0ߝJk8UFUHMD=rZEh jj*'zDK45R= 3N7Dͱo U3Y퐈(F0H2 TddI:+AcpAdYibPѣ3=>Wp5)zEu%kX4Vjo=i~4kZ|ܲ]u-UTm& eI oEt_]VhP~8F4|A(į:6H qM;Q'7Y,`=eYU&)unv0jrU3:%4(9ܠY$+0)J1+L#<TYI3Z)bVp $J)yގ fc:>R]y )Esũ<:FAX QǪ8q5-s\UYtj #ْ8eplê9A%ja۳n>z8'1-|^\/"zS4(4fJck5)5ІFCQ^1zFkE._m> ''q]R+(V-!TG`|/=I┺O庛Zũ\o饸{5( UT/'Yg`h,oNjTOU5) ֵs5nU$P){1 .QVñ^l@Z w7c&yz0EUk#(Ta+`t"k2iBeJ 4 ľ8qcw<_oKy:;5) #Qiufg"( z~a=Bo)}7OfQ .K6=Mam']0i{ j,TB]¯Xŷv'rʨ# EAf`ʑ΄sS-r C H0TSd۠E.fAƉQPfR-}6_\|<4޵C[vT~^.]?-}Q~p6Ai 8dY5ь5* @ k;Ҹfw%$T ǖg̮e~~!|?wF\bVXذ L|͋g/e5:9_s;d>Elr䴃 K"\ K`-Cfh)r Yjhf}e|)m xZ5{pb" ].Euv  @1 1 EɚT-+57H5mV3 -0ԵZ,rГ;2~Ű˷)T|? 1Q5)t+S\n]L)" W}ƃQ:eZ~5Gd䛱wSR)jit(ȧ5UK.zۣ)Z0*]+wӞ)[%T KP(b&bQ9P4d I&F9*a`DŽԉx,՜PeWpRN(G|>ZSW+뢭ՉsX2;9T#]Iߐ?m Cjm` sZ e J1~5)P {wdT*p]//{vSo-3_W䯐0M#|j)B2[;ZJ\.m[l7<'|A@ y*ooJ ڴ,hij A-kN\į ҃V,(ҽ͙&)Ҫ=`QC~[Аi Lw[jqJ 񠙜F79%`Bi.zJ +[qzޖL5)DC ;w^"TD J;*k+kOh~ioOVAd˪QOѺ TLd… *%m-[|~Ge͠]U3PS5-tF&vɜZ\!`dS9wΡxNvf BRr˪#mr !6eNc*@NN1cYC fZRAZ+W!͍BE.ьoUiUadaժކmJ _[^o5.3ae3Δ9<:@vJi49MUŗI1!35 вx\"J<-}-Q\<_١?lh5.j2~5)T3\7W)u |gm/iwױ >Lf>8OU|>Yc*e3 UvlYmfge6Z[JEהVeQM#"YR&h-c(3E%+l ;YL$ʉ(8[-$2MD2=LYkŃՙQM8ERdW݊Gw\n >mCZHEsC,ѽ̺)Əj #u+ Ӻ5)4C_:s7 "!<:t7/_u~y.q9ndyF.RGIKIs@16NRZd!(WqbĕYNEV̡g_Y%jR(3ā]3Uֵu rt!^yY9Oȏy㐴NcXW(/ayOx5u>XpnS\/K۶ Y*! \.5)Fouj겤H '`akYxw׿ZR~"ϧG;!6RE4c8 5S\"= t%t`f⪞a֪z/}05,F4-vi-**#4[Z=-Dysq4rwEppayBg 3'󢘷FU'rgqE/}T0>k Ӏ5*Fr!*+y JJo;;w_63=wz.{ #0 Y!2nM U#kPR͔K/uMC4tɪ@jk+ ਭ JH"^)R Klr**5BNm0jVwj낳kd?dqŕb:RЫ->m-@={[S0`orUqMܶ5)D8Vq*o Jb} .iH~Oao_,,zڰv7(*,/0~O|<'[eH%0ܢƢ/*M9j*aX{tXPԜKMhԫb7j]hB 0)ȓZw.^ﹱR(qySUmГX6ݼR5eW*1S/xg:Uo->x cj5-6Ѕ+sLAPQCLn_܊A.<166_7 FK}(eRV_ {zT#wS`idecbgқr֔S{Q"p;N *C鑞z.Fj*oSkЅ« '+ mKYft ^{uZ!:k*b'$@m'jtש&F#XA?5?Ǔi2m[F喜lBjvEw~5)͔WH00:+j>O!_y|j/t-\%,,ݓL-,bF⛎': 6P(O)ZzXj<9tMN*wZy W7f'm&.ŖgSF;Igd5T@ `T+dfi%A=5T]ԩ]w~5-vz!w4uZTj\`HCEӮ;ћ3Vb*䄋P8EĒANɗ[ Hja1a-wtR|GjZsn'd t|>Ǟc~P]zUdG婑D z=^_ ܹ !5GV&5)DCʫ\UY@PjџafφYkfv=gEYs 0ާ0=;Էصtmh+XI2B3*D3؋PROV!iC"weuVR^"S9DIcEqbDyXg&B\ 0ꧏ MV]Wu``9)rOѦf ,w~5*,3!u]S|A)*T3] owGkݼH=-83)<H(ұzI<g1=XW;xCu0DX&=ҪZRkWHPXķ7~m_!_jҴy_w~a߯Ot- F"&+σlWl?orJ<vcK8[290 V֥\+sUZp@$\M+JH5,tƢ7ߙLȲ*(n}*g}ú#`'v*6CvKO8/&s}.y*aJeRLD6KTխaLpR$&1$EuV16>Us/y`-jB.Z"z` uPeJ )0j_hCe8I91HD 0xY~W󞯿]|5f}hL{ =91J'&K.);S4 2,gȖʚZcEEbzOm}2;Ъ_M2J-6L]sY$JƦ0+,}4E˛5pKX0MD~{{,>T}1;`)\?JR1?9jlTwՔCţ W%k[<5)͔uwjP*(g~'g^Wm]Dvl7)*Ɛ-?^[$zTMl2\kd%3E3D&d朴\5NS"`wz5ImHUJA,Y}4:>uIlLM5{c`H5h9JwSHsP4kghQ5a"8 kbET8Id1AQag^31kW.꒙|SEqb5)$K En*P<Ƹe[=;,YOrkn5Sy}a&˶WԊɡb,~=/ xu#>?jrE/@lotiLbƽN5)4KXQC*-YA?:V2.jWr]~myDŽT|xU{:~1rr# $pM!ѵR@UI_+E˵F[%q$E+Ȍ5JLrT#uȭ.TK6GtuЎ s s-,# 6Jj:W;*"8ݺhÕmWS]DcXCjm᧼n% }hz= F<yn5-d3K9ժ"PPhK7xe'狟[wpjNVں µg'ؤ0ò>m59IMsLnĪsώ+(aNs]37dd*8&;%U°.oTJA04e3V\5d 5bPl}SҽB!|eMdZ%YR-CDL*WD{Gj A=a˶P {5O=Aˊ9 n5)D k7Ϟ7NB@*{Rv͸Cgn9^ٰ>ZLj =MDxu”]pueWĕlEdG)*"뱉V mtFf"X +c\rs^JN)U@h;Ph3X@+e.84dI[sC r5wmV: qD'e~`ecj+>N5)̈́#U" S!Cd9" G?~L>+R_4wG +m-7_\`cDd kk$P*b֬EtZ,`fP SLUl@9^tD$0$]Y]0NvEiHr698ݷ$ʶ@vVv6ri f65$ݸInѵJFr.>R[5, a\[/5n" (#m*IUE\%'/m{=IUn%7b^L#Nl [N>ƎEǼ{"}'tW^ր%,l`Ƽ#9B:{!|VI,4U.-t<]ax>&W(ݲy&cϯz}%r$<%YY)$nf#eFgB֭-Mp58BM36Ow5*9u["EJ ]ֹV\eca~.cqze/,kJ u  -;j']ˮ)(yp0҃ j"5/XnK]1 gqY#JM-n"Vh &F#){K?ĒNy-u:K=rgꖻE 'NL_y 5]3nԁ:9ɨd4D(}F5*- UuY}37P%E!Q=៳ܝK1 CI8~#Y2,M<$36CnYm]Inq!2[Al,׭,yNA ̑0XW!Km2a{N2Vuj[J';i(Q8`8\OpBq+$7kFj,W*[xr+C2r3@qk*`֊2L57=JJ▥ 5* )q޵9'e EEE:nNopR|g9D~Χ.圊VzٵLjpAE07c)TYz 5zgKRw{Dƣ$[}pf w36ۢz }VXP+5hBKJ:[@Pw!a=>P$f TSΑNp#KzpFyjI%k)[81+ 6O$TP:Asd&5)T2 U]EEJ 3gMa:|9m\]l2*cS۴8gp$ԝ;q>ӹ%FEF}: @Es ](tk9hjgiR!CEILD )ңt6rl7sa-tvSEzd+h *EBeɧkxY3% ouʳ";54YA-uTr_O +oS̭5)D8Υ%؈B*"'Z?@e};p$ջY'u_9dƱ)Y%.W N j:sIDR 缌(H(Yុv )'tk`& ƪ\%҇gt,HdԘ,Z-ӣI={ GtEJ?q^ls.m;.*^V/8#\5)d(ZUyrH"LȽ}rꆋ=nI{~\DdUWvm*"bjKk`S$f`T P%c1c}8*VqV)U3Rj% ݎQ\щcCtCE5x ) ynid+&bȡ'ʕlmJڦYInvf57L:ZlgCbtg6(rۛH5)DCZqzzҷqP%EE[ȱuowfUs$5yiٯ`۳gsДw_-h#Y^^ ?{'j ('$Pke$'<w{krdj؂O REz9 k aB43-;9IJ $=sMrK9Wuʴ{Qx">4SOAQ=;QTj|hiVFxN2#mgYh{~+wzR.5)Z,<.@a3~gEmvꞈLθ\P r ZgQ)!6Y&lTLS!A\gDezٯj %3y$-h| 0gB$'uz Eryg!҉]+ς2[U%UCbB̞7\ RqBdC@:y) C'|ipEڄBh!ȗ/ !Y߻:X:"^x75)D;]9묮|2*ȀP:LR{wPԴPugZ+v2ȃMhLٍk yo ʖK %~8-eo5YeZ{lRw5@,1+Q*LˈV,Xg6x1Fv ΋{c?J@]Wx5)TByD, QP`pGsWތcy]t]-(eg,um2 %[X+& “ʵƥdqpS JTgSx20g: PA~J$7(j*p`hc‡쯍*7M5fR?b:oko7!eC:=ֱ? 9I_@>{f W]^XHsVQOJQ ;5* d#۩2os.A %%!cuSMރ;o?Y;PRDv@2v 7n{@,CҢ(yܶ@>˾7k=QWt.lCi>;5)4 uA/=7I'f7L]C,Wҫҁ-5)+9oZ$TD AŔyPWtU SKQǡ';e]q[e=:r:?ճ۝zI'V,霿MJą(:82Xcf%іmT!q+ qnZ=jDa;p:ڙk:fBjJЊp9(/yV+,әb BkT+NY:V)7DJwF HgU-ASeu4,HBYh P$1{RUjW;L.EjF1/){WEO|l!{OggSbvbZOpusTagslibopus unknown-fixedOggS@ bvD9ԓXXbq1$!+COL ayRzo0a )*m99ȵdՌ5SօWI8H  JCttOQLkvb<N =ob< xOggS-bvG&vX~@C(h?q|N`s&_A_|k9lk9Νl<cXjY4?0'>~nPBDZ,(F2'l/X%>mK!@OggS@8bvXjm1C,ӹaTStL+B^ZjS`ޭ ղ+3a5Lݫ2vqWj'U_Q~Üt|ϟnF)瓮'eO53cĐf Viһ,BOggSNbv?pXI fogK p$ݒwNTAb}ɰ Wku21bpEc>6D IسB7%/:nuP@.Sy uW Ԯ&OggSZbv G_qX 3 V]._dաV -ހs),ga1#Z ^)hQZl3=ey!~L|94<:  7iQX.ʔQOggS@ebv r?|X 9B*$Qft& 2IѴհ)هR쿷CI q]@~:]Æ\OggS@bv/sX%T`Ox3hc|d&PsX(O쒤"*7;@gC{iP#6gZat׹xG 59bKe|OggS$bv gX:8==JoDsMŃ1z!Rr|r 'Tw33%5IA:)IL}p-O$Qr/i:PlnNҿbNGOggS/bvm>hnX XZ ,me`EDn3G>ߪ9AsjBp1(5l7vBSr(A:s0o {_ZykрOggS;bvq\X '.]Z 7w,?;o{2]#pޠL>\U1ld?'{K&Z+x^@OggS@Fbv X$"4"8|Fɾȣeķ'MMޣQ{o+ꌣ(e$^Ǧ| = /ijN gWR۝B-\p^&+(cUH' voq6eOggSQbv!X&U3TzվVwDo`HiVsDkHm+ BCWP^Kh4Z>||nAvQ!}msƉDypI^#>KqզelCќ[*Q~-1ӟ pg1TOggS\bv //Xޒ~>5&R@0P2Ž.OȖ%͆[HERdy!e51"tW"`U޵ 2ey.jn4];Am^V9i4PbuP4P'`OggShbv!sKXB,$hY镊94%q 9$lu24<Ћ|F ߒ%̂slv/D lV']_˕@}EiIӄSF+}J`5Bф(tB~:+X󞂇/a0OggS@sbv"]mxXo4G͕Pn!5j(Wwe$tw #wd_1AZ5e7t]W C?F]+#D T*,fC*6x.IS-s%reIvV1 xFdYՃ>P9USOggSbv%'xXFmg-z]񌟆+X*W $;OggSbv'Pd;WX e>oMЬb ii"r4=tm󦚰৔nhWsrFR] }2[6#K?e^}w r]OggSbv(QXA{k1# c A0[~Z[2@Ak 2"HQOggSbv)ՅQX+Z@!(оpԋLFREBSbkKEɱCd4 xJ8*lHϤu*r`nFSadzg覔OggS@bv*TX`͏ E)xB;Rء!vqSՍ!el< K)Fg^g1R"m$OggSbv+>q\XaqTxiPdTVJ˼ vp ;GF.8WܸK(5q sKr2qVO ^I綀OggSbv,2Τ7_XۼDǷatNpZys>ۍ{:bҴųl ^hmܩ;9"iA\cұ\1rd !vrOggSbv-f[XͶ 7$JE~Ų@ p%G%6~<K $P;jp_.f'>ؖܚ(q+OggS@bv.-XfX 5X0w*@`GhLEK0Q:xpό#i^9'Io1`[JtI@OggSbv/`$ZkX`L0n[ ~KB(GXRm*"/gm P"+b1@)K@ڐa?.gdEZ,U.`–ٟ/{ mk"OggSbv0ObqaX˼yul2E"e?x7.냲|S l;q$`UIGW:V'jv7+*]{0 c&X?+gt 4| 7x*sQ:A`c1Eƍd!*u{ِwVQOggSbv1n+WXI;\g˗yo.I"`Q.Pt #XHZd l!Ei*&}|ߞyE%bh@71p;ɺD'^\4rnkZc G=0gMwOGC}D1bR@?zB=!c22d9ReQHjsm",7(s,,H+p) ^ٔ-h2[&# KhpOggSjbv85@vkXPb{Dݏ,nkK)f4ZET8s@v\ƤkͰHcoF#~8aЍڔ; a+M S<`(R3.TpOggSvbv9jO[XԬzD9H?/M.)IYQ!ng ii@8U~9.`Nmaٮ05)h297OggS@bv:±,TX gO K4q][ meJ"Aoٹ<97[aH(պPQ?Z0:||Y%ROggSbv;g7X9 / GD㸻3оǴ:<EqBZfs8python-telegram-bot-21.1.1/tests/data/telegram.png000066400000000000000000000312241460724040100220560ustar00rootroot00000000000000PNG  IHDR,,y}usBIT|dbzTXtRaw profile type APP1xUȱ 0 Sxw2BEBA WlzkGUUUϒkif^n (_-LD + HzPs IDATxw|\ՙ7sUw,lm F !@H! )o6wwɾJ $֘@c^PBE3*Syȶ,̽3|?|LF3> s=B!BQD8Hwf)RXTMDKL`B `C> {>&ͻ46h`b"gH` ,Z=Z)'l @sCJ "@ @H ?0zf Hmf@mafPŕYXbm媸oL4Be@C5}1FcD@5ض X «l{ilzic,F <Z=5^0L=-S*<JcE"Km[-`~čzeS]SVY83|<@ede kݥHv G@9bI #2 mk&RO(}x3z.O|H#_rk<2}`oEIK*; WݸvjX ?!di )HW  BK瓁 K0SX00qlb=X| VT?y ,>OdX2pk@l[ c &XS7iۥ ,^st /@фN&3MuvYN+Cݳp4!rضR:E!eLt2n1.M _v]H+͖hlRF9kGZSr*k5PdߴnnO${pį\/vRP>+ ;.+H`AM8_#Ƕ%hj񵍵;`+J \ B[ .ܮ X5*2?Eٳiu.' *6~ILSQIdvX[dK<% I%eHXv)`^jz ZX5 ;g^.;+Xεu]Nu`~`I R'bn#Dƨ@1DlUOϊ^Vv=*^Ls AjaT7uɄêY=C934$D`dž2 ~fܮ'.ѯC!eؑU@)5Q1=Zͻ1 P8zԭ dPQX; 25unדNy3ULJ}GNUmTW]IPC0/bcc]n3^9Xpqe%Đ( 3[kg]xV(yT/-a%1mjpih~XRHX 1<vuC$gd&yLY&P#mY6qʹicE ckg]hT01V2{]2}gw]hL`‘)* Zʣj"7]HD`a~[Y F` N̈a)a%D L.e}ecmn3OVuCDD `4/5W=v9gd3Je!GʀfWd`-uaZka̔-b2 hғw.䠻iZ*Ô"q Ov-C\`ѻTS`dž(>&+Rer`҉]#<3UK剠@ :rXgZX |R^J|ۥ D`Մ#,O #+ݮ@`"MUv G~v-a/Ddʲ!<`X/׭2ma]!a%1P׻YkU}Ai2n%Dn`@P8jp%B ?$*Dnaۂ2ԄwYZ4z9(5Z0s(|݋oeټo%'a%Dd&nXeџ2m d,mY6.a".D N\XSW;Z ,~e%|"ߟ[f%j+wf"Sk"f~u׺VRF̹"V֭va[&J> +!kJWWF[XFdr@󑦺Yog.marRdžL%cUUR>ٛ] 2|54_{d,2A L]>#U\L3(Bȯ3q3(LL\;UE@RI5}Nk}!(16]D(`DgKHeRWKX QAQ*IX5n8ϑc@4D, ?&]L[`]L+!5n.1% Y7KwP)uC1_7˥I]CʐB50l?.!_.DCa퀘.OǵX˚n0T!ИEe?XNULʓA!pض4]P})θKŜ/Af(iX; `Jssq}31I$aؔm3%FC 5Ę5E'F_\$4O8fGR#wrK H`F@eC0 m >'D9!5$,,7qvU)N[r 9]s%M}|Y"C'h?Z|ߖW‹l4aTr|bv jrwwa$ְ>rmգܨ[X,-Ĉٜ<ԌL989X (AMy'Hp8k|u4U`-hX7 eb Ԕء5 Pso{F &60H뀴";+uŢ~lTPsZ׀_ΜUa!Ř(3aagȼCvs ,bl2+-$3oΟUS*K`)<:Yo@~ #%3bS!w 0 (4%+Kqje)&GP=_7cύ3#E.'xJd@fM^,܉Q1KTaU/\tq#Ȉ`>L?IwP5h|%X8vicgãUaSJ~@z>+A1^Ԅθftiĩ>ZQVtH /v0ό# E1FhN^(h 0lKM|bfΞWNCH]}6>aJ^ k֣f}( YZWb4qf:*Kp޼2yh 6v$)1Ckq@p;"2YT41=`Šb,g@}z0EI; Fփ5D?I@(dP]pd?\POTcb(ќ1hEat "@sM_5[G#L'*O K3(V}dsh*'.ola1QNTRNƗg`N+|ؒi`8czV⣳K0ba`oAd՘vn} ,",U!4#~XQUJBj?oa8:&p7l`-g)`F9 RJ TQ畢jr%l?4` \]j{yuCMm2ԧ0Ke+00a.WsaɌ"ɸ%5ㅖ>JjAYF|  JUCkΠiZSᄊbK;-<=C0{,d䝁 T8v/чarvy9H/& Ǣac %4r`HhF/Gpe8")xvsJu5~ZCý倁U 6Fj|%6͸pf0'-Ŭa #z{ kmΐ6W>4WS>D LC "IS8__efA,ɦ'"p46t ,0d)N hF@N[J1w_e]{Q"҇;З ynY$i:FEo.(éRuHխ1tYy=A &?k/X%J8 $ |~f1NW-r^LUZ1k0bUXׇ:UTtR6>* |tz/Ò΄Ɗ֘gOY0ņe`āi]Pzqb.(ÒC =H Y;vi '?A+4$3pʜRUISY׃ɲ23X!5/$ʆi=$|rZU3bj y=-ў_e``P_:@ JX&f3hbpze)N/"ֶ81S %C}mZrc4P*y~v4Rߚ_O)n&FQ.=aD>ib?'Ȳp85x@M+qR9H栍vZ (&XJR/ 'qhΝWS"rZz׌ .f3qf!ưdwG ^+y8zf1JHP,Yy~E3e h,N+Udή(Ƨ;:-<3f38tߗ2"5, fL+|t'W2L.6dB{rCʓH-iju`}/K&`Oy4^"Kq5o31%ԚԸ4X>~*)qB=ۓ8DZXY}_ P"SZ-7uuKzh}7J%tWjaq>z-C.Gf`{’ptK\a1:z'@51GdFW`9D L+S.E =0!xpRZO'!>͸0XeTJh};m Դ/Xh*]ӕen!\B$X1)#&D8+PiOGz0A\ד2XPb@6G@NдWg.!yH$ ʖ>t&2|HEJ!=܋ =D}&%K.x=7w[/pW~e%(և6]pʖ:,-AW@לHvL7 i{]7頻R:@jEX-[UHٙ@Wx>O e}0~VlieUza̠v]*dXe7.EdAR3ԇ{=w ]n"2l^d°G@=€?֎t]Ƞwc705zw'I&ډ<9;m1H AOHe8[ZX(={qc[%4z-mz33[XL؁ Ƕw#!W0A&xP`,CA"L5=֊6tǒU;blK/+=<&5}樝 Pn5.[ Omk:df|zsKvR 9}?o bI>{]6[8Mӹ"EV^ÊyՄ [cьC Eph Umq?mB_<`$Ul-x)&q]cVf;vJ˓mD zY_`1OAБԸ 7濷cKvi_R3ԇRi\y @Ͼm"M$OJ `Ian| '=Ҋ]͝ۥ. O/-,O!"i&~AHu,kŹOnMEO6)<@|"` dRH-]{ǶWDKtf}(غCUZX١(5ps/y3v?Ÿ=ղǛ U [cxfkZ-l<텣$-,Ie}_޿K;!,w( |zXۖpt٨ϋv2[~eCg.t(1<%%W? o]Zxe]d7!fRW4C$?~td=X^?_ى7ʀ)fXߗʙR%#/e;6G&ZwRcEKEҼ$J|!>^!/'%9lTGϖc*= ^Z.s),88yy KZ\b}7&KڻH7ҐĊV!й!96>OlAl&8no쒍HV!Sɰt92!wbVw$qm}r+vYcrgI~SV_VV+t~j܌?ׅz[m0"K&#C}}oƫwʯ\#`O37q<=Xsw;-w[޿bڐ݃a(܉_ :>i ;^it XT$#`A{.jA<k-̶ol7+ÿ|ۥd$na22XҰ=m'[eij{]^T8cZOn {mJmkMz˰`֚Dܲ X/Q,F_=Qb{lo=ۓK^y38[ ,f{%;N<-OS v"{dCGۈӣWa~ -L>\zlʭ1ٙR󯺛jmQgz]Ʊ!/pykk d)2$򗏀5]Vփz`B:R@w"d\ylAhx }v~=`XO^GLJIDAT;4 vZ{Q+:ٶ$^iͯdYuٲ僽wD)kna^jnz{7򥑥<d~mDoɛ!Lm<ߜcYm6G@2< -#ynN"CM_ @_O _j3)xy)8؎.zg$oq|eh1|ltsxعW84)m-!&N$rxIӂOG`VV#[77"<3mS_nCtjgՠz s􅉜2n_ӑO ^׃rz&xT*O6|WJZݖCRs!%h>5ږL,dSFJ iu=1\uaׄFQ%Ϯ+u3K24 ZrgDP"Wާ 0K[ϪبJ9:!F*;reAvBp8ڏ:^Ͷw|~ExiW+sL+#Ä7ܵ4|N)aiS,v֮U;3C1fȘp"){7FXOf]l߁C}X:(Z[W"G k Ycɸ|~Ex#';b3,zNƉcĘkm}p9eNV#<[bؚ2YH)0hڪGzq o OH1w]7%Ox>ܛ#JC* ,NO=1w8tHVb]Oyt<W`E/`ɔBPGۥleVGx zYո=T[u27=2Ќg}(aTo#NƓMf?J0]8a;K I+JK`i-{?@ZYOE{A;N%aޭ+d,0)³;ƲlԇRid4~ ^ښDoJVVabNǰۖJfuɴ+GUaŶ8r3>XZNu22}w/ k9dl%a;]{P*EɤJ(Mk`}P[Ѧw~/E|K ol1t$YN0R&jipj;ePk%]Jkwcmit/p* EX3İx%&yݖkg$j?N2"N%K^CT,RlnL\?c k bEhhau["z|}&\"G.Z[We90TtĬn'e>";uf$6Ä,EVgp,1ls;I&,6ɻd4YX;@9?}vZYwۅ2Idp;ޯ&a'P;v iI)6tZxˆO3x)ڱ[|IAoIQ_hڍm}NZRo"H87ѭ2}5"O?dn)\׌q+Ҳ890>f(-۽L?ض^[Wyz6v\dLf"Ex-_Uw@%m-+ [Zzlp~'1H5,L{p^n0Q^yusugٺрP82}dzme"KGN·<Ͽڇ^jY2жZc]Dzy߬|V;ּGf~N~G6k7N{v;(lڕȵi^ώ z2Z2[rz,W[c5xBATЃ0hڦYwo8  ӁrM$H7")8ww]LHk /,D+k?+r&!z'ٶڀW162M8ugS]33& Bȣ%%XJƺsݮf4B[!\߲s- ,h:O֟%=-꫾v5cCreJPI;򵵕9V@"+w6;<=b(D%7VrEc}Uu.`sJ%<+ۺ3 ȃUWVBKAŠM4ua8yX>OR7ZB ))hG{n.yY\A)3 ËBڇ݉Սsqtʻͧ+E0'&a{7/jzz-/ ޷XS˕͎v9Bd>hjq@WWdBL7'"@1t2Nv=ׁ5 M+Y;k"}HƬ,ƋW]Nٶ~} 0>G[UaH k*nWoIbochhnד-XB_?KB\+냞;+ KƺIl[7. d;B +@[X:nT9m Lضkڦʰk@!22k@SD!j`\c]Tq[Av X f%ѿ;V]WH` zY+_H?l꟮p}c]n5XPyRyLO8 ̐D&2R*~=횼H B ѵEԲwHo{y̻uR:( ٲF&Lh+~[?횼Nkݻ0-0OnvU%s׺]Sˢ JU2wKX*vL}nk$!, b dr̘8"2L5W%* 4##+ضdH!տ&`m]RJ;6OQί||)\l D; 5u.+H`Yu(b|HG1!˩t )R`baM_e 9HDl[T1O2@N2;5{nfV"?+?ZCZ]y(5=A)hz7V,hX7p-/OsЇ;~ >b+޿bZۥ ,T!\HvȲ"ڙmbY?7^^!P8o x(8XkHx@Jh 8k?sB&m )I/ ӏ=ۘDXvLwk6][W]B/HOvd Pal[N&zj{?>CC$<:#/d#Pr(k'md--"QGʀm]80 x٦b\ OEIH h4:tKS}e%Q?FP82A>bO7nf&o ^n}իn(FO+O, oasIA|Xc1O\[@܉#ujҨ t `/ Fb]`\g[(>qxMDϔ ]kgkȊ4ϱzoz$̍v9g+>4Qx o&'G&ρ @tBxV8X4yXgS8` $rWcjIpg`_Vc db{<#8dmѿdX}d{u8Nh^ ` a3zy/=u'ȋ#ENP)H * Q< @^LZčcg -VM+brE@cy.)Cw5ÐjQ|w8-CC^8<T1VMÝ sHYy y[Z$=ai¹ZQ̐jQrbWb֖R~ny bAGAJ+rK3 >tk]CwsA<ߏ|'K'`nuZw?Ȉ|Ny  BRȩ֨/ʴey+NwյdĶ\CU962ܑbul1?j*n᠗c)M !wIL_8vRh @)SԞ9Sr#攩^)T6NpEI~DŽJvÑ>vz_^S$J]n+D뙀;) ,AB*UH_vJ}R?ce+. +^LUR@Bӷ*$2MQ ݾ 1'>#wvMƁ8_)_QO^$IR*dEkI0jo .^cF_cRqH.*t(^rqQ*_oÒѽ{xS$,,#5:|*yXf<%t&854Y5J/\+HMxEc#LVIw ʻWjp[ůw&$rdÌ[-F,k!goaOyrjYԸJkC4ZQ2$Ò|=MZ[bCRJboT8z j1 DBO(S )!m6H -_1@RXw b:D@sB3Ks'%D)FxFi$KD?MuBӵͯ7fi1%D)BGzta4RBy"1Bkp6ѪZD3,&<y ;bN%!n-Ә 0D6ky(Ղ2?fR0!lB[j^x(٪KՁMC|Wz昃A3NCP L+#B/*:_Jtm[68= h>Z t6`[x>۽`Llt£+5G&^tqπx}uB/P"l+/LmPS3xN`? o+yV`WP}~JhE=r16!DUKWAzk0>;|j,7و3Y cw+\k5t:j.B{bPnKV/X3333Nkxp 'vu7CaR)&#"\@bmuY!Ap0Q)t Kpm6q(wSאMFlJ|\`A̍< !B ^$抅ςxU},P`d22=EӞ+1`N{]i%=qQ#FNvc@`,5U _qAa52nbEv̀y'~8Ӧ#*` N INoiylBOrLN@B +T "1tmD͉W1أLDB`:lZ E FJ} -~jNJ%xyQEaZe!50ǃg٦ .ϔQWPDŽz"( Ǣ}!IA(zD(015Â【L^FJ#<p,n1FB5-W9S6FԇQ,fAr@bqĚ01YU/c\EET셾6ˁ_Hkr@Sg!m7.@L]FH|m&,efKBh.]"(~އY*d۫U݂2۪\QX+u GQ2آF@`F]#܏S 0~3ʉc~tA-f<Qk4Բn^FAAtk'HM D-ccR#F]}}}w/J#(khѷK<K+VN*TB:QʅH\&̇A>SD WgT(j2:gm"u+UFdF#?&HwCnocRWrl¢( "6dƉzN 4IWڇEc l/YLgŭ9sJ;j5'#s/YjGZmظm ֺpրsk 68/A4T+%Uo,X/\ Acڤ{?#H*'/.I 7$}IR>UIgIi3|6)骞qI5upbȉVccI +fi1@r*AZIǨ(Y+XX`N;U: rjΝnIA!&uRq'i:HSۇۻ:H3I;u"餁&LWdhaANX*:q,lXPw4>$AjxwHn[Trroε^; hᥓRVy/J>mW{.["rNy9x(.Аm#Yyӆ~aRm@:R8Μ;Qv8N'e65W}F{"vOvNO(@)zu) 6lڲu۶6oZxB-E5C{UvnjNC_[ e*'Pʑ~xBi߇LkaX+ǗKs&);55=~@8p&{BaU$d(^?睉.&ߛIyjf$nJ_L;dO<8z`P0[k}߀.(owZF j= oո.Z6u)WX:j^>ej쒝K jzveQ!w%f? z [/gx&D$f>1e4h&%p;:v3ehinLLYj׶;i6zGcѼ@Gsk7fvKD,lk=M/>]REٞ_f W<@oz;G :Gy5,/td nʹ27 p^LfDYvZ,kAM'vl]NB~sB.gAE)]z[@3Im=qN`son=|F A3nj9Jŧ[a_ˀ9iL}yeT~:qq :%>^iR^,q\~[YmBCTK܃vWrh$ztVYPMcBJ>OxPv8͹ˊՁeBH5ڰ4]颮dв„TA x<#cDoePI8Kāz %AɬV[{Gy/*+gpூa SifbPߋwtmNuUJKr֚wM+AWO)GrfxKr"`xLЍU DWPNآ.xphHݮ8jKPXytJ1yXہ*&U"bW\jY כ@ЂzL͖bէg^ahJ qG6nt.-w.m=B~vŻBǍ/ ars֬iʚ}bBjP&r8xI@a08Q$νK;RuRѻr,a%򷛯bz0b@ ? %'QZk0,d=t0KS[ "B k[[G/xP]kgP_B+Ӹ6 2mMoh+"ԗ=j!.V_C_ޡ KͫAwW &i;3jj9utk=J1gnw%yQ6KR,->;0_|y;tk(svxMQQW]UT jKjԼ.R_+DVJZ+3 8:u6m$<2nv' Gf5kc\Q @9Dc4,ihljjڴ-[o߱c={/]]?g`y6>xOG]y$mb?њDY9m\մ-`biNrq DPGb tDMX_B+Q:aMmm~O>ޏfIB X߷𱪿%eOwyp0O4@G D{O梶)ϑ#޺#e{FkJKaѤ 8g 0De3K6ΡyS6MPh=ȏ1 m,v?Kp%U?̟:/l@It*nb˿zkO>adZhJo3+gC}|ђ%.w{~WRCqI4"ǽejj21;w*.<ѿkɬAs"!=PaY.7e6qTGO:|JY5 hq;NE[d̈Rs_nRA\8$}`XV Dtr#B $T8))^.Cкs4Pde4C̕\! NLHOмTiZ= _hkUw{?uWLyYaf+9\3TulsP#s9p=f.9g,̑\$/Ի:nk$*eΈWؘOɦ rEx=|XX1Ue~ޓz*HP*6u+9 8g5.2WؓѨ^kՅ~u~~^hAx|3Âנ[\Yǎ8f6b5"ZQ/ocĂRF#o"5p .wq#jpdp(FiQ<JEAcH0(S0zym!|c2-r37HA եJR"As4_~:X' J"o2ȔH9uփjh:ρH9Ǝ͌Al ;) p"^&M`SkG=TLX8;<qm)y'Syb&B*~EA=Oɒc^n%*X7{"Yf!Z$&}iHy$ЩL6B&;kx/J %4Cg\TBjN@o>)'ZE E%E D!ɍh(D~>LL?_%-|YV ͓>ٿ'=x!g~1?O_7w_G?o~Aeox=vw#۟3szEp`_ҿo__bx;~{m\q{6KiyZo|[v>;+ /3A2,nN@'/~-ooydwM> w$"P .ILt;Q'7!$fi+2%fOED.N*FDdԟ>a|bf(TULms~)NJq>iy۔IIi{>uPRMB&K{hw؏Jsiߋ|[¡(0'EӾFǦԇh^mS%8$ u]qI`,#E2AiW2K}/s eikKi=ZtTA>Ԅ2c5kl-RYNI*XfYھpc&b؆5.~hH.y!ODǁ?egKоC>Ud=@߫JՁ31np#3lCbׄv-TyF2'"SԱnmK~]ϗjH[@Uڄn ^cVm#{=72sa %9)NIAZ?Uhӎif;(qN9u? 899yKS$~;]؆6! ~tF A;8im7}!j:L['j\Ȕ)vgD]ž-o{\7<ߞ. I>*,~wdha0*Q߭!o#<3&UY=is aiqY7-"F8 1# 70T|̲FLZɼ/8:ii8 gr̻dMn^\]YBe+Sp"yޖ8B*d|>7%TE(ĎE??tTl*˓RQM=(bEVD MJ~%`"RR7VnPn8?vwiY=)L]G4IU)/sº( m7(O5JsHߪ_\:C?Y&Goo~durJ&ѹ.Fi{B٣OQ A NhX])[* ,\{f:FěH'liC:Ӭ3~={|7Gz-;]D<3jsz,RWj kicת/IŲګ&pBXv x7ť{s*Ue'VA8[Cix}A+ci/6ţWva'Gp@KtѯsU.{OZ@fqyK]zoN6wPd o̻Q~ZIrBCL%VIGc B0T\lb>-oC-E vG 1ꪡ<2^2}YЍ̯[178ph&;H֜S͉=0 b%[.Kq]gP55.WKoZjߨM7F6!-`uטKg~ BoM* `Ys.\<?셱dK{&SeO΍lCbTф+%>;T~ƘjQ,5] {6kE!:ىž-R'{y>)W>Mhj_70-œpoI Th,rž*[cݟyᘨ@Bzʱ~ yx323|[q Zdž-oz:z-}bOwC {n!'/~-o|[OrM?R KWž-o i-[DQ#I`L2]a)Y&xEf~S-uMh<0{.c"wto]yp2;L:zhUkY֛1]|yf?*ǰ+$bbw$Iݹ}TMRfjzI6sM]/3(Pyo wesgcFޕn'Ln8`#j ΂HtDnLrA,ʌ䙿g _xC2/oW#\%1! {2_T3%-Y; \ŷb@[{"A4O_fj t Èf.Ě/6H!RWa:M5rGbrpn4W:+jԨP5ݸoJ=?XrTZDIT%fQIpxo^onl 7Bb63wzO4N[f&3%9jyz:ר"KVHlKvcUĉzغ!>ܴP`IS kDKՉEϛUj1sgW׸, ?rz+= `~,bL$@>;u"H0K7LF/t7, %Ju"X_W`uͥ /|2*msks4eMG"&^-q;T8i_Mh&!*>ʎT4w9y_nڲ{6cpwS8enkO5Hw/'?-V$AjSxfW_°A-÷eo=:fZb+Fw7 %m,:cF^&"96T[ח䎖)oT5{ؗ_/&i;^3['xTH,V+)… b?5dtœ1*쩝Y)4>4SKGӽeRY\L"fNiŽGŕ TO^2&SG7i3]ܜ?OF[њ=6ld\Gq +jTAsCT8V\ېj}ۥ6CoExީ~G2h(|D2dkV&SY^Oɡ2ˊĔkx91UPcAR3tNSmFD뷅T]5dC;R5tNݒ@}p:\VyDV'mm@RP΁\aS%()ގ"P0AW{P EDz#\-.WP v[aIJ4|`d0pwhz$*;3FWnC_.\-r<7t)C K|hAV-=H>|*, w~#&q\UZ!¦x-0! 8ŮQ1Jb}[ &a ƜD}q_5#\ۙ/߶Gb׈hYo6ZRMK=f^|o 'x5}"붧" !CX;ʥtUM.bO}\]&RYN{ dޔQb\V~XIq7w;A (1YYP 8ݾGGYnT%[c:;?=OSGWu^tc+ ٌ* ro3 (LUE8dڦlJAʢR5f'lӴwo)PY$̨cY*j&}#&Q+N"X϶:+2z|+{C9֊$\oQcEJ ]Ulͩm=^@W<2ϼ|z-J޼o#<)6J#r.4?r:zF;Z3c/܊2u㠣j$j^PUJ` ^@/t$@/^zhN>WQeI&xp]lNxo4$d Q{;[~aa!יu?"X- PzuNbbK2A]4'쇯w"R}Ma/F9:Kls/ ʿ'"/: T%lF?raQ%jIMQbW5tFe'ub'.Af9d=)t >U i oK\6ZX̭g'}C'Ɨܼ;`oeG c@c򿰿(tV٠m/:񰓽A!u EqhdJkWE@(;ʬ4ʪvEٲLNB% W=yCЈsUaȤ~EW1U{R&[Qxc<ɻU"I"u'cI䉔2P6`~T%7<0g#g]p:TZej/7dQ;ȹ U9ݴae n \?p ?oS%t9̉ 'c_]vQZI%蜃engrK3٘U$WY+rLޙ 9J9gYGI$j6K(ȆAX*4~'3.l!gl[prxJu 6h Ruz g2%büʻl;=҆`6#U)ȩdi(j8P|!+~bGPg%of|ݐ?@Rf%^ w|ϔkS'oUoidŔPJmOdqL|jS Cm?[i(bkִj~ぞ.{ȾM<\mUCM5R ph2԰gysH*&k=&/jV;U'-!xFY<7RB/nP"3JfFlvvn t2[8h/{DB9 {K8LλOj3 4vXJ >D.h$.k#((z>c˂V-&UZJI&hOc?!!{fHRT,3 =%9J6D_ԑ J}ņ٥ nlSYۇN!?u9G -~ēt Q<#r|\QeyXJԚ1M" ]Yy॥bBwlIK s;'b않Ht+VCeϋO LV{vγ_{I&M"]ʟ-;l9A ܘGmkTTl6ٍ_z46-6RsK7sa9ʪ¸!FQAɫA9bUT}!פ7j(^Rη?|#P8BeuS̺T}B[2&i_: lI 6v D[Q ̸+H}p^э}D0>H' :I-&&ДENvyW#ќ; ey;e޺ VC:I?MJ+oIjjyyR igYl MȎDӿ*|B4 CEuw' +DM2zcDP]8Sp u\[Hb횶Y}_BXBt4Qf+92ulʮEy'DCB\ Q{9YEb}~iH^N*$ǥK5 Lp!Y^ve^CS&(&8t8Ai XrO0T s~DZ ~/2cu=$̨:ڔ5-ׁY9|:GV9. ?KWJBG4\^ XCp]!uK~o2nBAr* 1>zݝ;*V9u~4#5"}[谪pJ&J$~,\3N_{il@ݺ^5BsDjG o{58Iy)AZ/8η (*a@[͉i߮ u}U1Zz-Y2΁?d̶sN [I)VKasC?ä;?KjSADuUo3v6c)12F]U`d;xhuZGwX̵ԠMq,t)TGg~o!Xgj\s)2؞䈊Kwt@MkL cQF:z1VPĺ]!BwrlJGOyJqǓ@S4B6bqIHﯪNbH&L2HM8htPqf %)ޘ6^y}w;5L=T\X:w~zWxvR:QO#-|ݭ13%b8eCtŰݿiyYu5_j&1{魀|Ujb}<?DUƌަ-V531cbGa5&߀/V(ul NaN Åv(enQ(҃gD[ʪXLu+'xezȦ*;Bt1r5_(OW#T >+(eUf[Y@ގ= Ӫ *,ozN/*s(-!JI/9ok/CGէɇJx!GpWciW-b\ JZ7?_]h,[5ѕTi5ֽ3ڙ^|^:dY/ż0,I9zZqtNT_eI(sвZNvT/q /@l\SCgj406<[\IOh}Τ'Qp`He_&e4 fR\ј-2 Qt U'r1~*YH+:1ѩ}Q"J*ѿlCNRk[+nzL *86yD ~z͠.Fu bl?tEΏ)CQ=i=Le@!$SyJt%by{`5m̄|vWPjP "9iXsS2C!+QκiCx| 1o;1) ǤJƱw:KVj гl/ Й@a5 > ]8MSk;]JMW=҉oidA6WSPJHpm]c@V^J)XFʞ _qEE2X3EN/P@vmD`u%#cE|  ۟bBwM88-BhH~VK[ E dC"ӹٙc5 e&+1%ux0oj֥fr0bOŝ][S3WD:bp~r|Aw e =T0ˆ &ZŪձh,V&0 K4(WGNx3CEp2>귊0#(JZs9ZVt@|f=Hzn /x+(Cbe"Z&cboȖ[qVL/ ! ;X0&s,Uh m0'*˺o%EY8NYy& 8ڹ$󥡇2n$0M`-k.N˭ј4i[' E,$}0X޳EVүخ ;hʪZ#ʑX4B[fm*ǽߐYۼUB p&WM`kT).6vjNGR@'L)C9Ĺ :\t#\4^Z^ӵ]ӧxC<f{hT{(Glbq_cԑu<8/V`IA|I}z4k[6f*n=L@D OXbm<%CO6XѐKzp>?޸حNgr=Tup 5 i-=R(k$FjGIew2RBksK'h?.y@0̤m#C eFC#U&Q붥caHq?f v7mȯi'0&%ҢѢJsRo W-iR*L԰!?>@ (|R 0g.J ,LP] /PG\y\XlVѱ>7Z~bxh&R9vjb`46QUj IMG'1*Re6zAyDT(@ϰf2<4ƣl4-c^,Qvv+4f7*tWHMElX΍Uo@{a6޸ͽp<@e8۞`OB6-(FAC6VɥZ)s.40Tw\‰(1A t|7kXZ&>℀ZcdJ#y,dX Κ˖҇ gyO< h,y ̜uWѿk}Jf/BŹ]lLL)-HX'^~2=ۧޑ=&6 Dbcv`~7AmRTW:EUI~ 9kcFeR^ x0 3Tj/V~pF(3 LE1;YUu7tZaaޫW3GzK}G /9 +c |TJW LtX#lzDK@JꫭlsMMϬ=dhXي3 5{KML.(`z‡(U2<F3.ntK3^bD 0ѺT rpBP"#q h/2rWeadqoWccv-YGaDzK:TfkُF]؍qQ׭!4 ^ eL\w(ČJճHxy;.=.Wn]8 ^Wl%;OVq[_6!.8kwlL4 .Q8!XhpeT@\^V(*DAsazAY9|4RѿO&rYb y1 ;nDxױ3"UrtkǐCC7Fp$eV1J1DSE˸l ԫn@=z˘YmH xAWFAᩧ%suI# ] FJطBZ+]lПޔ&9 ?_i!!\!ִx}6݅V/'w Vx`vJr*1Q6Ƒ DH@tB]n{XFI/MHtHwOӺ"4x3Shn=$^ `# HSsWjV,p-zh0& [e de ,b iX@'^O٥N<pB\: )Ъ./ԕ7uP**0aTӤ?UyywHryA{}TZjcPu'MRb݈f($!}Pa%͊:QQP[o#׸K-k@8_srjf8]) 9V_RIR_kw{2dݹ6ig`,V9ucZTo4xrV ? p#]L>7A!1 +[g 2 aD*(.rj(i.~8٦LZ'~]\0h&&M2 =gFԲyq.q'|/ 2'ӷцpe9]#Zb^aqJ[Svk$Yt1RU{<;/ JSu!4Èv7z5>j=@OiFI i "^?t]*"Q~Q"苊q~1d7.xdEŴ>v3uyt_r k̉RAC_poHľl^ܑ=Abߦ60/jXg#[Ut P%=s_,=vR~ʝ}ݮ[YV׷~ [_!BoYW)pID`OW։[Q9|(Dvv:P* N|G/" %}㙹Y 배o/.'YWs#"ݾazXLR$lssUÁR1X(T.Sd'YZZ4q"@y8^%aK*X'߉gmKg"&Kh'Ʌc2q-iːX {mv8"XF)5w| 䁷g]v$a֩u榅Yĕ?1jE%x3G}>Ɛ\1l6ǾV).^asYXNSﱔJ5)c~C#@wRwI3 sRQ}SǚwPqf0Y1_6$s eYǞhAw1mp}H})6:_2û=| <)֠:"jtW߲Lxs{FupCNtϛQ(7_hԭN+ \`3թ=p;swRCo~꾠[_J1'9)c&Ý-T-0CUc: 2eew`p, 71GÕ̧/Az;Sj>rg!8"9ˣ%Jxg'x]tzh%>I ;X# 2=Bxci4@'LDz3選ߦ^z\>Hvs[veFDV4%L:C*{튄n$kn7d;Ӳ2;$ʏzZPLwɤ;ל0\a<ý2]m_[—SHnYtIt|egE*wYʈ=S~YQGབC;i⸫sp~ &Z/ވՑ!6bmj7H| 0).b1mb%nu` L+@6O;]ü`8L^kR"%xHȪPٵGX>c` ŀQw Yo=(n4%i?zUD{”@H2څ4`me \BLfc #DO ;+e#DgKI=&MƔJDymA7up~(#`#U=snZ>)Z,s 1CMq\i >Dc`Mo o,]۽ ma\?EgnUA W}tۊ:e(qbwwL :ĚW}mrUi8ּ܍ fc1 i} ldA`td6p@v@S_aйm:onc֘fOkʕ0 eAgQ$ri%A 4<3R5pR7uFt'Kw Eu~q `9K R_e%#eLi(2Syl<2x񴡸Es&URw5==פySYupΪFfrN) >D}&Ry4& IKw'#`Kp[rUoVgӛxh}Gs-^QE3›C9S>*fK 'V}2*7R)&2&gO)|Ec`Xfjknp l!B#v3sb,T![, L;EvGt"k \G9/zC_tSGnWq ;p6@G!Csl ȅz7IWE$ 2(p,FYݰ߈Ֆ6"}H{>'JrLYtvam fczkN\QWs-_Y3eMܙ.5=R{bWۼiybSb!^o=qbe 5pYk":n}ͨ3x Lz|ZK>},nYdV[ efS17aɰwO^f7VDf (h6Ծ1溤4j$4YmUA&kAO2NQ҄M0 h8}8'[KCy%}Atp&kQOKa;2䞮a"IVF%Cld?F 66m[,Xit߃?`>!.t#E̾US?Kbp6}[R> K"<}[]^cCn> ckqj3tl*rWm8g7 oƦ5v_'Y&Ė=u$ސw\=/j Hnz%lIUGYp7%dLB$>q*mUdhj/PG ?9VsuPIdnڥ'RLIxt)فÿkeT&m;/H*a,K|v\7ڜMf~>{xi[c~u6Y$*-/U7t^GwKJ\vʪz k%*B;i`d`5wȼN~?5-jvriBi2E>rv>v2gwTz9.oWhV.k:[R[a`ɵ!l9aN;e]`r΅U~x"ʯ-v4,Ӎ3\#s̉Qϫ:AN)ԗ:oVM+ffi-gA$ܤ2\r'BwMݞlYA>K7[=*cN(L{S {v2UDYy عQN tQjHځZcv+MI5o*C̞sp$9UmqAi0p7 qC,)GJ# V0ݿxl0؟!>c$Ot0Ŕ.N0I:P T1b{=^xSP1XJJP;`F**OCи%(RO}erÑkLk+Y{=i^rPV{0 !;k(oj}^GÏ!wbpJGH\ H[-p3&' GL#tY*Q>տAt(h32<i8#˅xν>7c=gR'}C_تNx4uU _ﶃʩC)h_~d9Z ˜k▅Uzf\Vv-|L՜g~_6;.PID;!5TfVs`289Mq4QyЄ? $i6 *QZ=•y3"U@ 8|qՅmʳ/WJ-imSmnE,{@/Jnz-GP.)F<ޛ؟:*Z3S>d=ĶG͌1:v!'13$lA!.v'q;*zI[n9dU4P v/[0ׂ+U;6 APXp5p*-ғ+E:H::Ќi8b U\P0drTeQGE5f%w!'$ܷLlltB{m$:GWaq~߆ d$-aE\ uYq؅nE++6]Q[qi t# G7弉*+dxq;Ogȶ쿈W)e6Clvw ._ia/qNAJؔ3P\ZzϬ%ZlG tXiP**xh[ʌA?LCyNM5Wj|xBi qZE6o| 'D]η̭]E [$PtŲ454\`Us25pGkA-?tIoLL:K8tA}l׽Fnz}Gj߃9:}@D-J+[dCܶQ|@͜aщOUBbko)+t5B>!!Z]VT4K?2nkS0\t[oDdq4w+*_ ݇ؾ}[?WBB}C!&1\ؚٚLeb]kJf cBw%2u#/ fanWbsd|l]7;lhPcsvQc뀫MnZbcDdi9ˮq{{90֛LxAđ("ZRdXYnC6 Pe H:Kys4鄄 !MLj\dƧ f2dtJγ2iD ~_\]Ud/aΕNݎ|+QJBɷYu ގUl3sN@ w/Tn Yux_鬘GUba4n}_'F,SfNHd^RU#"TmAS\Q+=1ߺ ܢjCؚ9@LJ|0.pAHȻdV)!͞3+~0_96ߝ`dED+NPֈK48[T/-Igdq*+frA'}ޟ>:V ' ɺRh5.+OI]h@# 3>Cݛ?؇IV`ž~OPEt\'Qz]C@ςAkXoH+7Ǽ8b핢Oh?H&CC ~* W)%8ϒ)H!A!XUA?\xSL|8]3u? Wqr C NNO_FhtrՎU".~4`)\Gno\vozʐs3)AtV~ovxQ-0k?u8'0 6^4̶ ҩY&cMqOhn)&]>*P&k (BJ<*~+ XD܀GY'{Z'\N{}$Tu~he] i9SXerUo'i'[v݉p+.EƈєuabBd-4T6voeHnQi:X XR-mϑ3Iu4dmRւ?۱y5'V v 9rԯt稬i[#WqHg'rsUqZ" {pIp8raXN_jlj\^ye>$@W.(%s? gyUN= M71~3df vR4lq[DH^OW_0+<Ii}:/m#x7]knYN)aoU6dZ|[秝Nv1ʟ}(><K.!J_Gb_l":-iShȈc3zŮiTߘU:&.hA nzk}dv4;tR9Z ԟ4 òGW$3֠%8bŹΛ)%5 6 _,Z ;@XDD^rzSX"@6podA*af d(}Mp|<bٗ{.81 OӉ1!$dN7 "˸ {ɧFAtkf7uts3I'1i8wX52s qEF3A]rga$ĄQ`_ĵ RjǛkfUn-u !86,G9y Bof(Kʏ/>hy؃@ˮU7(8:*mm'e)Qe[>d/>j4#2nSZ[hⶩL)7X>+b㋥oΎȐm~lbS3;8,Ψ1 dLK$N^ wOqIDW2WSY9V #/LE N\}BpN,-S;[o ^Q18c{FC (yw%mYv6['4l,D75}+U.@Dl -(k:ȷ9QzxWPVy~r\YKFo ֧rt:s _0(0Zy ħ(+h}$ AkS<7$'i|#ȧA)P2}'N5 ])A`Rw\w~F/$v9n_g{Z4"i&D#K_/Ve4"*R:3T-~w z_vg epwr+AΨ'Ta9F"h qw&xBz7.A&B$ɥI'Gá] m2 +hdh0 oϡ~v -UQr^?P. ocU2}#Pߓ\,xglp6JW/X,ز7ˀ߿ݮQ $[NxMUVG;ak]Gnj.,ٜ9 ?m rB7YdrVYbp]@!xjnHǮ=ޘCdͦfYOv7(Y}ѷ!n|puD"u+j'5> o|Pk$N$<;j-@,>&4mLHrK.Fu1C},|qhj-A9(sk@akFH6cMg@׹m7pI ߐ&0K O㿒S{=hJa̩8\#rfژ$gBph",[?!Ly]3\p.%e>~z#w|ަb7b l8r,d,)R\(1Anym,ӣŧ pEGMrI|' jKl% -o'HzT.@Vac  (Տ܈+yjmjIߙzomU ̻;n|KݲPHzCaU[_Ɓ</ym7gW+v#w[tM`d"{P"!mc@ .t C'9/Xx݁ &))#,@i+b+P-Z}H@4G!.}:?EGSƒ`['qmrGM0THc\j)PF\n9^ySmTte:NYaθǶ)tOמInK,"S@LĮ&dJ{>Uʒ2S^Ƒpy&Rfg9lGꮘy'$W\2BMv  ShQ┑P͍Mf7(-x<Ώfܵ .+ऻE !AS/L|8쐅f!Bh.wR˱JSxEz`AErd&~A@ݏXZnbqGuJXrMmèxiX,V6eS3:o?9fZG(k!|Me6ќ^TwĹXywsTHu r&tmXo>%Mc=C˞$:tt@^&Te) 6Tw̰/K j v7,uo",ı-u{UkZz \n]vba/M`1Mv}g1l|5J$nW'۪pw7pGνko_Kя$]O3ۡw~=QۭudTInT zHw^d.Cy0LhIZȦ#G?4܇eЛI!:݀ׄ^2|F./n,Ai.Mv)Eq0ObTI=+!3fQtlP]lv ?!w',ɶe#VXk=K[:k-c7gmO&k-+?C~y=qB>; 8>gJil7Ֆtޙ3Q),UcP.Йc8 ?c3](bև[xkV Pt   }\Dd{bi$"PDgՉeYUAp!5NhDKx)`Uo \BewO6BnR׫v]ЇLf {R%%H. +,i^iUB9V^cD~NH!5{Hw2ML^<vUݰPeIx |gJNTdlfb 32PS19q*kї`saʌ3wLu˩"Xds)dHS^-/jݭ1 ZPN !ĨvCM0S oS]`GP`[,>KZ :.A C+R4QTщkmx.n 0׊:w ;iHݝ G@M6}sVРp| [=+9mFVʷ,Vs0h\5x\"QNnrYi%ɂ'+ԃO5/&*idi&7 v*Td-SQ4kȇYt.[I;E.* )}v=Nm]bCS##Uo⺠M%eۨ><‡Q`tq0ݖ=:=…e|> T:o"\“֚9 sߜ`%; 9 AKU}=o'S!R!uO2jV^}* :jhk'RٷX<0{4Q$@eд<)'3Y;d;6$]W\@(٩P\dm;g Aw0w ݣ~i"QEn}cPfO{kҍjSsPVlY{<6PG5`<%qTDdtq>3뵀kla+}B[3ߖ~DHVY8/ZA.NXGxqƟ`ɍ Oz/"&QI`r 3[B]Ӣ5t+ U7pZ*@0w*jp^`gH\I2K7ie4(D|ݰI/5ZNAmD{&@3)$PS|5{NIHjOh&H?S2!+0b őꚐVݼLWfck[J L!8Z0rLVR% Xyrc&>˰)Y㬒O9NJGTOOXwukp04Z0̿aem ԟUԊ $X´oQ5ܳ檵VWOcPwד9޹g'allQ$,4)H8eB!~a,۔h'T;ҳU)dQ˽QaoyK@(*ʜ66n\KqfCN4l)[Ԟ,R%ỗV@]S UˊU:^lɼ&6\k;skY{H&B L{-p&F1_FĆT?'AǟƋw .hPI 87_ڄ5?G9bRXm0=S~":K"CZ*qp?zG"g^72 lMQsx'o8*-^{= ;WI'틛Sɨ2AŖ6Ly/0GM}I=S; K,E %wu `#sR;`oMTZ0WZ v@<`޶ MœvD|)(SQɉ, @HۤI4 9Б5?#^ bCig@20fܰz9Ԛypdew/TlKո:wX;%_%"D&W Kns)Gʝvb ǺdRF%i"ElH,3yyndFSNj{OMq+%oN`2YapW]VAhN؅;USq܏DFSў9={Borz^qOQcCh?q}pf,0aD9;„:?a!PdJ7)KakϹYU!*O9+lTX ϱtԤk5遚dΑAXnljځ XRޖ `.t,Uf6.5?r +Pc3~<򺡇I 6&xF(^][g"b.%3'mZ"L/ٲŽ`䢿d:\Sa[8'g5)cdR?yʴgN 5Y륶3t6ITC ,F}1BtYY`p"#/?{?GlGg}4aX1++TkȷsVsJφ:@+#82y,z0HZ@X<35 :8+K :iumM&#'i8 :nz"{H>YMå%A\CqmHwI>INi&wyچJD+Y{rYh/Rfjı~U+x#_ 1^](WΈ[χ#7Xp];HݎfjE 2xZ#ɑ%Ӈ!*Uo ;#ޗ,b FmfIFZ:y^TR}H}K瀮X9z'~;]hcwy36L|_Ӡb%%/8u^T"f)ymGp*s0UabfFI; Co%j~G'V qRڔO(b!%lQu{2V'bzB_70oUQ9OݢP<;d*ZrxT* 6-r-+U~&hsRCo]I}C9L?o!=;^㈎ F. BʗN ǓX>F%֪c<^IhHR0,Y9&~OW9 =뭭%bkSr)РEh~b5r{(+coxq52/`sܜlЙRg.RWAfŠXlXfQPy?s%8$~ΕmMe] ǻn04SoL"/}Uw.7~46CiD8;*`- %"fOU*-$۶7g18}R08a+:74ZUXpr>DLNU=}h]oÏt<F 4ӱK@fMݠyt{twpzEYZڔ)BM/6ad[_:$aՠ pE uoCVr"OѸ1n^İ@ad֨  Lup/b 3>8X|Q:C@-WOGM{#Ҵ^PЛuQB/vϢv5' bOFe0 gd26YTLAsߣ [g= CM6u80J~1O\߷D# AiY'?}D!GQ˅N<$ŭiXrb& NeD) ?~eS زF 7j3u?b*I@python-telegram-bot-21.1.1/tests/data/telegram2.mp4000066400000000000000000004017641460724040100220660ustar00rootroot00000000000000 ftypisomisomiso2avc1mp413mdat@"z8-----------------------------------------------------------------------------------/0LrT` zߍ3ޖ׻;h}?ﺶnncY`NK>w^U:n 0鞯!KKKKKKKKK0TWA#,?KS‚:?N_BOOy?>/;>mCu M@ռ10Zyi`a@ -E*Q%g uxɍ~-(L>xs)0UOyO}N\z>%ަc᷉@08BPu W}(h&RIʟV;/>་\l;PDA$F*GTz#׶lY,b~:6G  ILV"[$ joE8&eL'Q{KÛؑ7sthAWĞpnHy3 vbpG?)\N.،Ff2яȫJo&IsuJq,̉fȱCR"L̂21 (?uTieupEْp *fvaFsRUþU k{&6 K( $ V.[Y[e?V)灿 ?P"9㌥cR1^}WNA@I oܿNcC>9w Ls^Zb,4m`&U9hůiC{nTsSh)W}3^ BXs`8 (o+Yq&OlM<4'PXBawbhWH UwC ( ziB <9yT?]ܱ8 FG1*@Zp-&pfZlޝ}imY' O3g?򗓽0s!"/Y?F3`4SE~] \rs9*PD x@DG`ȾJfҁmHB>F#}U$7csy::aN} Ҳה Ig{)I7-J? "\"LXxU/W_ T]+":5p VЂd SD lf@| 2>-ӳ`/-߽d>c $qi|mauPaEaWMTE:C_䥹#~l+'gN5|RK2+ _XÕw"Z aLod ͎aV'㞶 ?٣FE|hAǤ{SPrYE0lbNӒUPR.M)\gNXb/烖L5?y| ,|c+!1MӠ3 | 2T`hj@"su'Net}ErOG 1{瀢rQ/OGL-nk,?-#d"tG@m55]]5Eأ ^9ox_A.xc^`!Ks?Ҷ` 6|_ϸ"T,! 0`5-" ^Ӳڇ9dѩe,$X刷k3fEFs pfZe^j2غ-}“xNhztsIȘ.&N&9RCM@iRH`§ BDa[\qŨopFi9G8qQLI['+)wBzL&yMNbM'kbab&+O3 WrZt|_>Utxf6~$>春LͶSG u%n3AA >Ee?ͮ1! D!H7E ]ZI`]1b]!FHl{Uvo%ρw1uQ^w |C"yM1 J#,Z( WHB"qĖTJW# :6OޙbޘpR>07;Ү`3wA5`B ĉ($grZi?Y -E}Y-O]&AblRԲ.MJaE2.{Δ/*\ Uq&h *+y#;|-N5f˻?M`NHr<6φ7MZ"EdyW5 E d?m#|2\bk=AzeVFI!c7x0yٺ{v4J擇2O ĵӇ\7xCIt>}@{ɔzY.<A`OSf9РIZd% QQzTaחqBN?W3?q9|zh4GxóV@Z@U2@,olKNʲ4{&NJf& ..nfj}~>7zb>lV   2D}3>HzH̒z`#pA_|+l-{\+3 C$xL{46Tg~ҿ!Ji?  v:sw>18=p79'50J&~^2 (x+78b'P0SQ:O45 ?&"R瀖` Ft|>n hNee~FB-J"iaҸ-Z';͖A<1 lU??4A3ѓ?~_30F I8k>\y >a(xc"t{~-ݲb pTyaQ[6'~SƿxS2Ft845ߏfy u//%Eo߶'wrXDӏGvdL="Fb=-b51{bT ^g5ؒ%7_ݽ`@ '"Z[y!Cj}p ds=>FH#y`i1*-pMJQo  ?h(@Zg`/jC|_0Yd'k0$Fb%lwᦣNOk r\&Ȱ/v᩾u0mAQ&?Xm1303чwT.Y&NnzGUI1?} N7qHPY^¥?LG ^qs[Q U/wz|BRY ٯnk6ԌkQ[*~"$m_&ۏq=.Wu{ }DZTf!b׌f/J8keǜ wПyk\5"v9+L{pk,?'{WF`Rr~ <:Z,,;[Xåo\kI;e9 uq"G԰ej<O`qlM?)$!]NC 73,#hcIcp,8V!aq`Fu g F||Yk~<T@ A@@W8P`V.gO̓xщ^<i GfG04IZBƚ44zSUi`L tœT%Yz h@#LLV>ivg2U_ב1{'}t[wu{51Ɵ7v|Ō6VW2ng9/ >dʫus=Ǔ;Pqoc٧?GXfFٽA\]ˋ֔SY#cJ43qf zCBam/ _vmlW{}18.s x %58Z+40eS2=`&jC+$ KЎW=ЃŰ8r!GpKg6 jA.uB{sh sp8@+6HV"MҸ!9\?YÏ{[!bҢ}7>*0A3p0pyxd'SB9R@'_Gb) $Ibj^`B,< Q $y /h(Ei &'٘/?)Z4GH9G]2 ,rW$H#U?wȑR]" sҐj|Z+. ]Q~y{wzAXаA]|M;/ιܭA"\A!/sή@ 4noK_"v~V~T<YW0NpZ%HA~Q_Jqں|0 p4'S2 } ;1@">' wF0}^xZuu ч+g5wɥGC#Ga.]}1ܢцBܧE-t|>3gCSO<˯d\)1Չ:$8<;42&9hqmC&epxaIӒh|`(iY+ '"`te #dLმ0vמJAP:b /^V,@ F9C6η^fd{'*ҎqqBǷ܅:0@H@`xkvx,<-b2a8G52OM@8ӷOުqXʵ-uR$Wq0OoЩ 튿M<]bJYb6:Lւ[Dbi2`1&gvΟ=O](pz,Z>pWb~"mlNR@&O @a)+ҍp3?Ta]b`Jb"uy7a)@Z:ӂﳥ@3н~E^U>|1ɈA.@V4~@v`T_.S]2G i|el[z3ܗxgx~{D@1SXT͔9p7kc9SNZ}T" kZk&9> x6";X ^bIT\GԸk®CsLs h omU:DH:s |6>PchqtiL9RC)/_׊/כM?uR+4H)4);V)mZ{oUS%Wpyt҄&GJkuO!~/I;5V%=W6@(Jm$z 1xM05Mx`ldkw6蒆8 |`=^pTTq8j(zx@gsO6\l, 3^[p7mit,=+玄%OגK6jJ6kF"7w+%dڌrQR{;  i튽-W|}wOu-pH|]Cu K;|V0-5ݕUS}}s:}ڥIzSŴ?\ݲyjk ~~5j}ok{3S={t=U}pݣO=KB0|ǧ?R.N~:PNo0A5aBS|P{^@< >!e)&uT@e s`dG;0coD1G0 z޷e?,1G֯Q  \(|k^RJ  ߎҀI'r@f >c ÞIϾP/\6gh(sj)H/,| G .'B`?6QR55슼˹vO݉},BEPVi2._W:f lPSes'vy=`\7?x)rrN?}mn:L|s7 E$#SQX.0kn{]V"Ig7ݾ:{{jb7馝4תkEߒgNǪ !h0[qB^z|P^꺩eƽ`\Cp~ɬQ? fq{* x{0G/0 cV] Z!%^Jp@} @,1 Z}xi^rp63iFd&SXD%=ffJ"@0Ai7*}:8JÀ$XKrI` yPKD&>'8z18-h[WRb-`"ʏ5`m1>R+.1~a^N,k#l ٘nc}_ƢjZt3z5GN_gMys&FN= y)4׭3xάf vCw9Yk/.r:4WDӥFjVzq$c s[;=3}n+ tEIaxTJm;I 7Oz}:VvZ.Ɨt[%o3;IhQ^8`a1F ^{x Y xe/DqOxc 8x>Ċ 'pP&1fU;AGNm` I1R 'oh-z@.$t2ZwyZ0II–3@1ZVU3agƨoٙ Xg`&zf*pϸT&*vu+ȢGҷ ODr\6BNR3GYZӮ%{3>PŠ8vcjÀjŀՀlòF0@^AOϵ> 0;RQ* P mLąD p~y$Tj0Bfr%[7Z(b'0X" uSzTjDY*%:g}k89+*iX ׁo(t$0pT{ ]d%AH7G8R,i1 }7Hsp˶Ine.HRלGkU;/]Y.㿴ôLwqNZ @F^JqƜyG A  Ê UX B\@1@k‡'%u?[_(B4 ke< A; D 1Af BQPs2!xMiXIer$^@oU Hp?R_1iP5Rކ~!I>ӭֲ$ILҫZ  |9E&AAb,mֳ^Z @\,ǂ ֪.0 Į7a%nE@&Em_^`I#-g/x]VS꣗* 42蘕=1|W烠 [: Eƿ>߅mL:44 gdǕh3ί0 6OĦg VhU AM ٿWhΪ65hc^fB: $?(wWVza3\ ($-o3p5`kZ֫"p?qn/Zk?|"N}UA{焙_A%K/_hM>1ݦnb! _q>3bn0xx\Xa*x~̼2 ʔ 5 :f0ھS;:^RM`vr-oiT108~L!WmUsR4UZK8\p3x40)~֭Ʉ,24ɈK-2u4 Mx֎ BUZC8Qqs&R/ʋ@[%k V 7R pqhL&2gJ{5j{ p%xE᪍!p€x$ ʔ04PSJVk qW|<,уUcT8p߬ JUVclf9 @ pM%*4o! 4 $]yF0* ~p76#Q Ҏ`eG[@@>& Mmo<3簦@a oV$A@`4g8$ g˱~1jU ۥ1OnU AC#bCIBXdj_i* hVN `pc4oz&1 +F,4-5*1$0p)uj@ZUIFp-@_CfZ?<@ Z@fp)Kx0Sc+MMXmRPdp,@3QJ7:@CcoaJ UUUUUET\\q\+; p  ]k48RUV1Uʃaq=Us#4&d9OGNL`$ :jF[UWUUXp?k p1D">_+^f FUUocjꪪu1-~ِZ֟F]Tp1Up p UE#"F8>5818<8qQLAWUFe&xSUUUUUUUPXC T>8|av`"mUVnbʪO 1" @(ANn3ÕUP^emBu? 0lYUZƀ$h>ft.xYQsʂS0^0֩~[9#0m6/ LIxAi`3S]ԗ +L%?TW!"a7]@Ό[hy:]q?yczxA6(UPlt(t7hTu $] F楱!mT\]T]IG1'H2=6tgLڗ&*2邎0Ba]kZvOXƁ5\M?=2OJћN9>7RÕUUUN (s&6%Qd+po Z5CC]huEjy`b$lf}/>ɕ1i \mfEqy1+ ܙ q`5^uCLK;QKO"Id CJTip٤Q#UJBO|oL\ţz0M; jOuҰHT(Ga;%G kTpXxl+ۉOi0dx{׈x=C60$]IؐEC Q[XX\`ZAbx IBYeH4MU+Ulh_U3]CRd) 9R^Yeb_*d/Z9 sMI,$ ꪱý,8]Qí~_13&]TwyȽE^" Zk B_3".QCȺUĪR?ع)E2$ 7\'2%GMWjcj-411v\O)ScIIG 4fZ*a&sS;cf8v֨(cF:vaH9K;r yU=BD %rT*Z p;Izpxhyyp2IpZ#Y8h;Ē8k_5U=G?=ˈTYKP~F?:m$G?zUl-U|馓x)C~\G0G4h_3z9r-#)._L/E\UGɲp`'XYICbQóeG?TsG?Tt~GGQ0aQ΄G)"Bq3/Qu O;8fzc2^vW8~x{sG?TpQEC@ +֎x&xM*9㠊k7RUQ{3*@"`f4?{'-(bJ~z9:(?j~ *֎MQ>80N#95[j` UTK GsF1k?8#:QáUj~8~Z8~uG?TsG?TpXxɁ0%5JTU+P3%/ 1᪐@砹P׸(ꫪ8~ UZ~ ä5US/äf|mQ?TsTp=0Ig@&H#!M ,bQoB͡PP9)%ZCl)%kGUUh$֨#USUkTR89x,kUGWU36:+sbRuTI/毆kZ8Z  jh=UQUVZUGxnjfL>/$ 4Pa$0 T11Uƪ*%H*Ӛj#Ǜں>Dk0. -V5ej L>`WMgx>cm_h)NW-0h RzYt* 3/FMvb&\LW5+LձZ)Z򉞖 y#˞[Y:b‹Axb @^,J2@ >JڱhoUjk a}s[K |7/cR^}}xbB0d3vݵx){521}4!?3IF+Vԇ^>x 0`Henn?sߍ_.eTLJH2EZ4f트c;]$@lBC=ɑxo'whֹ 6d\^8yg̭Aur㎾cn 4p+ZIO>֞ ~vl@eceD$0 ] K77)կI^BdH[H[]t׼IM,`,7?ϦCr:CclfÌyбDEۚK Yh0B@81m?Ph d淄i1*0L! N){=fDG p# zeG 8pm)nPzF{0*dyu`xϹ`ׯ1)i6$sq[!A;o2"bo  {XK4 `;'6yp'̖iT8;ֺ2п\vBy@:JjAFk '* C_g^]y8ca"\'e&%Y=#õO;/cWohjyÀyU$3cq.N/eVҪ&w)B@@ 8FG??2cߙ>V䞈y$2@Q?PS%UH>p1 zӷ?b]^l}$8G$Valµkwx2.`# P;ZL3mEbjXƖ_1 jlJ"{U!  z8cGc_8c8c_9=I&2#r\qo{sfQw g%MyTC V7L$$zCy$刚^?2_ _+ʣJEsL@p3~dQ7ߚDZ+0 |ЏqTgRFwhP8*u ~0/` kWTr4@Gm}flwf3&!(3rCbp"eHtkıC:hV{bC@1qiuT-fc$2E\4&2`Nثf3_1Mվd?h' g6*L VpUCx'8c^tC@߅4jʕUI\ Fc_Pc!+ |ExAۆ!` @݂ > K8eLLOC| _тr+{?O| @e@@ݛ+ ſ 9ܩ7 '@$_~ &C@@ri  G:,rgR)CѠ]qT0V:F0 5x*==P.>Ym-?44'c|2^-vb#ކYh(ҵ_"9ZŘ]헤*#JhPOF2a? p`N9G GQQc8/YDp ~J $ @t?[faX[+mM|EjbWxD b?_`ڋ{suP A=: YQ 7pQ p+ u p'cʿBH|`+ziۖhxdjpwD9ޚv! ƽP-˅,&esm4Tp1G<ٯܸ8O}6c )62Ov?C>}'G/IlOx7s`k$Pg>soqN6 s|XAss20 F0o{d~Sm 9`R$8дSFjZGL]I G;l0`[Ir:88&*8x;J8RLUGUThCGZ8!c_JO8,ƃC% 'I4-W'-$(QY}7n0 k1lp1A{U`C4"gUKU _.GQƝ IGmvV8~6۪ ;فf\kU4 !oQCja|B ?] )_FrOydzǁApWɆJ5XZz>7u0(꫋C}6XR8#H_B-J(P.Xz8֣oAa(C@ p) )UUZfDPYUJA+mch$ ÍYG_(ʦaiO/)_2QMH2t8pK0t?7s4ʯ*O#H_'A O1 _Vҍp7GZQay-Ԑ 肬quUj8G|*$U[]ThC:GpCˑp3F8ⅈy"!%F|q=< Ȕ@P@ en>۸hWT\q{="_o$jș8" ' !a7/}SC /9_ބ PU}kUZQ@SU  eM/YXz8釦UUDpp7BUUUVgf.p7MֵGz%0AQpxYjE<:y]bq# M8 d`, ) UUVy򰾫&.ᡀ~[ĻUUjD KO UuOSe!@89",UkHh!FUUUjڪ8_֪18uoqCZ izI(c 0T>kҢdC-~AӾIW6igX׍ntESY2 `BCf?wI݊ 0:$MmX{ÈhWkQ 6hCk /#N(1AL2EU4 kB%8(45nH| 0t6J%DA_Yi15\5$%\ɪTdFiJ ,b]b*+P҃>$,:]lB1 c󬄭"0Bi(%@yH"Tسr6\-=IaE[!Ě[{#dXfXi#ڼAC%|QS+#m=mtUV**"8A8sočVK`ļD{  {p1-9A{0 7ٞ0IX} P^^[q[y"Fr<y ?-|l0aCawEٜt;TT~_1'o0@- ,6?}3"I$mRE F30H ۪(=@\4W>@@s;YoL`?aZ[ʥԅ0 J  g{Q|csM6!0(8uK' $Q<8LwY^ cQ!g}9>n,|)b*!,`\ c2Ħj[9bBu_G_4*a2Jlx'0 gmC.Gl)Rڢ؝ܑDصex )HDaĩ{^|pٯD弃9[*$ @hÓ$9q> r<y1y(vR0p7;ˁ~u'$u7*͛2[{/@ 's*GshB 6jkZb a8J5ݪNP&CCŏ.EO@"Zm7G_szX|°~> {'>pA)LPv;'d0P,lL907oZdzMb+.2)5xwUϚ ?0>NV̘aGϴZjv<_ЉBzR[a?s%; v8])@~F*OLU޲c2e%h`1=B28Tw{g_,cߣ.4g8%H!t[z;q& ,97:x!cogb^Db0! <0&I>c= ٛptq~%3OQL2%-(&w;wo%e0{ۋ@  @ 5)^80hOxxj<4_]UxCk?dG_h}w:%Gލ/Y @** ᳨.ŶɊ!hfP? w%RGX`[3׺Z1#uORR8-sts% M%@@80a>x !zG:aGHe Y zou3qs+Uom;vyOD7x O"yFyS[l qccpr|^s^o'7ߪ;c$2`:~L.}lHCmځ0v Nyr OxppNMF 79#0tʭ|o~AfVRAp٦˧@,iE8Shq"ŵ@@kPk/K7ϩqN l>9b7KCXbA@B8Lt v+w-N Aj''wPaFMDnOjIm5gc<^w!M!! wOnelIa+LPOk^{y !AVC㓻r ߾HG_Fw"Ј8z (|xˎd 8(DSqGȦ gج+#; @0—ɉc,lQ E_+@ dT ׁc V[d[2PN&I+qphvÅ"@22#Sas"W1kw!wb}O{I%Űp@@ HUڡc&h|0~1AU @f.rgUT(mCO `!@_E.Q@:894% XR<oU'R  hs؉`LDN=aA\X C$_|?60?h_;H 1CK/qxӀcig 4`Zy Ca04Rd_ ZA4gec@8 ~Oz8$t ~}W\N3,RK, –[p!wo6 7Tח"^m7 {ی( [="_פܶh!@'X.8-`-ɓwqX rzzS:eU\dp5I"? Hp2eJgcv{8@O [qj"@B_sOfV@AhK3p׃|7Ziq =TKadhfb_AN,X|]̉wՒlks`:_,N F(q@*8aUp>8G=ItI"^LYFLSZqQ?'6(0 &TQrf?0 iQlW <0Gl7cä0  ֌> 49p 0^,&Ȟq~ 7]:yú.p<1>( ޼)֫|@ࣻړh Ӽ t5n#h)wpy?#c$pёoFM36۷0'f@ 4fl}aAbcz.DwiP!@;~u2Oxx BxQg: Zd8ӱY!0xɎ/9|a2n;sg='>ݒ{ W:@lHAKY,'g"ܱ;| g\7lS> d 9apsjϡF ']Jf9ydh߇k 7P%61V!دprk0X @@>Gƾ gyv@8g K.4p?3AMէrς@"84>]c[oh =s[ A\q@85cDp耒8G?J8G=f Fd`pjQ@8җ'1.ʼnm^#"B Zq-'@8B—w>n!]+ƀ$p6ǯ Qx6׽Nwogs_bV` 0Sto}@xJ|T l~)IC$$^3WL+y+K,n q `kt`+8G_]w)V4p)s E͑@B8|q !@@)h!@ft C.p` #Wv}H }hQ_x{7E[-, D@p |w0S=kw;_Iq#enx8y>2ڠ@@텣;wt ( @:04p*˩}v :Ka<H K$=ҌH "@:8Q@28G ƔQC-.8쓜^ ^J4 ʏ@ab#nT1mY=)aL 5Ap@ x44׻<320 rŬ~Klr ɗf4w|p |p ٷ@A ޕp;se;j5H@'Q4 34 dp"@0&s@\,Wn3:U3FN&]vy Bzh0:AeO?A23-~xTH!3ag ̎W!H^e"2v7=w™oCC~H={T3*@5Zw}OhG~d`PPG~R !0" -@'CNZh ! {MJlC=`8}X@=RpHX fڳ b6U_/r=q0.yje&`[h处`?^}=`vkb=N D6:Tp>ذ~}[~E$RzCdpR[p~PG<1G( u>G  )AI xV@@Q>FwQ@1ivo[B=$X!RD,wq?{AضpGw(fUbU!ۗAZxB9a0pp+`mRΞfC¦@q@0"s>n 0 n !iP%ZTp= H$@@+` " ;l9焀&|0}=GGps;= ͘n=0 (D:Jt!%= oO:1CeGU][,c9PY87itZ н,Ӽ-օLM9S2b{5\ 0^.D{IR+B,v/$QQT~IsV틷 l?Ln`7 1l9H(>'J,Us 0F 'LD jH+A m-eNtR)EHZWz$ bu`Bݍ4<Խ5JY"L~Ro̍j+ < 0d>BeK$K !O"A!њ/H+ Ԛ_MDr]jܶb۬b0K^:IDAݺcBb1KnвUZRpOܒn0W ;,Qd id;CL4 eaLh"0(ԕu"<=qe|SV7U-JB^\`3#\z:Ԭ %,΀_'0 J$A\JN¦0YWzv/XazOAOKlɭx:Y-k>d%FV"Zԯ 0V3WRJ*%DBr&)l=Cx@HajK"JwY{:jJԫeך@h_ 0FDx PDyZfht$ɻCieI\*ﱖY L"H)F}ܘM/s_=4-BWqOS ISn"0 *r&,uDBhy2I!t(p.Vo:j=zmOǍfjc{|W)P,?AxAw{`WoXNVy =ߎe-^IR- 9b~LC_b~K+Rnw_e]/wV26gWƥ")'`3ޛDro'/_Wnwo=˱[wȭ[{xo6тro’;hrM"+Z0O _%DK,`ɴ<tZ+L/" D0F~8Ă+|4w0|o,Sn[_v_ _C{?0e-$ o>\<5o_n~ww En .LX67稳[w}-o/o ݺtbS؋xV[-#{Œ|-\X=w(~K?EXhHJV pwGد—wDV+-_ KoL|wq[wxEwq[-vW{:ݹ|c;7AxRwwwwwwwȬV6,iP\u"@{?yppWNʒtt`۽,˖r* 5.nZ%@z XK+ww]ۻߖwJ-|)=wu1i;ᱛq"{(qX}hd BF] Yh Bma2˅8]GC܆)-虠g>2xXXmJYR9ndحs6{Ǽ]n+w|www}ⷾ7r]Kn}˅3=onQ-3Ds'|sh^%46OLKt8(X| AA9ݴr B "KMb/u;EO*Ӻgae{wo<'1VNػ9fό8+-+www2V{E+!@Kq[cŀZණ AukvW R+q[߃f z/$Y` ){lYA\n\B[n[p˻VǽW'  >wVa,]磽(,wq[\'Y0 qVwKow}iDVp(ww݄Ƿd Vޘwx@,q˓^@QvrwF l[q^;wwt—ώ*{gMVgC+www JV{@& w$@!BA)}ww޷ .wwYw]Em}1wgLSv%˂}J׀C/wMΦ>Tyowz$Lfn|+#G'|en+wwwek)pS ]ܩC ;>;_K߻9m]n+|$]s}2&x\n?0˻n+ww~ww{QqvR,4"@={cF?'%uK{޶4+{6>0Qww߁4>7/d+wq[Y1o~˿{ߊ;t~L|!]UWT;wwww~XZnq]2ⷷwqXJ&( v/A BA9KO[lV?q[K-'[nTmӌ48ܫx֘gD;CЧO&@ W]d1˄vvcjav7/}1__?{VO>6{xcCD")ei[cnX/3XD5˿d[C|kdج+' \V!/tw'7mv#ldLt 4_YaK߶ ^0T6sݻuxKwxܟޛj+'$tCJi?hhl6_C2 g [&c-  =7Ǐo=-~Or+ЈwJ% a=[僜=lWbR ` ۖ t-ǻ)'﷖2dAt|w.P XP{ V\-'M.J{m*58+ʑ- !{ϼ"- _2*lwP'L^zKq[xw_y6;Qq;,v2|) -KSMF!"vסm-î?—v) g[A2!"$~-{wm4$AwK}߅6[w9P[ir {o ^4vݴ`8eX;’耏fgK[-1XJAĮ[hO૱wobrے S"A7+?/hۻގ $~w,+ GN1 n\iF?C}0! AnCr HDa0nȆ[f)-blVQ:$@g&/c \2--e"X”)_\'>ʋ.]'y{iR׀{10G~XYȀl 49 4- wylC]#U.+ܴ+U8rN%o2tH v{miv7)wsX>%mݬNq&@ 7' 9;ج+|Kn @y cM0}nⱔ;‘!nb+sb_v+)ÁHP`8 v+wuc]phN bYcX߳{Im'"HXb̼ն6[ܼK"lb)] M]ߗpYTwF;ROqYX@ǂ;̀|>1RX$lcY b@Knf=l+ CpKlWF./<: B{b[KˋGx. dZ~Ԙ],LGp! R+n+V+. |w@ P :3\O`~VL+9apX8T)JK4h_8l)}aɜpۖobہ]`-E} O@ 66"2$$r9Y5m` kߑNׅ!>WĚ dB+'7p4dzW@?VtwyR+ 4As]Q+v>;s|PR\w{ގ40>@Vnwwwŋ# ۛ)q[V+V+|ZR+~9ܶ^0?:;j z\V#G@0!>[OfV+w*A5߁2bXVᐤV+b Vn+D~ݴ y~gݽAE0ߝ ݺfe B)~n!ȓ̰-0L$ Vn>; \+ewۊkGt@]fw`  'ޫGxT.[˖[q[bDR+}zK(~%\`| sO}x* Fg+z|#B#s㽻Bwpb˅2|Oo M8`Gw+@Iw<,V㽠bq[T"EEb'>ZVEr8MAA}K~  +w$wۖ+A X,˝@)V=pk(f[>QlnEbv+Oqowqgaw=(n wqX7 wyq[/]Sk<"b[DFsvOoQaI͚ r}( * /wRTw'}XR٩b ގAQxW7ݟ/bF+wV<LK ‘XV+4ưKaKX]bE֎@֌i >z[nK a"“"9zw"0 l =~N p+f+O*3)A1.(Wg` 7[T\ۿ|V+Vw"h@ 0V+w}pU^aS{ ꏎAf{# ]bهWxbI<1WsʼJA Ro0_KWwi-`X‘[n"wجVfحxXPR[-V+wq_b@}mdat zA8s\;ޯA|s0OcaYᜣW۫7O>5^0LУ1wu nڸSG#}j}c2WE ;hjcd5NOg\tnJ*9rch6w  H1mhp"}TJ q/6fGJb(#mv_:pʈU=o w{~ l3']48X&}cmջĭ18tgb~@C(=KM{dxdz@Ѽ-`%cfJ m4w,e qdgl2\OeԈ/4ߑNz X4EwXg9&v;`1}դQHdpb=DD{'`CF*;W'Zsw5@웫75 NQ݈.es_?!ك0.1^*40CmOd&4A]]Z\o% |kQCn8k A `!FTRa y|niw/OױB ͹*D\aV0nr;bI9htS[C <~{ș?lto:`YqV zkUoa&<m}p$A&hѝ߅-Qyy+GP䂑߂x ')Q:KEQGv~c? sH4a0ٰo8A.VxZ;MhS/|cEzTwᙙj0g;!ÚId=_^CS·DoM."K#z:[><\@?Vwؾj5ͺ2͸azgWvV )%~>#g ?La?JWl鎅;UL%<`%H!-%ClH-V j|;Q@X LUF a`&`mSO,3U{절TOHwApW cP[Z7g`KaxϾtbo;c^n fNf[x AW=<1.RPN'`_|haCTXa4d>g{::(IY>n6Lૻ  Տ4PBa~,m\L ;  Cl,iO?l$(|ʦp]&!QQ ? Ս|dyG~#鹓J{|t@4J3j8=ZAޡLq l;CG f ܢ`[]j^;I3/]IpD;je W8mxlgXYB_[7opQGz0([ׂ9i1^Ԩr4Rp֮|&)nq% jRܸ0_J)YlwZ;&T%&A(nݢGzewC]eviZ֨(tb}zk5h۲>˴ڜ;sYLؾҮ_NZQ߂Hq2tڪMUUvK|;CwLn[`?~~ȨPUo`XP)3j:(tA[ Os6:U(-˚8rr$l4}7Wz]U_Tw֨ _Y x "=1hx!>E :~j$-˗ m-&wGz@L;YGxhj֎넁G~{;G}JoM@s%SeQע֗{R'Q?8t^ .9:}1߂>G~&4>;[QST()3?߂ &X3l߁^q;?8 ÊS5-BBԵRDzuwi*/q-hnѭ:uᛘp,=g 0(3 Ph4F4|NBIed:_H }1cۯ7qR1 "u-)~0A7rZJtD|xDJt-P&(cF/F53PnOV@fqM̗=;& }߃8o0JUS "06*DAшS5u`JSIW`215ҝppu؜.~m4Sj5EtFxGQJ$D;hs΅yR.0 &C_:{+Nk )h#W5n-5wMdnfXZl8_X'~P,͋[aI 聞`ĞLf^at֧0 J5͡!zAZhk/Lc&ʥ*%;dJ.JBڊ%&&*X"I՘ހ7A-sx]p 0 tVuK@T@4NBE` L꽼"4rQ’w4ff[ 0J:JE),#:7c+fP]%6_f m6Iη3?D;jcX|߳|Ib1VJ}1gf/~@\'c@ LycԘh ''c>gUfIf #AmK/ 见3iPi]^_ ~2('*Q;@m>Is)j*~fV}H/;C0;ٿ1Wg)w8_@[VȂ@Sh^*#<|&OE=OSv}2+TѸR6+KA"A(tgIꩉxjS iKeԖR(vsܱ "UY.`hU{"IY4K ~sO2 uO 8[PMCcB7GT잗4;3˗}]u:׍| }!w8gJe2,qDaDn\iK'WWW+e +oWY=' 6*>7E_񓞎7哤dZa՟@K( MphM1$dv)"o)]{vSoZU7ԟJW^zs94]U UnCaX|yFh2Cr]'Ӄ2tȔW Be 'kDF d:3{u znz mhk$1/r[[L׌Z Ր᥿HM; v0y)jy%={q|o3yrv@xӹ/)1m|~q9 ZxSj@Vo*o9n)H^ wOщpKo^uV fФun;}38E7uW& !L*_w:, nziد7R0]JAD|G[35u!:G)OmxzWsNTnX9cOV(+^߾ᗘ.`ngf.zE~i/?o#kt~AW$>TkqOxvNJUY^OvhU|~?ÊwDzI(T9>M,L/`I%),;߳9p)4#B޾;m/Ji%muF(4!߂Ir*y79& T0w(yr{-L, Npї[w_}bnĜpH e^ [33;_NZ^'MXzGlNeKk~S],'gtOG~Ml_A`:MOy唐L@܃Ux9aQ/Xk[/W^/A;x k܁?㠵5i8Ak [n?\4۟e&Nz}~~') u)'9cdVZP lS˞el}/ ,3n$L~w{P@tv@|zεYP XCUe g|gN^,\FR#MWkͿ}oE5z$I~v\7OÑms0E ﴰ/%{zjN~z[w[E7֎eWZ;Gy ` r\]~u@?_$]r_IOIkZ߂-wn;UkWP\}wRTj]d ={#Ssb>:>־>4g ¿+CXc-Tf2ėI_2ۦ޾+XC{[ypx[em%[_Lw*A/c|8sGl;n_4Ò%snn :H|w>;Mq>$bC *f@t=.2xv|b:#3?x;VB?Wl2~ZPs[sA N-ǥ/k4 5jbֺae^>bP -ٷఖwo#ڶϚ+hpxy _鍊}ԫj .RB(_tG~Wxwj6=l}2ˤT4c{c;ASeC1 ɳcE%YLpﺸVܼ؛I}*r,]:l׋[тcz%NbijGƺ |oS_psp0~gWl|[]+CeO 2ƙ+?`Lь4ėMEx 햒 C~Ծn>M!FYh ݛ:߿YXqЍG!}Սk + eYpu ;Ⱊ\cNn},=Ұ ;߇{o!Ag˗|} x䈉Vٺ?oo"TH%B[{qcd<6ࡒˎI2+)##? l ;+eofsP w&ܐ8GL$X۸T?EF`ƨ 9hjY3N3E-*J Ǟ?სh!#Lib:, !-j:9U(`Q5މ2S#H!p4ѕUM:/I 8E G @spk)3gU (hſF39"o8O!{%ް?LJ+dlo¨}^u'}@v$Js H; 5h,3_Íl ؏6ZB T }蕸G!Y,#y1;Ðn@0[s =\2hw>=#q:aŲjQH?74׉_ݏ=ܕ !5RWL w9l ǤLO՜)56p]),n`V,?^UX]?m|ac2qޟIJM,yQ-hޖx@aM3fKo@>-x46ƒu\q\rۆPVV~+=w_z ޲{ 6UI^2GkvpDp:i*KLXK(] + ?K0*0`>'+vd\V[ϙfx= c-*z~ `ٜBz*ⷼb`F Kk$  Q`1aCw~21XV(حb#p*\z Upq;/+ܸ|=ȸ҅!gGݹW&@nXV!lV+߶JAM-*Wv+s"#eypG1a)]t匤L]8y}K-kvwNOr% ia~PGa'Ta&h!Fc_شa[ f|ڗxC c LrH (-G o_AmK7+n3 ;"Dow_|njmr1 Hy=YϪ(u(/}*:}$`+][V#‘F+֒b^fg(%Vx1X݉it;d,BbG.;|)n+YWe[|uH\-Mr1CNO $= jǖۭ7SR]kJYml.0qntV[P6]wϙ{ ]q[-sTcH>Ķ[ $[AK"qp^PX@}}_2^?O=,G RHVr .! A1XDrq!Ht ܹWONpb<{t}pZ-{+2W}5"'W/ȁn_X)wv`> qBX`/2mRMww•9?.nⰲ[[REBo+.W|O0!9h`v7P4D8?WG"xܖfv`i\<VL񅷐h5z;Vz/uC4Pe5$&vepQ8H .҂(,t۽md R(nX;V+q[pwz &YCIBYlؗ[oݻH4{~ʟ?0pXV^l,,0PeIV52qV ^Z /w rd{3xj1yex5_ d8`=5"| {`|q;W?sFyl[3=`c`].Vۍ&› 4bwƱ#UfV~xRXYlVS.Y8+}ž+.H4ҽgmt.۾qj w9DToXhcGaKحVp,u ]9Q@m^f#n0jx|{uLV&ڧY#ͅίaܦ• Q*6?A 0`98z-{Dmx E5&"'3oF+ylRY_w(1ۃp+7v(Rؗq_pv#/P2jU[,d慳XV+mYpe^R"Xe*Xm[-߸b .owpnp{ ßpQOW;v ]U718hK ?6OlO0V>~2 ,qTT_~4@'$=ws}~fB#A-ۻ.w91F%> iB'WmDf]n_(Sb? {nEKi)eEeԏnnD#k] n[LV! d^&mtS[P IA Nop Zf e# ޣAo 5GR#H:l೅\80Ta׋7RU vPL_@(H0D'69<גc3}SO\!`"ޗM_lgr,~P1JΤQfCC >eF57\&(ɂpsIr6zڃmſG]L鐏B>HzDJ֮mݛ_x<ߕ8!zٓ/"*Ѐ(ۨ3[l UϞL`o }.+;?zny8,exav e"w3n\&3|#{+wNI6_2YT Fer z>CFl4?Ò1Qr-iv4j:OJ0)MO|pm]ooM73Yؓe@kMq9?Ji8Rwž۠Ex%V۫8%IwXb}HI 7~S< Mנ$Wœt.;+4ҏdR^H5@vK7ww 7b{xUkw ^Jw<8]B?}]5O``8f{W8iI=:xq_c`1W./ /|tI'~?o{|dy$O](`sל|(!_۔ʑ”ZciF.zwwtoϟȱHJT80I6 o*6^DZjٖRL  E*۵h3(&)W9Ro+67@6{@Z}$?-k_/BpT|x8ϽBv?l-Po`&58 {aԝ 2}Aec% `5<@_ r)VG bq_ק.YX@qݾ zNx0Rgש=?8@IuhlۊgI0{oO7C|\ =ὼJ^}Td a͸}j@U;wPXo^8,c2u+:fVZ%O="*jTb3 0N*u D%꠭t1ԭL[' +Yp!voDSJr\k" hf~z&J+BL]ʅ<0HFTD'H=/EUiַ\b+ u1II )߄h7`%r 00Ѫ)."bslbĆz0,N*$RRB=ɤZCr Q0̌Kֱ2@c$\C4)%'=kMwRb֩ 0DNFJ{ZIwIbJ:(FTyl!m+@}8"j`?{Thk;6N˜d:$~+Jp@8 0N%]je-BΦ|@TwuOO9eY&8l MõKQ]q5SsAE n9@!,0̈8#U~%ɾtQ ׈ uiH}鎂H0jwU(N8+\/]#;۠C%ww{wK> '{2/zkET<_f4X3]U f쓫(wMTT 8o, GiSz /oZv}!W=]Phe=};3V&$%l-| ֗ *c̑l7VDI\})x{"7w߸:YqY fbl}޾_ZwC'UM2dQr3r#( ݖ>˾LD9Qj@nz#KRO/Inw]kD{曹s# $t:X\T_ዽ-PY[u4mI\&wq[/w2u8#h8kw}u_7I7\\8Tk^[֩"nw{~|Fvl{;!/˟͘_ZD^cww6A ^!o9>}P1^q#'2Vr8c]#|m.(ȝ ]geǐeL񇭁]Q@3kH]q# 'Éަڍ@V1i\#h͏aS(Zn !YqcJl]D\{}q,y=/V U`825mޟ;]N 3”!/+ 7FEfSzh1vl ԯt} 0կ0]> aش6mRNo$;|3# H6C %FeO[XaMP!$%R ?E WxUQcq q)WVaZb/1xJ>^4}Mzi`g_.ѢkR" #tt-B SD>ZOIH•jf:3zp0W&c1g*j{͜?N>C |/Qn00 }CnNZ<`jO̹v;$#g?[MsL1PHm^ VJ|B_{>{^^"߫8Z$=#{WkPp2@5}CR~H{aIMp\q[݌Kx7O`v?)qODuBFU8qv?]c [4઩N&7_ E#|?B cC/ K3I"}&)+Ywgv\9zM/wv"}rf`|&$r|=p#1-o- 6 X_ m9+4]O)Jɧ5] o3\m3j7#KMũYK[ 9<'YcrMyqêE5hd7 m[Z($ӹ7MbZ`loV7w9oKDz҂ <2z{ӗ]meXe:s9׽Tm1..2]||xRF|w[k76%1S?r`v xW K69}O]=}k Lv˻0ۥw `XmwoH}?pv%ƚٗ])1&-61V+,zC}*'q,< !p?9v0^Eeo֜uTbօw9.[P3ajY8u7w2_W]ʋh{\}ࢂI5;|h,w{vh-ң5~ {ؚisY v0%,j 1' AQnm|}Mebn2{)r lnC*Y{۽){!m#bLI:q]{2>"m—wܹgvIC K\!/|[&ܹQ~lY@ yI6ح[sGv{$n/ S{Vq 㲺WAGo'4E'RP6Rdh 8Mm5Gdrb6vD ws ۻ^ψXA94|:w G`] ɅWb̈mh7m t-yϸ3e'{> FulJaNH*wR]fJ͊~ 7a좯V^&mJ<%ϴ>}x߆ݱ?'qxÅBxcqAw%7r;d}Hml KOQ Y{{1'rB}ژǍ6S݁&fxēB/UzO"pfn]#-Xeza(~7A+5XZ#$ggL W+ "ު^IeIE Z,Rc0S!Z)v{d,ͅ/Kx{;=I:<߉Sg$9Be5%\6(S#2_` h8Rf{}}&W1_F _j k̖w/6YMA#oP@A >6tQjc`Em_4ܨw|V2G_3u^1Shd5.8Bs>5)E L~71Y$$`Tz~ڡXӌB4ni ^&ٰA2d%߸&>Uif>r( ™\S:h5-6唐(4l t]Bu Fz:L3`B t 5PvߙGWgGPnF}BT+JbTΘ?bNhMڳ})U0WqUh/Mg:a!Q\#}k7-'niWNvtcCN\N qGϮ ҝ,ү#\9Rb8:ÈTFP|aU Qa^o@"l{A(@|17{4Bj>"Chbo'fW Keo C7ދ-ߡ.^{4Q191he.hc{%iJ,n*x}ѴLK_7-!iog!Y{൜dkhn?O0 f_6HAQűhc>oA8p S|1g [AeM=:Jcj⎋l8V?8ܭY|h%ù5ZM:~^^dtr6&6SHUw@-\& oCב3KA9-+בic0оg_`x q2p+KĒQ{f2yMݨܸ _Z1+Gc9ig=JJ6ft4an@BU!7.ewQOpU⻻i-[E{w;eݥO2|dΚ!|q yW,)ROw@+DmPV0_+b4Ex~[l aƔ;M/7A *ZF݋zFn.M>uڵ.6mq}U6u ǖNA֑5beo4/@U;يemJ? $<ϳ[6u%z}``~u6;^'4<\G<Z f?KߌVX{uwbBʟwzUM -_ٖ!!|fLzJ3q3c|)qANOKKQTwv #? $ld0Xp #a޶s Tި?KMrZ)Uoz %k@u̎ܰ6v1xe%aɠ(?ðV9K2{H] s#"}x3" e Ǝ6Ɔ6t2LA dt!'n y9)wwAϡX"n{߸&r^5Y)mbuO)b{0G~iPUVzP( \󨄬14[*>?_ y GaH@kH5I C@ɺ/Q<^|eVÏoIЮ! 6*8Eț5e[(~n_ K}o/MϞO띂t+ T FE|rY4xkA <Q+A3hw0!hQDͷo0O+_KKu64>'—/ݬckvlÛ඘e?i׻ ;Zm&ezt^x\{ B:titX1a~HiݔWyz'LS wcT߯8%ˑ F+ nIr.Zn<qDELw(/= dkGLtHx&4>0Z58z#}(_M[ li&v6 Vտۣ_,N›iи݆171|v*eq&0QO߈MR_ l}酕V|xd5fύᝡMBV`/i,&sOId@q ֲм+kWWrv+]xưG܏yzꚸVߔmb_o+ajdp~,=wx,v6i&@Y}GW4PsJxUtN?);{C{r#˚ epC} auFM&"jȿqKFkƞc&3A^̝"+@1q8 B3VJB?vibÿw8v [~^ Yshоwb U8 I~| _AW 0F*Dү.T*A=Z?_2=Q6KqY;kuq3,bYjѴ `'G,<Cw.߿ 0V/-{9*'U(V3 lpkzGr ȳJ"E!Iw6a/襪'DƘ' 9r0d֫d, 0 #FҸdА{G N9~GͻՔk& [ z3baU$[BM13$$+Ylƾ"0 D7-1%"ڥAZcK^g.*"VQ $:0:@;H$bR b*9^cBf5 aK 01D]H"TMM`35.퐟7.(2©(j!EՅ9RMXVFT2xPJ"#BK7G"0 L/uQ*OZ֦ g<&#jg&%-H&d6%/W 4I!s?7=pNH8 0lnBFEH"WImIX@ĦVT=T.ɏ#_'@>W~qeE|梩iHe 0h]"˄D٧C?_xG⍿PTR9\QʍK*㉯ˢE* #{{(]Q)"P<5gR 0l %]j* !B˾NU<s+yVs W"rBQ^-YN mN L= "pͿ]df6[bp0̭A׍DyiDܩ)tiN̗TJlȌ[EФUCؕR "ë$<ve4S4P:q}q$Ce$~sѯ X{yHyQ_ e+vWh~l|r)G]:w{VQc;Bg~ýIk]oёvp&3MzDwm m|zGbua>HڀD ' ym/ռm/|x) :Bss}j>~)13~ N|98|i'!mPhl97v7}c$%B`C.0(_66\2\vaQ:T B fj=b50+)t;[<+zޑQ"zF jm.7 1?D~3v8K. l4qY_ֿI:hnzg!VC&^)&DG}g/ Y^PSv4wL37iuf< *3?_)?\74Pi'o@]hQ5om0'yS/h0ChPczO8ծvO o, bќ(\ ??gpzfN Ȇƀ@Mm:U˜6.|?c_Eq|ROx_Lg 7} uw=Ϣ`unrxx3L4j^~XvF{f _z!>u.}O7vn)"6=In6`m F~BWp]k[N陷T~l=9; UX0|.:0l/HTǞrz+5OOJ{N͏X?ш4|)EC$3ua[hЬ*rbu$AO@?܅==}-J@Vfq}}Y?)Mo6x8/ hR4X=Ws`ys۶FzuU }]ZmfIUwNap(> \2Z\@iomOOZ.}#P6{D9c8+A.ۿTδ9ȇP !.uȎ,.-E밧^˜^4`n}'4g]{iӀ_/Ƀey\NOD}M קg ٟJ`H4U4Y-=(t qqFnMi-_y,mKji&MۙKa4Hlo`Zd˻5j5?Ŏ8בw4q!Uohwh>p=Dm4m1/>YY_ے кMZlpWj.~dS+cT00{krHMo*ӌW&wlz F@>^ohl> ™xJJץaowf=楚gʼk9~j~Xm9~{F^v8F\S@΀,%`NYc $_ji %.;lZ4{;Nn@134`>S\fm=7d^`_аfd~E1z"ςg3@Eƺ'q'$x 17Ѐ MLpm/ϫ>uWVsWS zڦRǨWHvS (rmw6g`}C/^𐻡%2r|6!F?SdB;8 A'RI)#G L錜P}]ݯ H6g/pd_=p ,V'(a*QS,eOlI `qku6:NջSG՜u Vw)y,ж#_N Yaޞ cH&Fg`SM@/DzkyXd*m#S|P<"hBq< XAW;P$=MmF"=^|ET|)D+)?Ѳ^Y|H .S+Λ=ؚ-Z3~F#ωJV!I*A]$( nr6fUֈXt> z5aیE/{C \]` }|;;̛5ͦ$DᏓ‰E)H`-FlW<V-;7QiO _Rwÿa8R>00zRA.<"Cag|7L`7uj.i-Ok~R`{~]-uHou\| eǻ ˗y|;^odI]pDW;)Whwԙ3 ꄊiqm[}Aϻ?@dLP>us_C+ XQeK,ZD\,wA}NRZju_ZSs书%ñQ)owcC2ZU,=S˚f\gc&.s>K o1>F.z3._o~,{Է)Q-0q<bQ;]ER&lӮo.$R?mZD{+")A3o-Tmjgv>FSEI粆L}DػdQK׾.oKݴb*W&:Ua; ?J>`ؕۼ4?o0΄"0(#D3[#:7& {cuށpyV/Ϳ#qAK ~_vW :f[(~ ,9z`ŕJl<oo񔈥~~BQ}p@<l&8h+,(kׂ"o 6W-_T%' K8D1j_<12?#x^(F܍o?.#D}}~::P'Mw-2AEYV ] EU 0:#@bClO뀃] h#K @[U6/Ύq[G9kƌk1/a$cD6r];vQƛx ^Ŀ Cn&^$>ANQ5/ ' b'pH kXoG>Op + ^a!y9 ߳0Yw2 \Tf{욠ѐRM2~ӿꃅ*ǩ9@%Znr.]75rpeR>C$>=N>Čy:T4߱u+^u~}u 1 3~OyVȷPlLw%w謰RE kb(1Jh',la4ѯ3:7!/Ă#x170<=TOUD7  $A?ˌC1R%cQC^c]FMZ5\4 ^`5lnmʋKdDZ v;+h[@Е2-!O~‘X,e-u`ˊ4Bwc?{a/]No@*7J}a8xĸ pLV)J8b_%сrr+X`ZR[{y7{ELr i@kMp|P<2"mr=4O=3xJڣtc#}Z!^־qQVPۅpq kMg c[naQ}Ayѥ\.C%,μ} %{ph'dJ‘/Htjh7B[%&/(5E Gx=@c’[-0_p~Ԏ"z1?U?YIz!f^c’m#륰;v*S8տj9G%tQ#5-ӀKW=zn+WxE6)đ̓ 1ߍ#y CbbB F,^Ї_i7 EZsM 13|Qݿ{l5 wt[v-Ɓ?q[xcłtcv46:sOxMƘeB+\ 1Xڽ.l }y$JcAqτ01^HN5S"mg^8kgQSRqk|LH\;Aq{%%?F8)ZXbS.Y1;[5 ۩n'y֮࢏t^ѫaxX1fr– E^]b;cl|>B|~KC=7`v1ߍ3^xyH{,gm*SϚǎkaf6f{|aG{#$ \$>Q& W0Ų=YjL!0vg# ~wJ|,#E{{`cLVZK־-<ڴ~[ob9!JA#G^ J\N##P~& `Nj-|xWgh4g>q;W>P”U tQ衉nυEp cV1S7}XtW?w%T&>&""<_ dMİV$-Yl}jp1Dz*[H"и2`Ceح w@RZ+w. pX{t!X8)AV(bvvH 2QV峞+,e1~PccVXUv[׃Ir-wAG bS/~PlĂ[,gV[-Ġ%\aG:8a`fPF1m#;{NzNP&%m 7`@Œ.m)[66w&v wnL&;?*hZ (wm=oZYƼXz3exOAFPm5=YY 3(I|'ff@e}g_k߄\9PDW ]X@E `t7g6mg'.%{I83%¶1Vo-aI[f0KoVդ2!L,bۖڠ ew?LVq ep +s78V!YlC,VU85"lJsvʣw(cLV+=cⳅw#獫ެ?ۂtq3t/ q FQ;@$w8cՖjvؒDx=xL h R8TBfXõB``Sjn+{qjMj=fpō+k!I@1}!dxitMC;Xb:DŽ[,c@1ܞ:kZHm:XA5;Hם RMu{BtJ e:և{QJkb!%h0pjŠSO,ꠍ*Ort 9>8mVteV%1Nw)bV!ۻl*pWwwq[` n mEʯ]0 ͤn ,ɌI0ًg6O G Kg0gHe@+˃ _bX mт;q= NlƊR3Z02oID6+Üsbmv[ج q ՘a,v ] xGF ^Kt}EB?F_@gR?moۀ_rؙPY^KeL7.q(`9G'9Wxg| ^İYh+Yv{"BLj`7*+e\XxKEKȶ[ڐVsb`)+RxBج$y4QN0[VeLV+i"TA @$a;WC%GEF$mdat0KUHU=qhL~dөK1Kȿk:ͨ֌Jp|$}pz~.G(bD2 32O@fP? 0." K N$ Ъi:*egwժYGeJmgoe!Cqx)s}gD{"#/b-ck2p0JTWR%Ju%b!ȉ OjthV. fz&hs.!$ΓҨ!)4(ik0.#^K3HB=">ǝf.A0GH6x7^&NS eK(>:ENON3K [,oTJF:}v0K\.kzJAWZŸ±^֤piָLFtl+k4KwlW .w0.`CogAPy-[cZ( FldHqCmNR* )l7w{lj8z'"S~"N<2l8hˎaŊ 5< 3xZ~< .Mhn+D;@h%&WnY)d嬡keBl0Ufݠq$`H+;Ha]ѹRV4#xZxph7F൰2=H*XL]pܽtKS%^>GQ)0k6}p@ >@T|q@NdcBAI_nO++<-6rd-4m:V=6AE~m McY\lѶiN|@2\@iUzç8NF,|Y\{KqU_ݲZ x\0=d#p= Y}CnWzzڵ>+K&1:O][8&1lsTϋ9,v%+#}BF \gc wJ$/6ʐ!ƚlS7.#mo(h_Egׂ9- SHwc M'2:nӚUVwuWAh!!c6%xSwtxMՏHxJ@[$}jhԦaXf7l+_%Um|x`|UC}-(t8IWZQ?W\μIn%,>'YU+Q1?#ԣ G6W"uM*5y,WNV%ԏ  H[ʴ>كPm` 19픦d;C#Zoߙ1A?KnO~|ᔔռiwzOkUYʛ?_{|)|,$,-.6q{-O77_>_xwEI4% RxAfAPU4?TJj!7LZbP`]>xFns &)85ÛX 7wNZ@pY"3F7׍ˆjteM?!wUw4~K{i9! ;A6[lgD+zPǗXhkP$/ނpςM8"q)G_Y;`Ae##@j%{V.8{j7 G}{p)O~AVo e~}}/Ш,S)? fDB~ȑצ|n@<_`15gEpR8)gǀN|w}Odw;<d"АR`5c]KbqF-p\gtn#9K).BHXpR5IFiu` nK9{`  ܡUoU?ww#DZؿ1?n u Fex 8Z aD,ګaRzצD;Yc»IDy),XL? "!owpG*5Xb8ޥy v(I68ˀ# Ž<@F ,;m!f1ݿILAl=ʧB< {/>EI|0p#QɺrFf:"%ma!'K:{_[4wu\9 ep$y. /! lr^ ߒlSvG}?`_&K*h֜ߎV LWA;w *\o[y-X w 4d "c#1j~iDc[pF1ߨN;K&0rXxi$ 67JKUE~k`!(o=f\ s|pg屷-+;G^? =1)ƝG2Uj- nTpmG@IIF k]A8q~C9F,NV)D Ut=C_W=t!!w(B<-ޓ?8seK``]7=7`Isp/Y;s&ј YsMXP677μ40 d.\.ffn zWGR?rρ6;+Uh9G -l%Sg\wM \x1m#8*Mqȏ;zpU\p 傻O lVqfmaㅍ۝n !>ɎyXMBN=lQH~eu#I[hҟe8 !_ )8=?+y"`d q87LMc`7˗> v}n1h@o%po(%F aP0Y^Uw( .!w8O `?\É1߂=m`Y<8?۪Br #h!YjA No}GoEe;f7= iF;Dп5B$IW'⼽SQcœh(Z* Xc@ZD29<yO.ȡG*S}zṾ*&?N[ZX?|ܜ_3;MhXʴ:3}2h٩JYMN<9q(V >BM3&vPZIlE55#d3"OKq Hz#b>e w__Ix?UR+> m=8u*oCU W W L@5O YSHYfFrBW leq| O=!*'0C̨^qk=H?!pOv>K{ !(`ZYK+ҴKtXٔ_w >Z]dB+h˱(ΏE!0\[VNji激OLnxH6ZI`:L%aKd1T*;|~6A"b|U~$.^'+@cQXzy߾ t1`)@s׾jꐵ׫n7EMn2omx# }9^#4Yu,Hg᠙44c*^XK˟qi=..*7CAa0^ ο{ h~x_mjore{n: a% V!.?џc,JzqІkx8g\fA[@mH]|E1u26``|=_ey6aY};h5L-5a2J`hFT@]=M4Jej @M ¥ ldn$_Z [oGv`s1PDS{=|R$BfZ)A.BVlz zj9fn[Aj-}` qy c|rw;w|Kݯ}ώ! d/3<M=|hv6}\ZQ4_aLA?Nc{3|)d[anی)Bws|m &wy1->< 0]HuwTH=r;gd&0Wlؖ0U8js'J$aWq=zҞ+Lof1ժ 0:@ UPU"|sS9%A ,,YfMHJ܏,MV}ȗ&oߍ4 0LDB!&IFN6RC|O xkJ%띌tQs]l)U4T4PCeBJɦ!`U枵 0 AyHDTjV ^5IN ED6o}[ʠeA+TRf5HgN@;z-&jFȔ%,70 Bh΢ Kǐm?,0;4J7Z X"J0,Kg+"J$&1'Lt8";+E6| 0.âނIDTAMjk|l߫/THzU-Mv m)ҔG"Fr$v+H?WY=4|AK_l 0HV D藻TjiKn^Jƃk,MVHc~j7 rҨW#t ZJAqd Z!ImᯄP&Єv"ȷkoڎ 00ycY*A% `DԒAܽr { ŀإVGBx?]\Rm#rjG<b ?A#*i딼(\"0ȵ6yiQ%A*=&{sZ. /H.ws.y#huB{*\``\UYy]InsBwU:KapZ0ȭ@yhL%=kYx{FImQ @adR娤Rsg w_P  \7̠'_TOepA4hޏLԀ;z{AkKo|؂p{,zd-ܿ#) ߖ!^c`HR1^){خlU[EG>exBYQ\('K~eՠC\eY0c8|F9U,ERNݛ2YA|kpo HQ$[Qx+`R46 ~zf.ncNzw`cU 68Go2 _ z]#6a2Z10ppZ$ hWTu9:+ D|%$\=X@?B.!ucStόa//'B AfFZW4 ƞi<O,{e]]*>􉯸tV؞[,qPnK |3x51T|m֌<7Bwk،4T>a:   8_A6#n7 h( l&1K3jzT@ mYT v?rcOd1ET4YCp!CU΁ڂ EvJs4juC (իR|?Gz~6Sf#K%qs*?m /=Qըp?éH/@9qQF}?/s3#ֆJfף 6b \V$"fjkt&IQ!f[.U<Jޫ1"zK*I!K?t( mɍl~61Ю\V>hsٮ7 (o&a5&S=Q/_F+hx Βooxz l'P!_ih*͖JޫnZzZW`na2l m\pF~?+ l񀻡_df^ K) #a<8q6}Z.F `q}O}_8? EzF?E] ao@xa(l 7;^m"/ms Xكeܢ?5((L"~>NjjSc܇SLeq *.A*߀+ /TLALrmQc~ ^Ixk+LANЂ]Z{A`Ogm|ýpwi[^mtAA! I ~' `=x?&-)!o- z'~@AeχoR d }TlAz'kAM--6mU`x q .T\c3'ž!maToiD=0 u^CsZ6WNFm@oĩ4hώب,1"4YP.6!ĭ(IfKF R4ciE%U}p1:opܦFw# jA/n84pG⩌ _70x/ [Xm䌖 8q/;%_,HMpZ`_^I74f}T9LG`W ;)iI˄2hJP5F ÈG&@FfX Ǯ@oo@>*2ZRyN2Yv5bF‘3=]Br3?)JTM3Nٷzz,%|7x@cЪ\i *p{ _.peP0 {=ior;ͿYfVIԝҹL?d5<)1%91\Nq|m@ okҧ;b`f˵P~L|j<, {f!!p)mi>,(Jf>2)Y~7O5ۅS'Ԟ/ ܴUB{3|n^ula2;ȢtqW8q3𧄑j,lO$xZͺxw:3;1r [b[ jI&Kea.9u@`Nsa]A<,Pd5 8tgd۾Ѱ H;4`Q7؊~~pjj7K2swajNN:D4ȀqLx񛚗QKXR7fﬞ3. 6w}sJs rO&ʠQNՆmg#:z8|9x8QS%a;JΝAۘK5j1; }/w&sBx;rB?>kD*a *_i\4k|W^*$N8vڰ@Y,"av{n %Nfn-@V/&g<25S7N0E()<[BBcܱwLK @nV4E۰` 3R)8 &8gDJ \WM1L@Ɨv7|O[8*~R"7X̔ 9XV)*F@_SñZ:a1R]_G%'pRܖ"AUʒޣrT%S2|"[]&$ ` sf#.l5 }V\M4f{>> nCܹN\h88V?'I2oZa8a.HVr?]=Q߯qZ"Kɀ|%]&gWyUI!vc薤D:ϛ)&t"UôaWb_^ĸЁEmj#[|LVӠ &H'o tܴ7!rAVZ>%s1>3iAm2R&43̎Bw*[|l f2xHe78>6LkvTQSGHdؠrҪHz=6✋otmqo+][iS/hMٗ,viO.oj~Zz&w“bizMlCl)trym/r=(g|V@`oTZ1mre^OPص AE䲿X>O8!k\ LnNP*F: HD>5BP>lMI*,q@h?Z" -[hv;\f>{n*)\))í/q^7WHRa.mt[D?k}\]-y3SpS-Cܹ˘no|)BvCy?HYp]2:RAq[^~;BbٕOl%vfIM^o#jz "PGD qxf7:1\FM`qa;-Jt)[/l&΂@@oێ>}_6nlz}2zD(B Alo~R,MEh q&! >A[!5Nь7,p>n!:c>r[2'v6x'E s֙^%Dzؔ~r84n޷.'i>Y s{D>(P2Ԓ~%#!C"R>NPh])(]?4m_ަ`xFkӪQH:DZ Mk/`:9W*6sk򫈆È.`6i9[7|6G~Ÿ5o\_h5C#KlWwփ.;! 5qj8WK[*rw1 -:Th?hoϾbUC܃U&a~yC`!6*8c!˻JÑ1QH/X59>7d ͯe1 IqFhwT]6pDX/S}T-fꄖy&GYxjcmzwu7W\b=]h2 Pht4̆c0?z,ߊcLHD.q޺]BXŜ?IwC Nziׇl3M §la[ ,8ё7F#I>lyJN^OI8(.6a#c$73RTe|&=Kpl?c3KeShG&sْ+AuD#R4"^69YeA0z If4˩!2lV3eȾC9yX?80UYs)bG$B_O"6wk;pe =62o \;Ej^*M@K{G)xͨ%.rQѻU:j߅/d>PF5҃X ܐ܈!D^gi#MQDQF.#8q"8k Ձ@t7)x@.݀܊_CGbH#WXTnF {5Ӧ9z11P9;`[Pm$ʗ%?>21ԂAF+.w D}'4h4XMHPR0Jj = YxЛ[TK[YV3YXf;|l`6ĵ6ѺJ94awC0BlIikv%[ p^*\ɫ?|M7D0"M1]#0N&MָOfpXsI&lCz1!fS< 80_栤e ]}N`G|nN='b-ngYxBB@]-]wL iਸ1^*'M!̨ ?|TBKè <AQ O84 3<Mh ˊb0@!>;K]VARi&X=paF:VpIfLT*;@_k]-nm5+*O"lNwD7ѭ/\@G}l,JX@+pPT"g#!Wё]Cě aXA#J-+U񼐪4 : ]5h߄z{Vk(I#WCU 0@p]Fnacpw0]}MNy8.30?O <y/Kz1/;z6juJL*;;I"~VQ<#MkY7咎 CN,i q]棍O\AOx ]8kM#qF &b*m3r“. hK c##BW'h Cn/Owxy 悟~\\tlri{Eh07l|z# %}6@S7o^~ANl5cޱ \6mtӣvT,;J7`.8ދڸSe] ۶R%͜Sv#%SWN4SZv0CR‘]ڍ@W*~!ƛ-P#GPՌd0HԈ#NC,6@>O55On!PU]-Iwr.򤟊ۍ0l' G(`;b`BP.Pc‘@;nIcTiQ5Vdm U)أAm 6|R$\l#j"?NFKJM0)M8߂oBE| jVaqP>vfP&~ 3/% &і4W ^0o>5sЩ"{Iv  f87[̓sf #$Q ] "Ǽm3g t6wcNy<= $5C@,: ,4 tMFbsaT j48˴pcNrOhЋ% xSsoEy8}Lfا`$SOt6\۔  s 0c PVQ;ھiJB !3-^~8?!6MEkAi26LX&92/va1p>Þh 6A)O9NpEa9jш;|=;8I+ߤ ]U1ۦ $]#Dw.DY&s6ߊwQf87L/l8ςfԂ7pIq~d,eZVf!TkmQ=3xAA[Me Vjkʂ > "r^img{ȇo/rt=pd?Hk<-??cr<@pB$'A> #H)2Cv<uZZl/":%zv_1eatCБj G!ex|C[HWoHc<.2lwd9̯+[G}NLeZq̍?0GAM+W i1ˆ-"8 iaFV p 2%*5EQ+ %qBD}1B{,${ܸ&<@E0? isps_R Up(g/aC0:55s;]I{&Tχ{L&PT jk8mXA fa @`z4f^s-.EewO-Y_xX R<9+cJ$=$HAqs~0깫/$-Yޫ6łMS H xZ8_8zFoSPxf4&9xOɕ\2יj_ ٻS khR OyD"Ѱ1>>%B̓݀AxXU/5hM=\M'`{ju `|v{uw!dg'qxsvkU%^JՔ밦^8حJ<{ȽD=10eZ0%l cGFVK~-CJ}? H`,t Q!*w @ us>jn{$NnSP$Ҫ2~`ߨ*i9+!CiM[h;RGb4U 'ִ`G?z3=z?zX p>m~A2z8zǸU5p|M.G • v8<%`1 Tu'9t>4MSsQQciX7,+d.s{3/٫n.BC쾿c;Ao=I uA\oW7џR3*qNYP ~/< p*=0hcWE&!rD;0 ,tb;tnZFNONۧjsO`wKAռh2۹jDQF_2S}}*1(א|) ծ/' t)x-- pJܿ'`G3)!3Y+q$=s(DmJTakvS~P1ςu5^2p;~79 F AOl> CEe9q>A^7w *5+9#zr '@f8)B$=pJ>5kuhЈ+mHS,4~{p ל840{yxR ^H}~9rїv޿ a>}ࣿXm7)pOP@(Dk Oym_>8UFhCFpEjcbQۃ?m1 98(#umHM\ԛL\9&_B~F{HVfOAޭC/*d%l{5T4i X=FAvWӗ]>6"ES: |DfdDg?QaeW&P0w`c+W }2^6Ηq yKM=݂|eEqdbpmѯl$o4yJĸ#=YSV+6&wr9KU\auq>q )FڄW.=fj'Ӊ#\HF5$i__twSl-.-;/dIf2zEV&7 G;5=>OS@d: K T[ʀr[Lƹ?H5:Sz-ẶC5, GMn7O "F*&:sINІ. ;'dx:Ʋ_ 'Qw @<iCN ^b|( @SG4F{t48xz @-8FFcijϓO4䮙Ւ>6Ŝ9.ZTyX f-txϠdh3xT%gy'j ~SopA`TkϽ@{{S?C@~7XB8{6l *+~C!ZLcϒHS q;0CqpCf*o%:oIAw[w~1Xb|۱[9ӷUU $JɛUHãwV 4ehIlL2h7il sb6JόObv˖ E>k bQtdPxamunYǀ/ 6c񺈠'nh:ScC؃,aKd/CD)s^rH ;PD?>x `Keoߖ RJIcxcO_aiA>!q;{2-Ywm ;G"]ςV=iYp?f !8m}‘}=UA S\$b;C`|" "[?떗ӫi Qm1RDJ^|@WQuJ-0EhѧGzaU63: %+27f6slaV\WgWMVƆ'?; gŔJE”3CL;! ۋmwlYh 2d8; 5G0>ꢤ !\PUQ BɚI4Mm/EvtUsOZNM"*%cdۥ zD5e竗&`8 0 6zE$JTCpqaZMޜ&0a_2wq)Fhdf6!;Dhdv|"!Z,d'got3NE,RL<)"0~ʉP$#j+ q s\+d1 ȮeeSbP'0"{Y: OQFpO0Zy 0lF藚J,%-Hh8m&74tM&t3LȆ f~c.DvSex%I/R0lJf.ԨGjBcCEQ1;a P%^rEI!׽[WխNYP 257ń 0;[$jV2g) x;G,c"J?,҅kO+T/gCwDWQSS"0 #[ԡ J%C@Xve5\a |!"5)d uE2n]#A7nAf˕2!1a!.K6z IS< #8#0&5*T">-Є#T6kav#K).uN!F%2 ;2K~IU$Q rxA%_پаN]QV@%M JӼ @ TQ*qNEBPttlYPO x(=On^F7 {aq+bN FQ6h1G;) Yr׊Og}$˻W} ZgiISeS 7&O \{=AMPۅPm!i2[|'L@5s >:C4h|w{ e@P@'*4g->0 C>]WeubK E㛟ȜS|l4DKAjw[_F7{"O 06MB*Aږ0b*dzNZt"w?2 SO~ұGD,)gr]my͢<z xZbsD80Ϲu衳/Aލp th%7}G*hv[}]yh޻ՋJbT^G(_`h9}*|)mʦ;v*#DCAy{BUkٿ߉OW`:D6Ӛ i6McܻƀvѬ^Hs뛀wɭhNzb!`f FF^r_8BۈF/FOsnq~VIjwB]{)|U?{R~߳gtM К6+s"0e!@MC_: eG$fA~ W+ GU40b|l9>_xp I( %-<|s%;Wp'i[)=s HGTȦh(:\E + 2N|MγIbD3IhiqHa-:OX3[;Nտ} '^q!oc: 6yXh?N% {;+{qѻ/ᜮ[_ vto KB=? S(Ds"R[Rz-R.CˉeC8%wx^и$[c v s> v&XPmƍ)B@E?3HJ zM4~E}P"\cኲX% _rZ^y\m?|xݒZ7kjA z uL&A^-qa<*'ӆEoOcM+lQؘaiS[tM`w?p<^sg%AN{{#z~>}ڙr˶Ӱ 6^+=B3Uws6y}w>Z__}иaHv#weoz$6[!H)MᛎW`p.m^o&)B7 %AY ^2|DR<т3^oո}-f7u !kr4S Ongecv|o#ޕ/JQq4)+[^y@@"OF:ƪ|9}i.ifO߈emWCɏ"[쯯>6XeZ߂d6H7F'u P 0\ڞEο1dn5CL$R~]hh]9fݿljl`?Xgk=h$!@ ]m/H_&n,}e &qq!w-)?,:;TqA#[8̀QmH^{so*Y^5 C us=o{cp#f`;,FjрpC^(|mZ&7APb M)<;$ w%{en"?- ߏv\ߑhT;6|F{PwU@cWla@;Áx̌ŢouJO;H/ XB#BR5'Nj,?z|ҹS"n:FҪV-+:KZ"NMyKx뜿4mmWZT{ WPY3<ip+`f4X(࿦} lE("^lc{D"Q%i8H ?إH$gd_id(_إ0HƋFVS%oG[U)PonN 6L݀O ShIUy@uK6*)f>?~ mp(Ak&`'Y[]o!;s~q zF0WCv e8v"w?.Bx[5PO|'*ե5FWO#Zr^Od,$B Y</⿙T]$Gw: 4b8_禐M"az GqtOՖzV'ѐA7D,pg )`l!wF9ӝ R6T hf aڰ3.;9ìݘ0bᦐEt&de0*X F#ayAqa8Pddj`M$u7]豬.xS k%Pi AL+Bbx˔Nw)CXD@R vPkb!U5DO PwfGWzGDH!eGD~gv{:UtH f44Xyh|w_w6u$IIu!,)FPe,s& 7X|{" #bӶXjZM:%KU7U7~y_5ZzgAӖ@szB ϯ [*[KaGiC[1KFqi>"=BXqsQ[ Vsd]V2n'Lp!eY=0݊Β1}ݛ[ UYCfn]@юs^ /m#FY%\Vuti^)TxFb 8zc`!8:޹Tg:H;{T Z#FЫ{aZ_D oۺ}MiwH/K6@UZ; puyh)rYajGv șj`002t (t[R`$ּkT;#q$=7yW/!BoH[<"(;@:p}D&7z6~o5`m,,q\vN]YbwuKbG!n8mqڭ ahS!4P bF: h]?a{p З+bh*4/}yecdW;vWL; fESy+ ~ClaڸH7/^fMT}{H`gZcWH !b)Fcpx|7? a0׾`xiS=I;mۇn;)w!7R`_MS|Fe&i͌4&_5Wd)-7Uv&qr|Q#_=|A(K6@z61P>XL KN<%;H}}_As<| 1$y..dO2Mz-c;}Pay/QfVyysl֨&|5[MY!Ě `wUsK&hKKlϴ ?.VQ]dbobPRa|gk%,!-\\[FGZt[i+ vibLƆljO#Yp8έB')09d>3p$fM}Ǽ^N7SxG %?% 2jkҿ0m@?t[SJ%M~9;`EҲI]Ix&ښx`Q~6RϧwC:k!W=:<- FhFǎ]io9'u" }!,Ll_KPl;T9Ĭy5D'1Asi8nBC*2 ʯ+%0Eԍ?̀u9'LѾJ`lfÈe}bX[М)rCE@>KҾM8zǎ5%7=jH{|lddC鿿47o[ri \fܔ%2̔`A+(Թ}'JXm+wFptH&3np]:{mX}^<^t{Dr9WqY u H+"@AըsϪJ/F\^њRFL6c=jRQY[ĨQ6+*;'Q_WW6 J 70In@|R2%mHF)_H~ ps?~"$>@zNP'vMd pXl( 1:j͟  Gi5 s>VO2+?$,=g1P~6`|)˜¿<(fRqӶ7 ^v ꠘ @i[2 0QssWM5l`SJL<ެho_ݔ>D\j>`[@E &n4>;dFb<܀<-/=XL6eW֎ֵf柼` .7y`eD1\%n^#Я"?i;NC-"[vϩѵpgJ.+Pm/lP}ݼX :v {Fj  ?>?; Lik`Swll6b40P)k,ClA"[w>@Bfzth$'Ld0|9t2#w})0To%6' rXqgxz1Y"g/FUyo玷ߎ;n]isPC69،R󮺭n ΃({9{ 52u "+?c7^,X#.w֟ WAalގn1 V1Y6)Kѯ-XBr$-f6P3slG/QyɈ^@~ݡk "Gv uF=7c2F N›e2Qjv݃kRafم/XV͕ݕX q8ٿu)\rgy]|;݉N\`cߦ?z6) {Ee=z١60m;~ὁ]D\FZHhrC֬8 2~`{9黊ZS`|=:qݳ Y}MbA |2py|a;1]$j?_+%dywDהf8TRU*O:yސF->|S8ײ qv 63k,-k}Wo\H|/FMO#7el/E/2P\'sxw 9&j=L~ g 9]X[5;𦣿SWL)*Oxk?S>,[HuGK4w)S^Be.+pڗ͉!8Ɍ\IND'w|J)!40W11އ 2WDx6v5b4Z*%G;z(X%|"xhhX&Hh UP8y}?ޑz3 ?e|qU{{`hz"P-xdfY>g•÷ECB2bohG!t%ϏGu`ٜƶn. C&74R*]Đ?դY (iZ& Qe)k}(CdC KԷO1T̫<#lHRaŢ7RBSO^%KoJ*}[&'8nV"{Mt&rk!+$s3'pQG|]ىa%Y+L+tdvF|<1E]cZ4e<ƗTի־mThI캰oE]B .2]8):B@WB,6Z7hKʓ~ 1~Lor6O Po,ifې *:$|[k/YWENO7q0_+rѲlW 3͕;jA0nfxާ,sU{JR_ 5{ߚ>pko~"zKv o,1χbGti߹:fd lO LGhW$. ^ə=zAI\ww|6m2㶖Z=Wz)ޡ{{iђ@"|#,(ߨ,xv| ]0Kq~'kW>m]A]fəB; @ll9vaWNUiٱc ^v@x$WX m1KNtJۥD~+0 ߳w2W5 fiTwH)0XWgeL25aa+zW邚i2"G\Mmvc%Cv9%kpUM Q+OrrƃWCr Y$ݹ $C*4K +m0 ?uN 9j5wu4Tc2wPFDςmVԎ6'cuOvS$6X !]k$m;f6\o!O6]%KV5v.9'L 3\FߚCN*n~?"2u/A#K Lun񉃻L.@pbyp|)c ɿyKn^ӻlHA{A{D?0ہAE Z ] NKn7W.4W=;dJO< 1]r$sgUֈE| 7cvj| c`fG|y<>ԎFcB(`m7̀Lg2cH.T}7`Yֱ9&)xs' E_]B|̯Baȿz O,շb˗L} >2._C="ƱwOwutO-/ֳ$$mnc\4u:}n=X?|{4疸f ;wE'>޷iW ² 8yR-6u;W@{+A\~.=d}Hp8am1!՞-%q7T1 9p 7+fY@&Æ׷xkչw a0׽"—7eαfND'|[.QwۼWvĽPjP ~_Sq~+Y;+4='$M|JhX=K.ဇPnJGQC-ngft5־Q=]@cEߊx z`|;WNDy&x QIF5P}19>SvpzwҬ=CGQcWߣ^V> I쟡~霈Gz.åZ@D'o3$CR=dx1{> & ?Ro$LW( ^trf|M"1':hԄ yM"E\8_qvVI l 3HuR ޵0[>aCA$rQB[ j`S*/fgώAzVqܻ3䥛HU+ ]pRo.E4kw34N\%Q)B)thL/MlAR{3Vpc?/{<LFVFm%ZBlc(m`CJvGo{T=0yvId&N4T]Ifq5ɧa*j[(FCtg”]Gӳ[e-\w\q2J\0ofwcb+J¬5P\ll}] {^5Ohqax[Aشťrk, ĺ£5y?Q(J(UF>O 4'ޢ΍\0n9ȬwY(Ӈ~V-=g=)VN ӍlKET昈^&7):ddҟ_f 9|y{p7XF3姌' >揹 z` r KOzm`yrd;<<0`k3#[XlR _@HuWzG4}C"=y?/XSTTi83~%Z%Ps8_kgTlOK;3r]ۅɨ6G `@:,*z(^o@3;;WԎ9hֶ_B=m*^!(RZ3:n>Ɗԯ?zgv{r4 Z)P_5dz]lyhd6˜kQS7G{aJ$Nj{-@_i_EyvAl4)qhﱲCaє_DOw#@Ը2B4Ir/Io\Ï"HBVQBe ѼUA/2 ^Aĵ+_ơca<ÑbLeYHrppikWi4ΌD7 /rW|= ޟhތg'\+4Pإn`{܄篪0Ew5oe?y"Cy B.m?a =cѳ{6V1Wg)J=ll!f!5tȌ%K*4j:9_EoUOegS-;\z7:`l?b RQ }(h`81PC"*yv3{察&3%bݰg3)m+g#o[C|$ĖrX7e/ X>6Ь -]閗g0[tq[֫`l]VJ;0Q]Q E,Ker_}$֦p y'` Y+,)v\XZ``?`480H^~Z/ Qa (4U0S7{[X6rf"`MW6h/6qC#)JTx],Gxm_l{{/>"*|fuj9ݠmy ;h秃.Un5Rza=XNB&ZLFI[o$I;廻*C Utm(Ǩm1FjY{!Fi^E-kk' 5*',CԎϪNVFx{1ۍbcci'7Yq.j#Pie! 9rI+e1Av>;hv7hctfn˧+d}m[eGCZFX\CWz֙;_;$2+Xdv3O* T_sq[n3ѧ긎J'T zҪ;UcuX#e}ӂL&$CMPYTniW91a&MrA547Xanq烩lʀ?܉) nê뎢ݳ2!hM0˪]?x2  m'V i=h)E#۠apUQY-񻻢"g'{K^ r}}T޴PG[Me$W8upڽid( |~r'[Uk$wQO;l-?SWbxuۖޭby` 0Ȑ Lk^THCkhEH F)1"RCR8]% a X縈ԟ'Z|NZ©oz,tx"0F#L!k.UkCMI]06T.)M9Ů*EU= ;Α5בA;-x.|ZF Pz+F 0HmF!$"%C.kC?O ƪjvZ% 豐<%9x_׉2g-@jg0=J^BU)*IQ2KQ?]}1-4{Eaˮ0TGriCoU@!Ft8CXD-,]+ p0 * R%YZ9xD uYlHu؉Ld`̣ j:n֦*B§H) 2ë27p 0lN9!V{ST+] Cv< ۖT *wTtTNm] ޏж9nj?}aDlNp0&^Z "=hQfխ띔d$k0_]" g*s-pt6AbYQ3@Dg5Q w 0 .D@,{6 ']+8a+hR69dGVccl#OԦ4?o]. @E*6;[Q5J[ 0hV#Dѯ' &\Q8jaf׬HqMIYa[A1F |.KAKi]g^*iW؄d^\SA4<-,kbWu {VO$e0K3P@>VW\?|Y>%5qZ֫p_/G`Xt`98TI%\1)5$KOӣz0? A|aq٬Vm6]d?`\ډl? cvS@2a]K@& ^ 47:Poƙ zwSoX 7C#m]_a'_C+j2@N oxM>B#pY  bgEߔ#p誰J%iA?k(=l mbn5 c L <.gXޯ`@ bm) ܤ)O@{T; JbRD$WpgvZ껯z1osaoadRc-1z!$i];:̏X]E_6ڥ-!W]`amVqp"ܬisưFclx兩&"H\ЂB, f%woIYg @=FF5h|>)ߗp}Ͻ\'D+6b4 aH6V%I'' H x;'%7g fOP!۸WZPCQ ?  4l?Y/p gu<m?evOV&KU+ GТ uE^,(P;)IQZ,b}kHcy-`gyou/4z+%Džw5im3 ܟ_cl^eғ+gт9 Vi>A] <ߒMa'uF~ @N稪pwmȟo3+9ߡ`x'")XSxt@zϩnKPWP>+ ?ݠkgMvRXƫ>I1[j'J>39{V~(摏;@GuM=q1iMM:GնDZ6i<;wAj(r@7bn< ~T0nPDս *骛?8rUi~uX |b)1w :m47P¶ѻt)D7ײB5/^Un?Kҩvpy@2tн[E@a pev5r+lɟØ[J j~-J!R`Z9Yl`pߒ"{C9Ɩ^]cӎOӃ&x+MRdLٞFb&l#wPUr>@Wv::zi7.mҫ;*?rÒP@3w@z)! qsR_#jr'/[zZ4?YU7wfO^{OQ\6?~(kیȀ, :jA$M),-Hx7 6OVsFʼnF>šҊ'6 ) t w L}֎G!+1] ;rI7!0b3%}a~yVQ ߾)Fc4"훥2{^6~T9k,ǟrFCyo;H::IP.gUe,smʻ5k:T{utmYI-WTNxb"c6=V1w.2ͥu׽?@4) BV-^j,]nkMI>Ra*"J";b·id{P_\.?gQ1 ޟ;t絽%ԞR`tNP5L;Ө1JZR]N{h~~ESt߷uv1%fƺK`~Gls\ Z=ڰ!5=![F[-PM1| w_to?ΰFCw?mGZ~hq"!5tS} *`V~+?Kр缤|A0h]3.*rW6v 'jW8 < hWUAoC'v9el,_JjR@E "d d0t[o.n^h;=Cز>|ߨ+:v>"A'RsUg"hI!1H:$w*7\5dmLHWHs^;$LwDYUND8CԤ{&r?Zok! tkQuaOzf]%}NB oJ\nbV_V} vVY,`CLeh<gSj:V 1^=奜nI? > V˻qUUݗq[۸oQ?T3.6-R{Y,sއ{Iq2~ѭ"["XXcrA:|'3,>va/[8Ɇ$ٗ->::1_'R@ypjP-"?wW UxH? 8@:+pj|ߟ /_{.7'^|̈́mƨ /8 pt,K 1iROp)uꁞtVn[j_,G|šMM%ۻW}r{,ttޏH6ጶOlAš,tV ߖ_ӂS_idű染)l?;b2%#ap O8 UO^K>Á J>DdԭVcH5anE7wVШdH~cOO"TIluTfk࢚tuytUȁ ll~2QD06捁z4m]|dvҮxPsr!5{:4W+e{${Z{[/7FW~ui'~?~HQUR:G,ݍ~r͝֐"!E K<.>*ҳ%TifGyW i])LB֣5ùJ+\}I6eV嶤 퀯ww;-8)NdU+jYKrGG{@OX7v mm?i }U-LopbIi[Q>[듞O50mYTm F>.sv61R]0ɹw=L:_~AoP"w|T͡х`Fc189r5R:8S"qmB_;8ԯ3K􉂙brPCnV*5" JMy1 %tiZUdF@qs`ěĸԉ޶b`kUFi?=UkYOeEq/0/~ 5wKnHhɶ|YٚF%Ke _,'TDrP;aLG!LT9zCe[-2"'8nv*HdiGg)* k{} v &!>g˧C/hN %DRg|84 %|`7[PP|^Q_nVk,HSUpҠm?-{bLS$}1r~9'%hӻ(uH yLn[V]k5; MUO X `kHfs0uѽ'q m5Ȼ,YTjB3}HӴe@aa?WP,%xjuQ h_^=00vб})Y|Jo(Ŕ|z``OWqӄ"a.%:ǂN,wwiD'wn+x'ގ|tbt'b.sQY)]Ƒ29d?eٳ!ѣ+}ޓ()m*]BJ_JoDv)OE*kvlAG }e[w>  i{!\e[ Kn7֮$5)M$br' ) ; MǍ? Rт;w>PF6@ji2w_>tYiFOQ)FN-qj'HASAs@#2ˈu2GiO.PJg=I.(k~#`f o=91ᅿ˳*ٽ&: ? ٪xle EISím[X }ާ7L x!~ 1d4բ^AU;-{xOqƤZ#S1G_?.Ktr{DMh?pv6CZaẁ+T ;^0Ek,eTh6M,i/ :{u;D/_|`7.NFݍ?s=rFmaaKAիzpP3 >_A,6Y"taMY3Jv5#*OH^ w6Vl5 E?&k05'!< UCqm$֛0³* 7C#Gލ-"o "C-/T*>ѰG1BH1(Sʌm3~Rd}6A~_&zs(:OZ~L2H}?Vbv; t0b_$pX4]> $Pմ|2 ?3E:s'4[/e~ ׀ 4DniIo3Tm)b\H,\d*K$kK' k-c%o. MJLiH,]a7)^|bK\O}Y;k龆?'2<mXRf 띭*9UZ1s]~=`髷ZZ~sY;q,)>ce)[0em?/=gXJe隺wg|C^mp==_; ɬʇ,*S#s;vV٘ﹿ4hӣmRl>v|eQ}7)մC>:L/aoS)ŅE5N7kpЃ':3=qgꯅ)^}d5 Z#ǧ6VrDSm: Cհ0tyfŅ9~pߌ#ނhC@ދu@Mrxc\FBNOA ֣xѩsX[F>\FknxoY۰r5SJtak͋]rKv~? .>w^ d[z Sn|п7*I Y5y8}K{>,SxЋD]27;/eJ[48`mSAOf8,A\{;ڙ?~P~>qɨM !_'sXSKڐN H O–dLW8@Q/– Sb@C|wc1dε0"#ojZ֌5׷t^eA!ic,3tFK߫4fQ9}_KR 4шĴ;V{, (mHud\<`?8ؑgT8#^'w9a蟼7xn:KYt3'# M7N_MhmK6 Af!+ݷzF=_'`8`/?Gq!^Xo_D(}Rp#3S\Zp;cJ`nwjV zI̐NSu} nt8Ykfl-ѯfl0MĈI3[OK:;HܿO\C>][ٳ7жn0tC+ 3kH}P `V/q w10OV*&t{'(H43#s{hRId]-Ïݹ7)zrP<㘌Jxz%VW:O,<Ջc8sG禡K8za/;;1g?>5ŽC7E gen#9̷[G;ܵz v98Fp!Cux}zuՠ/ffqzݢMOX}\Zn$f@:P Ֆow0@<3AI(VB%} ےv#~aUۤy-:OXƌ|ȎF 5Nqܵ[7r45wJiƶ0%n+Лy'u孾i5-A+JO?}9 񂬞V!V;hIԬv^ZnGo˜FX۱ɖd[s\e.=[('gDO s e#TQ%9RiV{ew9|bLHx PU4+DY]1ܽI߈7o pNpSlA q@+[Siga}8=hwwn7JS1ázU2UI+VwTFP|hz!C|3Bx=)Ё c{^ZvȎ_+ 4f(7ۉSr)17 ++Ĥ62dx?ElZ'H-l6sK{c>BPI2rcۼSwsm7)DB?q6gQiށ徏5!YW{k9 h„F+Z#0%B" x",D#3ѨCUz4@UoE ,gՈs ?&Rf>o@egf@Zq>&Yy- "Т@b jyd4cL)M'Uq^ww;wW{ VmCzS6Vcn#@[0{Z:}隯$ 0#[6A8V0Ľ W_dFDuTvM& v#3r$E͞^?@ (}te{e3(ʼ ^lo BG~Idof6$!dM~D?BףRk9TD-`?'I Q[sV|$'ޅC8\xۓ#MZL~xSo2fզU$9yt1]IBW}loN4G}%7 V6]_|nK|>-Q0@3|ċ\ ĿR~ø%yAw?ֺm*L>D;2#1a4f $(nD)DA s+S ԟFSL#Ѷw H_8 V_{>a4Hs~Hs;_"a?UKbuAͶHLC{Aۯ0(=~5 Zݲ>`L6&ivgXox ig>)<KtAfǷGox-@?Q>:nʫqJ 1AvS(7 R/3PQچ"-JB:SXIwYw$Z%Qh`7}{3]r.8EBW(Eұd9IYzLzt)rt&\l$k]?&.O=Z^dyc`1c(w|ݿ2!982] Dއ40qk_ɵB$ҟ#ol$7?%x#ރ{G;wf\CsD0L5=PEsL0 h aȟ7r0rQ-CjJPGQ'pOK#8'1 tЩy "&@ghM3P~0/ϾvRl>oܹ /kKv4-}gF'Ϻ8^E csx^oBZnc5`jjuow*|e<.%CfM܄w& :&u_kj>d &rklGK.ţ /?.Kaq0nutf$ERUyTh <2, lq)aqRc >(p97t6֥|A>ka:όu?y[ND̅56jZp5+4Pʉc$,^n^-&LifLfhhNeG 7j Q=]ij|u2 !T#,F' xܠODb˃^HWNe/S_7;>GNozsjdw)47P풷kL--Xӷ wgu)ݭ$\`Q nK xcq-Mw3LX0Es\_ \"/;߻@>' CGƓ0*eqH@f!Ꚑ7*sI%g:85qclϝ6i F q!s6l]gmZueàȃKDGev/\Ho3yREߛXSz-{"g`f7*b8 KY!M};1l'$7  dP7%)`$ʽ]@N&޵u\ib<>]Q<3A`$uRS.`mi&B#8N_2.?~7aj(GD,'5wM4ȟ/zj$5UD٣A]i]H?4])7T):u,/ZIӭ˶xưZnfwys$nה]$GiEX@$M%R{<{Af 8\K63Gf9ceد%URqKx<b5pRp#(Sw'}xB I+ZoM־ mQ/c[l N v9-c)(}$ q[Vh/-#Iϛbn٬lu^ Q<]/!U^a:Y|!z_{9]@lF)I [lj|񡌐өn']ȵA?g 70g,\w\tzeDAMw{1\Zߤ-=.n^?9tFfR ]0} vD^Y|s])4Gw~\n?e< ܬ}Wc{Nݷ Ś%v}.8V&Vy%tI_$ oK)h iH|:g'^ k.S_qqDsw/_zy  0^k,TAiQ\eMe T-]uX \ޤ!mS {3=op9\*R AD<| x y[o*1>V ˼Hqf Zg+Ņ7C-@ ?=s> WĂhK\SKb]LM?b͗^+;s)za׾`ڟQR*{xo8fL2iO+#_Wvn <w$n /n5I<&,dʗp8栍!9#8 ׎ _񽔳C B8YXs.wk OCM ݤ "zOw޸h6c$$6mب%f*U$(}KLS4IAT!e'5: U)^\L_ k8Egcisɗ<_@IX"fOY KXDjɧO[—4P|?Q>;0q{<=kK?|[*]䦇 f__ Mחîf#c\q. ÐeRHLי aZʽ/W^2ٌ@ 1dTrW }1RE@ B0sAcNd+yqU0n!\qiDž![Bjf A=Hiˈ E?z~WN+ǔfS2"PJd:?acL羆\gÚMc7rX2o!z`}ڳ0(!s3嬫1@Jy~֟&mp.S:0L >J&nGļϰ_fG}_X3`oA8y 5.aXʔd*H-'HIiz"BA? mACJ44ԘlId"p`kMxl'jh6Z}/7Td!j```g)1BTJCV~m u@,iЩ*HrۺzTʞۧm$Z:bLbET"dL1H tK"jM_cD+zW!'_jCH.RkymW-'>'e>q^>.\@/;UIRM̨/YWMv]/ZP״2p+i%h:6u4B酷]w,> qN,z3gs!U'CZ)uXQwMD$Cdk^f4p𥆠iFh4iܼ'Nsԟwf D-Ge,^di:6x aCKje0`Dʬjx9>!G[,zֵTMoխɗVnS>Z F|m-~7 / +YEt]ߎj=oKK |#7ghpqסd D1`3| γ=T6ZGa1ZcH7,gD-_WjFsG~ `éaw~a] vUㆲKvbq}֨LwFjWCΥ7RUh7 s_n>I4˟xpAಥj"LƏ@vW$Ay<# :8|4Q]4\P{pDKtTi1ʍQ-}A.k? {W|}6)-}eYJeRf4RՓH^[}0oj!޴fA ? 8ePQ`y Atr8u=|)DV[J߅!- ..h<}5GxS|EʉHqj jZfP8 ]Ipҽ0eAHUG *Z& 2b:Cg]V/Ht|"%7@ocЉo J5h8ZLIN7/~vF E%ח _rph=8 b`.u݅GM /"!ly)a_/pҘ^=:n1bGY$mlҫu_K7U[7]|ps/Uq^>|) %`qzi4Z@Ļfљ3] ="yۄ0dM7op +ESZ;Wd #7]un3;T[;ດ;|^|;> %{1YR5u͗#c%^?90p@.|\R'.7 ٌ&|IGUU_%$tohww}q)ZjVa;pz|[6}\mln6 JV_"X nZcV*/S5uWCxW7V>;}Gwl^7 5#[w fT&N#TYY=Wm"8;>\lqR$\3r`FJ=ɣOUL0r8@K>qxB^@ydby/y@hDM)}Ê-> iU2lߍHqLpo:%^81s425NsF~S ȭAS3ARDnk Sz~O M) (HҀ%7!Cy|5ݨM&T_ a駴~m%q\fN#Y'Nb[#J4OA~H]'. UrmvZrhw z!%@讼`k^GUS Dqݚ푓wW{^v/z8>15KaȿkaK?@F]`=L?lx4N16:a1QTcx |_pYFdAp$FFϙ9^Qs2(*a $7vXѿ4%gyῌ6:a}F ׆_1y)O0c!3Xnt e.v&=b5ot{m!G`5=,@E 4 (REգ:RȾy ?;ӄ Y]C:69u;N&[;;6¨1)^-o%;*\u@~_ {HJ8OP9 #Ї&_GiI 8)<:&!#@mO0{8F[9ȎtG]6䨒 fgB 9/TzhNOU8};}+0=~L<[<7 _%H2 (r#mߒdlɔV,&߾OcQ|vN; gD(?e;!9\?-eF0S&kQN`DWaƴ`*C% ?ڼ\w2gYpioYiwhsD|u/9_Vcl4@}0;4:Dy:YV]\!\rЂK/xr7M%[^>{^X pmY}</a0!ggNdx7wza(^*VkVǬMc6͐IJ{"DK Q`AJ'X @N?S{}W4mAO<@C4%`ԩA'LF|#0,ʪicYM:42S _ ]rHCIqx>m?P˨v_# M3R$45~ 3Z'g//8%gI^N!9 c |3fJl(CJP )rv1^ةQj~\%Q( n|NüFi)?e jhesU^MrBi|ڈUHMKGeQW ȫ oucCD Ĕ9 ]D0ngTaxǿ}?7ש-}y=jK\ ȡTsY^wdDr 5_cd(/  *n6T_Yq>HUݽu`J䇻/ &-n^lg.8QSҤٽQ Odž45 }KX&oMJam<˛dvgfCeY(m7ş $ Y$n°P` l@6IgBNj&SA2O ~ S=qGvX)M;AIiἤ5A\Ӗ *jX4) c:$ 2^)\{ ނߘ~vdR=˃UDJf7?le O#@յ`d}ݮ6ghÿo hx:Ʊy mp5U隭c`^mFka^> _lfe1z1#,ѽ}So:I\0"x#E44;s,M Pբ{=v4 A ̽+ q!fhT_ ŇGob#+=2 A{p%9 =|o>>O/ڸ/$Ϗf5-Un\Ol\I`5gF\샩:?UCO,('T]٭{dr')ŰSWK R4f3Kr5]ޯo&-1Ofړ}0h ²:ڮAi-翓tRPw })v޳X!&MtSX,s!?|\hb:pc'D}@f+&=%q=Wr1kvͦ{F}xGtöᤆ, K'ܣ>"k)>*[l)ZP= xz3ԏ .SI6}^7!?!1mMzN\m\+=Kk^ 9UQjD rڒ?e9NpKtn}ݣlk>ޅ!rL@R-G[lC˝׿/ƀ;KJ^Ox`}/^ 2Vv[I|HolpWf'6q_]/EX >o _J2opƅ֦a b7+S['4< WZүA/yOL`}}s_I,Low0!\QyJoBZHm^{~"ӛl=&ep@>`xq}GlԕGOf!<;Z@5Xm443_J a{u3xEp@ax'8T,# ?)}PN;`H"7>5w2%I-P>݂{pGOyz IQK#[2q"YVZ(9Y NPo{ mY O$wxU46~쮛~t"jN*?6ejR.20~/$0K] ˚u`'!R s8P\Z$^mϋ<]0e}|6:L)rym 80,6uBiOmj56]cHBs8`1fcDS :YrܡDS1|&P8 1::c҉"0L5TT8Txn5\dBJZ q}ɭiDf IMgd˶߱h0@!W+ 0 dfC&]\"T8 |i7S&S}e #mo_=)@yu<%?ދeI%S86*<Q _(%>C 0H*3U"SSUF.<  pShhF=zxо=D ¯/cƠD?g_+ "0rJRU =*jYt51.W!^Vp͗)J6Zy ?ǭd*W2It<0V"H5ΈV"=N+C[iɖDHDL euJ6l)ظ5r #/b~Mqшc3h/Fp$0! HLkvĥ"hRw{vq܃\ЧL\.Sw6 r&Z,0APHc)&nojW ,"0̤N$Y&"Tĥa=QJޛc1af*Ǵq5K14YGtmtmb\e\kҸBjw+6dҸm]eXY19V^Z64u`:ㆾjYk%Y^vv?ǛKlP+f~0q Z:ɴC gQy"g/ޯLDL&S ,7R4gYb&QKΏRvg1锴2gtyڿ*&🞔${ȇF>6q-c|`VkC+`#B=Ї>6cݏrŝ񚣂^OӤD?G1?2ԣu;'Z3_)?+۸1镢7Y>QWF;~x/~_ˣ_㩔]%%Zmb"13HSJmsT&F1P/Z&a>cDf3fq~iHf3a~iLf3q~iPfs&q?cTg3fŁT~{g6.^i\ռӼ01/-7`--8ٞdNҷ4 ߄65[Ll77%4 +$_u)1SF~e*eMb.[qn=C{Ie͞-L"1*k<ֆƒUghu R;1D)˕Q-36?K7k3M ue:xhƆ6 ?UyOױLWe_](`"9Фh'N#iEkb}TzG=忸r/:?߾|/uᡪ`0TgeV*kvGJ<9-H&"[y1@L[A vId'k@ܼ1ah/qj0! c/eWkpVkZ % QϮgkfmYs3h啫 &3~ tdX:3iKkk"u 4q}MxV!EƳ,V7UgčgEJr:gEY=)"imF!7^.m'S\+z x9{٢dU 8p8|X8xX8EwT8šr)ƣ))̣ =O1OOњc; O1V<Ŋq<|`~_"(VU (#(#("(APFPEPTEP1( h  h @V yQCk(R÷wkK;TGE[!lU`@QU*0w*b{(VCES#U`P` XmPrx(VyGEU!U`Q`XMOs'VGDU%~UdQdƊXE.UO"s'VKYU\?fWiKĚN^%.%f'"q'Vi]O~~*N_Ot3~+DcE`zFoQgyh\! Q+!fq%=K\8h)qᰴrCD PAL!D^KGf/f7$g`:NA#)5[?W*Ld/8|=#"n;8 y~9%CvNٞi&ga <)g8Ļgug8č>Li{:`=2;$HPw>aQF aQQG qqa!ǰ4\+qqg, !!!YBGCGC Cs+! XW/jb@|}xXXj4ѽt_r^TG+b;D2 ={ XjTCs^!VC*b{ { XepU\V8*/*b{DRJ]{ XEEfUdKW@*2* b"VGt/[%WUd"VGjTq;qYɫܣt5J=JLW3ʫ| bUV@ 1@D<DR͖~9>}W"1 ?_s1Yq>1;}1YyD`&>`&9?`&<`*Mssl3q gT&>υ,[ܻsuA5a^!Q/?&uE 4UF4aO^O MUÑ;"ޑWGkXn{Z%<$#RsSuST$gCڳ /@?s<&_NQ60@BTZw@zSjӢ~i[1BkV;xSYL6zrvL=Nў f22S4}yWH5, z6IuCȺ>zaG lnoUXo鶨!-ېvG7[lH𳚯2~&UHwidmo$6oΝQJ4 '=nqMua;Rn3|Duƥ "mN]u{ʻnv8 W^'"" &-͚GphsU߈Gq5c+V%tpCS&GOtܡ Q9uMD́sʴSMO 1L4V%Jѩk%6~pq x$Nev4!xMN~#cqhtZI 6U=|f]=m\Tíy?J4#=-Y7i͘-Qti_X&ą4:uQߦH II`Dl {4 SՑSH.ۚA!WW> `Dt@uހ18jԷ[7/{n@WZ5d{-)r F w@Ő5*hMXaʭ br58gD?,4͉}HA5" +I'A!N iG;ӡ[IiF:HX1TxO)qǎFV2I'0l [AyHs%baPρҘs  $ 4Ctt{ҢLh{:@p·@R=yVEܷtJ=M؄唼FqEl{%l LK*RĮgH+'+'" 6l_NkB ZJn4b4Gr )eRY0Yqhɾ#3ȕlxVhRJZ$:"DXv}BAR?÷O?bIZIy+%H\iE?Poed9ֽ#d)1 |# 3h"IG8ghm!٘q# 3fa YBA01')H֌N!&Hh[WffnUM3W߭q|X+8"ppm%U 2[ŮDТ8"X8P$W*Vx+:"HHpx4dqe+ -^p<68,ުL2"/vn̿FUP冎Z}WzMC3'Xg*ճkJz'T}7#^5)Fd&K47ymvQf߂g}a\ qXpQ|ͷbG4GB&'i77c[$!,w1diF* ؀K 8IeԦkRȼpg%K8s˯H& V+"\x`Лw4]HM%†MzLΗ^s+ :p_{7h}KJӭh#xh(3^vF}2ldIPb%oHNim&BidI|-ގ|Bk61BJ$HB$)ډQ/Ll'-h/|ٱ3/.֑Ic&_itj:Np+{! hŁ6pZ/ M^нޒJlIݪUVk56`tȡ(g"i臊Fjo_>ow_ טȗE\hJP!yiBU>[g\'"{fow_~3ug?R C5E!L44hg}r]q/Reٯdȡ|ir G9RWUqŗzNdbH;4FaHǺs7 +IURNW{nQn **3 * Z>hjάb@:E35g]&aɥIKq\BCKԦR擾P7"׬5>Jbc@V G.ݞ{-'"Ie)wa\V7D$G+XBĐ(I ®B5rƅΝmH*r}m8r4dpU.mWV-* hZ:뢐 GtEWhdxvrt>^KS{3& eSfoɴ#+W+FTyԣQ#oR8d1̙YQz:>O]v\#-ӐZ!5#F̍BIx^44t15 gY҅:^OSKvsC blatA<{Mw33ۻdk<E;{s!<ӧ>}T 1Ns'dJs%.ZlQ&lF#r=~S Gй`|i5"rCID+g$1p~MߋIa\.#ɦsZD]L{CmNRKg/5R3 Yݳ0 MG'=PJ:yG9:B9g7R|Ty8IbtUjcrJ Gs4Mq!:hNDąlYtpɒerYi~t*B!5慲e2ZYBY2!SZ,e$=&zXs!샿+,#$e{l%u=]Etk/8< 'u.N G+x+ HBeDHtsܷW(Rb"8[ Vj)MzeY\#r.X|oInB+ڈ? b4MnTai{sWqAG 'Q7Ӑ7=ŊLM,@ Uvl3tb)lڐPzGIH5wQ FV_;~TVҨWyZpr[j޴θZd3P/dPKXJxoU)IL3[&c j_DU&9GX ea:p0nSVq ?w__{¤2q I_&E3[gI|Dex%(dp+m?/w~Q8Lrjclvڿ'`ȷJC$B w>X~7.A A=н%A)<42 p+ϧr5h`v8*ykRa-#I! 2nB,̀B=%+odnb.\r~\ꦓ9Φ7V5L9vߗJΑz{Xl;z 1jhŇ-Հz0G |X%* kj.Y"cۧsYQ2,tYOV~:tJE1YBs k9jC{ YVn?odud=jXG8C*vX-)%O&mlBmB/YvEEmtlPTihkxU^v\ͭ:_E Zq)R8wPjǏ߆ z-ʥ]8ȲB P!]l@B!UB >\ W]%n]&ǧiaXڈY5?DX_!SPWIW $A΅%txk!"Xn tAU1c!'z²9"rFŃjϤnϤK{&]2;&] {&]V;&]v&]12{V{>(I+i%Uʏ?ch!z6o' .vY-xO.GqV<orPD(72] G2 "! fS"y#*YC*87^vCye?K޼ꥰkekeo]S%]1eC?5-gߓzf5f}{1 f)e%%)T4K~$MjcO՜t; m3 Б_i.lAE E- <Qbs;6zqe(Op b|;Q2 Gq2 7$2T3rq MLJIKc&z'}ATsIik+7sѾ.On'jD٣,#V"XH9)@HOBM H/8!Sk7ɂT%cm(ܨ"JҤҘ4~%^c[ p)|&M`74_3Q9K '@i\3Y+\+\+\T꣨\s?RorUP>6ao*\f]SnO_O9vk*j j/bgڱ*(ZKa(c΄or)fHيDA}EK(@[[q8~Qȴr-s-]keswT9vwUrכHj(.}tC-Ft|d\rZ7zӹBy)c) %s;.\e.VYX 0K_+bψ*B *b\A0$~?D@Q*,\92{q0]_T&0_,hɰM^3 rN16EGlAFrͱQ(D2 E'L\VfQGLkf_SF:6IX4Puzԥ#`QG>]:tsCJ}z J_8&rC!oL˺F._ыS1ʱ=m[C'D@Q)i6Yē|4I7hϾ ґ(*xmP'pz<ﻜz~#co\eW:H"fqBv #o#OHOќr|CǍ hW?$ &(D:tv#<(Zfl$wȸɸ[Usuۈ`ܤ'`,ŵ>l9!X-]:kW%6#B|!'(lsrN]tťi&5,N\]DkcF)wO+! '—Q$-!)z<4ND؆$sV{M,(劝 "8ꠖa15ڣ bE9Lζj؁ 6+1I3 Bt\@k $+2\̂;$;RC~Ԗk~29x M/ ޷xIˢ{νyGK: +xD)4.{񈰁R:; 4w\|-5s :pηc:P)^UL`mW=}+U.R>Ub0eLl8I+B"n7)1x]dm&ɨl2K3{'Cv:Ԏ#䡇ahe/6؍$_"Z:X/^824vWo czh9$5YmlL}RҐQ. }7c"s0|Pj#]?cseKn/[;@L/xn%!\0`u;ycEIGr`Ea *7<Y9CrQi>g}|Q‚\=\wyDH(m'֓/ bAb T\a.!AKs m&Ƀ(7rrBczJ/Jo] ԖGl2(G Rۺ;D"7tAS]ܝ^IO6TƝLÒ^@ B<#gtOlc=w3Xc?^K^:WV:6|#S3&+PGr aĪuEWdpQ(ZĪ1Q =C(BsԬ=ĉE!rF!>S [ qb߼|FPqOh/q!Sqb쑊SR1SP8$vIGOlj]pB;[J?Fe:m!Go-.v:k%}^6}I(}oÝ*w@#[٘!5Z&atVdLʨfU{M?s0 : dB^\qpÛ{qCkfaӊBUAI WqHB H+ok8G5mGUF7CK%]0qhĪUiE8iejgkT24XǫT T%b]N&KYF0ky9'W\ ]mtd8΁|o@P$\VK*FUÔY+¬H,V=ŭ^ 1V/VaOTc#DP ؑKiѮbάNuڀ|Up3 @4L WͩWy5 :UU bWh%%Ê?Ū!91PI#1sT%F USe :qTf]Ee֕*Oթ{rZtM]7 Wu-橑L*Vad}銑%9>EVE*>R(% @Wshz]R͹^"^7԰tې\ktEm0]K(>!+/s)GiKAC4 *לUCR*QZ|(jf7zTpEv[fgdEKH&׶`k&5e.Z%{_i7W:fHuJE4ʭDlq)ւځAT1>..6DD-496HI3YA:B?#V$FTZM8|Hlq dK JA5)z*[2 ꤟ"tuP((JEgi5u^VG'v) {WO䩱:]"UUK!f+źLhF"lYEWx9R]A*'@]Pg3Ğv W:| 4|> O3z9ݜbJCeMݠG Ru S2M,H\Q@!iFKlSN}Oح1Y3#7,CME0 ma+Cݖ_rn+NPjnPYbn+-1]d= vjJi]@6X5wj@,y1 J ]kJ;솀pvr3\5XaNd0Ē/I dXYtKߤQJK\MB}N hI_lP SKF9&xvv}n9x Yޚ!L"ì=o3R8Su E>!FVޅO_Y/6dq|5U聵 QީdIaoMdV=K@ScSRr•z禌m1hSTi.0=S68o1|)Gz0e3xnt̫=l+ѠPWv=ʀ*U ;fQ8=72fyt@`xuoSg{X[ro/|Eo/>b{w<<7eزS_M\ߵʎRN禌TQr2[sI̸M~IgUN|Mu2zs윬s[>glɍ 97g~vG*z<ڳt(֔KBx/aZ5J/q{ jSST1S\` kCdps.Tl l/re`&-6Q}MO/AN:RvW Y.=z2 Ȏdj4h#l6#BR"睋b97 ^O{_,mRcg1f=(]v];_e5RH SApŷdq9axbh岑x^pQK`/0<ܣDjR>>(ĕ)Rr?|!AO_ˑUߜE*?xATu~|aeG~ X 6Ykam\ &cݙƴ(x+A$LXMi@Ě*ll(ZHU49[wxn,G1*.v%D8 NB"\C'>t/ߗtAӂS'4H+qƇIL228^zO3ʬUZ}D$j ^J'>uv!Ͽ*xҿ3*LWӗ?ˏY we֗k7Z@Ł3',&n%F ;墀;cuT-UchAOlRDm^Q,N<ںҐYkH)4:c*<5ᆮkPS,B_4?Sq.m\N (ğeȄڬEHHՋJ1=v:s8 7! 鸔BZ2Rn`#l9b>X|ZNK\\${@igQ # N]B"L{*5`_ ^ |z.h'nLp;4?~ZD7m1]`h %"bVHNb,-N}a£Z=4;i3$(B[ե"4d8ʛiؚo!(YGQ7=lS96MFl8A)Gãh -R[zaz_H> pUTFA5PITI49m Q (|mQ$lM¹ʸG!C._m ŭĀJʽg-ퟣP|b6Q(>CK;($A-tOl!2n _:3Nev|v2ԗ&MU\"׷cnBD88OqghCT(+ 6]tb~hYWI#z+ʊ(hrJtni+̊|5Jjs:9ǠR%YSx`% ʉL"ꎁJ5kA\X=/!DF 0(BkJ~4DF^%Us$t`@d`W^BBBjO[0흆흆W}vDNk#;" =QQSJT4mXՇhRf*J#K9HkA HԪĢ'=sZ4ѧR}QQ۹g14#*b?6NC- 7Uypib RsU+U X4T/7ry @b̙JK`JGAtaǤzX#  k<"Yٯ|-X)A $.=IJŰlAox"LK8\2Ҙ$]n+@۶nZ]DYЃM&όJO?H`ʗe+,i/0Jn!yM <Δ=n"0p`5 "@EFJH~{56 l(% OܑR(G!&F C᤟Z8u.[_z ,ܰ"  2I{u.H7O ruwQW+l+>x}0q&t ("B]Z@Kj>l3:o V}Q!2X06B a툘U6L„!͋^nmw:85k,_:2zp ƾ"o?}Ae<p.VZ` +KFOG*ixKQn SnnC&H$_tX\x,q̕ϥ |\̄M/h3e(<{b[!ڏcX5?1xe 㴭 gfb<"ʗ*XRy2#c9h3T_A. 7Q  Тva~O?mHH-t eHU* HfdG)w;9 *T&X>K$ 4:50sfH`Kʂ>K$:lB{ܨXJY #ba툕l/pTBT3F. dK#_ٿ_y&|䕗ncW^\6op[hq1f?Lp ¹o1 /x߁/^ O|{G;*&vl#(\y\q&b?dQ\YK+|eP&J<;aXc)U 'Y2R2J(g27܄3]p uRf:ݢP;2#j\6a4k,_ΣN3sv9 Øh3'X`i1HÒk렺ROoWQ)N7]K5//pSWIiSif4`–隣:+4{ֈ7<CW (DMUf[%9ĜLk wEI#9"G sC;2XV#e7ܜQx`Qb. &V|>p`Lqa Q`wo89#k0Vi AQ,wދo^R9:$kh{$;먵1Μۦ\ rkrl  +?k3VW(Yq?[3 /_Ifda12קn,v@{ gXbk*o= 3OCu9Z4>3_85 @3}m[DB]Qp&~&fQ̎T; !; ]Zd [`AWx;|';2Z:d!󅎗0T_0J=bX\½j5TשXo|fzGK vMNfcb4zo޹FNqE +{)B; sDgaon@A[;O.>_ lB*R̢VKf>dFyS*rdsV \Pb v)q@R▀*U u.(@ )b@ E;b4KQVj8%)(QKI@ (P(m%-%2ZJmQ@))PhE-%QKJ)P#rG{JdD7ݩQ@ jþX?+u6\ \RʝźɛҴ#uX&\Pj9{REPEPRSh:Z}Z((((}Z(SIb"1@ E.(%QJ(&(2~(2~(6Q(ԔP(b\RPQEQEQfIKI@ IKE2(())hKI@ ((EPEPEP袊uQ@%()@(*EHz:R:)O4@KE6(QERJQ@J( ( (uQE7RPEPE-(K1@ ((R@ EPQEQEQEQE6I@ ((Z((Z((E-( ( ( JZ(:)i((أPL^$Xb2*h1±bVeΗoƀ9elԠӮnU|ppө)ؠR `V9> QF(RR5 AA@* )Q@ L)QEEE-%%Q@ (QE6(E)hNQE6ZJZ)hԦ4CXil2D4l7Hl˱Sj(i12ݨ?κzF?t0% ld?|7VŠS PoQ !@Z%RPQK1@ EPEPEPQKE-Q@Q@ EPEPEPh())hbTQE-Q@Q@ EPEPEPQKE%P)E-R@cQ((HhQE%Q@ EPQE(.E( ( (QE:RR@ppp$E-WahUV%(3IJi(()hZ\QNINKI@Q@Q@%-%QEQEQEGE?bEPE eKQhZ((EPQEQEQEQEQEQE((((((QEQE((ȮCT2DһEH@R=jLxܴ\U( ր/ u4tP*AQhjAHJ%-PLUDYPE-qI(KE3S(Je)hQKJmQ@ p h R@ 1KE%PFjCQmUy=?:T!ǫ րv(1\:7?ӗ2[PR03u?ҷViS=wn5OCJ - K@Q@Q@Q@ (((((((RPhEPRPh(J( ( ( ( ((((haJj#@KI@%-%%Q@ ERRPQESiТ((J)h :R) s((QEQE%%-QEQEQEQE%PQKE-%-R@uQEQE3hIbQK1@ E.(%QE%Q@ EPQE("jԍQ((M((Q@Q@ EPZQIK@x x >)ZJZx֠:IURIE.)(RPEPEPEu6@(:)h(((J)h((QE6(mQ@ QT(((QEQEQE%%>#PQEQEQEbA襢 )hF*ZJSI@R\QJ( ((PEPIE.(2{o.Bt?º*(v9 &xrҌ--4Z 1J(PVQ (E)ESHPTRPQE)h2Z(QE6(( R5Ԇ#b\QZJv) BMGhb\st@+pH xiAJh(Կԁ™g}5=?*T-گU8 P[̙Y գ@ )š)€#zh= )h ( ( ( ( ( ( (E-R@ EPE-%PhJ)h(h(((J( ((EQEQEQED4Q@ EPQE(Z(QE^((huQ@-%-IKIK@`SF*A@ ENVXJ[J[J(JqQEQE(S)h((RJ(J( ( ( ( ( (Z((EPhJbE?bEPEPQER@ E.)(h(())hb((QK1@ EPEZ(SJ}1A@<U8B2(1})kgRr(1KGJ(M5hK1B)h))hQEJZJE%6I@ (LPhmQ@ R LTP-PEPEPL5%Fh#E-%%s*F]Os@Ҕ:RmT}hZ.#+7S`=Vt*oկⴍf^rjQӿ*--څд%-PEPEPEPEPEPEPhER@ E-QEQEQE%PQKE%PQKE%PQKE%Q@ E-R@ J)hJ3Q#@ 4Q@ EPhJJZJmQ@ ((Q@Q@ EPuQ@ _ٱP+4^I@ IKE@iL=jEQIK@((((mb(((((((eQ@Q@Q@Q@ E-R@ E:eRPh((((*#RFj#R))i(QEIJi())i(/EQEQEQE:RR$M@ H(QKH)hQE-(j:N::1jC@ (((QE:(((((J( ((((hJ(EPEPEPhZ((Z((((J( ( ( ( ( (EPh(RPEQKJ( )qE--P(QEcQh JZJJLR@ 1KA袊J(JCKHh#IJi( %)(ZT*@((bPhbP1NMBaBQ@!;{ /Z"j~6TXA7WHw-ciunYzb׷kk+v%t¹]ӢɚAKERZC@CާQE( ( (E.(%6Z())hEQEQEQE%Q@Q@Q@Q@Q@Q@Q@Q@Q@ ((J)h(( 00f5)2EPRRPRRPM%) (/QEQEQEZJZu-%-J)š)€$ x (Z(J))EZ%S%YZy-<(((ZZJZZ(((EQEQEQE%Q@Q@ EPERRPhQKI@((((}Q@ J( ( ( ((ZZ((((j:.)qE(E>mQ@Q@Q@ bJeQEQE%Q@ JeQEQEFi)ƒE-%6(KE%-%-L*AQPER@ EPj袊c@yFbAg$:(;VX Poͽ?*֪ZjүPDIq#?mJv'' QF(jM@wSQEQEQEQEQEQE)i(QE%Q@ EPEPQER@ E-R@ E-R@ EPJ((QEQEQE:(Ԇh#Q&IKI@Q@ (QE2 JZJ(EPEPEu6@-%-:%M@ x x %QE:( )W#IWR,H*5QE%Q@Q@Q@(hQE%Q@ E-QEQE%Q@ E-R⒀ (EPIKE3RPbR((PQ>eRPhbPhv(6(bR@ ((J(Z(R@ EPEPEPEQE( (EPham%Q@Q@ (-%GE-%QE6(QKE6I@ QF)@ V@ E-QEQEF TP2± m@6 X'̰]{ZԠ\PMej'GVb.P?GҮU=?=SqWhk&~O/=C=z~ր4ihORo@ 8P PEQEQEQER@ EPEPEP()hKE6Z(((J)hJ)hJ)h)h(E(((QE4 < @Z4QE(IKI@ EP))i(( QEQE-Q@ EP8SE8Pi)hQNN `pF*A@ EPEa*uM*tajV )M%2((QEQE%Q@ ATPEPEPEPJ( (E-QEQEQE%PQKE6Z(h(((((Z)h"()q@ EPEPEPQKI@Q@Q@ EPEPEPEPQERE%Q@ 4JePheQ@ E7Jj:J(EPhmQ@%-%%%-TSJ(▊(((أP-P@-CA UrYWl?,U+.y@ DlzE] sm xdj})6 tw|~T+zN\FO5hQE-E%DjVAV%PQKE%PQKE%PQKE%PhJ((EPQEQE-Ph(J)h(h(JJZJi4QE(ij3@DjF%%-%QE6(RRPIKI@  (/QEQEQE-Q@ OSԴ( h S0S8T (m:%JSJ[Zjրj:J(Z(((pIK@ &)h4Q@Q@(((((((((((((((J( QEʒ@ E>SIJi(((((}Q@ &*JZhF*JJe( (EP*J PtQE%%-%6i(EPQEIJi((J( (*ALZ(()hi -%Wja4+6b!Ve_F=~fp{u @ (^}dts+F=\PD;RԸ(={#u?nӈSf-R1E.*qVGVEV J( ( ( ( (E.(%Q@ IKE6Z((J)hbS(:)i(((((((((J(i)h%Q@R\Qa Nj@54ҚC@ ((mQ@ RPi)i(RRPEP( ( ZJZZ(Lu-%-J)š)€$L@L@ EPRW#iVҀ.-XZahZԵEQEQEQEQIJ(h(m%;((h(J)hJQE.(3Im$(ZZ(((((QE6Z(((((JJ}Q~(2ZJ(((uQ@Q@Q@ EPtRPEPh(J(Em!#4b(())hS1@ ((QR-F*UEQE:(IQZJa=+z:3@PR. (c+@Msv5\Apakft4QEajWoZ6ˈp:ɽ9܉>TpPQ@ U5Yc5^:@Q@Q@Q@Q@Q@ EPQE((((QE0Ji((J)hE6u)أ)أ)ؤEQE6(R(QEQET SB\SHh))i((mQ@ IKI@ RRPIKI@Q@uQ@Q@Q@RRSH)›N `x`Z(%JYjPū)UR%XIL4( ( ( (J))E-Q@Q@(-)Ph(()E%-:(.)(4b(Q@ JeRbEPQEQE(E-QEQE%PhuQ@Q@ EPEPEPJ)hEQECKE6\Qm%-%%Q@Q@QORRPhebIKI@%-%QE)h%PE-Q@RҊJp5@(l3lC膲,*؃@b(R@x| ̰?ՅUӾE-%s7<1|WG/*ZRJi֮QHj1REPEPEPEPKE6I@ INE)qF(QE%P(J(EPEP((((m%-%6((((uGRTFTPCEPQE(EPSM:hqIKI@%-%QE^(hZ(EPEZJZ)@ x x P))ؠ>J( (J(-6(()q@ EPEPmQ@Q@Q@ (~) JZJ(((((m%:@Q@ E- 1@ (QEQEGIKI@Q@ (C@ (N4(J( ZJZjQEQE-RToR cPCL1@-E0~Vs)=ʏ΀*i koSLAXZX [ YAͿcV+՟e# mÞZaWXϮWG0XvW[\?ؚ.(GB*\R@ >%]j(%Q@Q@ EPEPEPQEQEQE%Q@ IKE2 2((((EQEQEQE%%-%6((EQE:;TtTFj&ihRRPhmQ@ iJJZJ))i((Q@-%-:MPEQEQE( 8h(+H9'?gH;?1)i-%PV>t b5j,g;]>xQ@zϐ\O'S.mGI'PQ@G/5%G74Qj}T ))ؤF)QKI@ EPEPhEPh((J(EPqKE-%Q@ (((mPhJ(IKI@%-%QE%Q@Q@ 5 Tơj#RDhQE6(ETU-E@ EPfPQEQEQE^(Pө((Z(J))hAR T$L@QEZJZ:g%\5:) :je%Q@Q@-%-QE>((((QEQEQEQEQEREQE%Q@ EPEPKE6((((((((uQ@ EPRRQEQE2ZJmQ@Q@ EP %:mQ@Q@ (( 00fb(ERR@SIMZQԕ LR [AsN&RZ(ZZ()j4Vs8ehH<}|Eqڬ_տ\-gL :Z)h(2a/TuT~:zE)2$>@tQErNQ43_]RP@-P*)aP@sZ8+U@)1N襢Jv)(RъmQE6((KE2SI@Q@ (QEQE%Q@ (((eQ@%-%2 JZJ())i(QEB TPBj3R5FhQE6(E( < 6(imQ@Q@ EPQ@ KJu6MPEPEPEeI@ H()(ԢPQE--%-:E*f-XC@V%VZ(آR((uQ@Q@ EP袊(((uQ@Q@((uQ@ EPQEQEQEQE( (EPEPEPEQEQEQE.(b((Zy-<QEQE)M%%Q@ (QJi((mQ@ ( 6iQE6ZJe( JZ(Mm%%-5`n9 \uu]G4,qJ%OZ?)1ET &(Ə5jm"wfMb(5+ l_j]Svvt Qw5(ԴQ@Kc7chie>X&U5QP袆#w=d&GY\󮢀 ((VMVDh -Q@ EPh(R@ ((J(EPh(J(((RPQE(E)((TfB TP@i4(EPhiyJ(MSh(J( JZJТ(h)ԔQEQEQE%GO JQ@TBZRQ@өEPҊJQ@-FjUPS`PIKI@ EPEQE-Q@(QE%Q@Q@ EPEP(ZQIJ(QE%Q@Q@Q@ b\QJ( )qF((JQIJ(J(((((QE%Q@ ((((4Ph:JZJZe8hhi4RRPEPePSC @H6ۤ>mg!]+[؟OQ]VAV׊8R%UQçL;uYKWg>v@Nx6M :Ck>Z3]H˴f ^8ʀ6hc,z($uja۫tv4B(Q uPSʰC${ƹM*qvenvOOWuOMiivgg7&4袊( sH&1)Q@ Q@ (QE%Q@ 4bIKI@ IKI@ 4bIKI@Q@ (RъmQ@Q@Q@ EPh(iӍ%6 (EPEPFi4T-RB( eIQA@E(ii(iJ( (( QE> ZJZuQ@Q@ ((PZT@P–RZQIK@(fk9*f/)U5:((hZ(EP((uQ@Q@-b(((((((J(Z(R@ Q@ KE-RPh((Z(((((J(EPQEQE0RPIKI@Q@ Hii(#KHzEPRRPQE(EP 00P(`7 nߐC\t}nFM=z$iPsх08~B7U)^(*?柕b+w)+(vjZ# *z0(Q@Q@a[]BѬc޷)EqryZh cze8+U pi yt C_]],z1w0BAWERP0-%E<lڦCTUƧV HvOW^Fm#ǖ5@ EPQ49ieVb1@K(RPhmPhe%8Pi)hEKQPEPh(J(EPQEQEQE%Q@ 4qi)i((J( ( CKHh6 N\v{S 6(J (EL4L4( m:@ EPEPQE~(Si( ( ( (KIK@L@Pœ)(QEIEPEPXH4Օ(jMmQE-Q@ EPuQ@Q@Q@ EP)E%(QEQEQEQEQEQEQEQEQEQEQEQE--%KH)hQZ(RъJ*"HuQ@ EPEPQEQ@ IKI@ EPQEQE(5%Fh:u6@Z(E(iM-6fB[ʿS@ buo&bTֲ]s0}4iM((QEQEQEQGNOJ*9ޕRQ+?:~J ic_@^~V *T"P)(FZ.CP=ZQ=|c$ P RhaCL4,u` 1V;Ph))hQKF(I@ (QE%!4JZJmQ@ J( (((((ePQE((Jq(EPQTFTP5BiiEPu%G@ GEPfi(4N4J(iJ( (( QE-:NQEQE(EP袊u(&%L@(QEQE:ziU%Y8S (J(((QE-Q@ EPEPEQE-Q@(h((((uQ@ (QEQEQK@ Rb7RPEQE(OӨ8`L((J( ((袊ZPEPhZJZJ(mQ@ii#RbQT-%-%%Q@ (ө(֫#ϓՐ3Q^nu@c-n'.YN?JNy rVKfXU ͸n2(8kz!נ熯?t忣 1E` fuRiSw7(blI>}5ƤglA@V֓F}js2ֹ53ڭ[lP>E^ֹ.7 SE8c%?3Yf~i -Ha?ƵU3m)> Sh@&)²5Xq|4%Z'+[ sAO[w< 8* @ H5 1RKv((}G@ NT եKE(EPEP((QEIJi((mQ@ EP((((QEQEQE6(EQE2(EQEL4Qګ2h:(*:(4Ph:(#4iƛ@ EPEPi)P(J(/EQEQL袊(Z( (EPE%'))EPuQ@ m8PV)Hh ZhQEQEQE-Q@:N(QEQE( ( (EPi)hQE:(E( (EPuRZ)h3 (Mm;u0QEQE%%:E-%u6J)( P?/.9@IʱXI(jf$rX3Pk Q+ kO@ kKv gUɺ =*Piv'XwMU|S?*yԟVC05Egeuԭ+;9'vF"8ŕH[bnA'}~CʢT%pF1R@-To$q.`mcd'sm1q wN8@zsLTvKw9žNy֥;׭]@ @QztԔQEKQPiюi1V#Z(-QE%%-%%Q@ Hj3@ (eQ@ 4qQEQE%Q@ EPEPbR@EQEQE6(EQE6((("5aZ53T&A@EIQHii GIKI@ 4iƛ@ EPEPQE(( QE:(RRuQ@ EPEP)i( Jp uH)@QE(EPE"-YC@Tj- ZAK@ (((iuQE:((EQERJQ@ EPEQEQEQE(QE6Z((ZxS-Q@Q@(( ( ((((e!#((QE6(((QEQEN4pڤPQERRPEPhm6MPqjt _FrAʲ&U $/e?D-lx{@8?V'omb/'P.$=$v@_؅I8Om=1"8U{:j+:{9elVΩ2&ͩĠ<⹭ǀ:v>P_o4;0lifyUf#exs+N(baG{fx*ib(((ъPVT SUjA@ 4((QE0u%G@ (PQEm>E%Q@Q@ (((J(E)EQEQE0RJJJZJJ(EPh(*3RTf!5Tji)M%2(RPi(#())i(Pm8h((J(EPQEv(QE%Q@-PEQE-Q@(()(AON%(PRRZ(R)A@Մ5QMYC@JQE%Q@Q@ EPuQ@(h(JZ(}Q@ EPEPEPEP襢"R(QEQE:(((((eEPHih)hEQE2(E3E(QE6(((KE%6I@ (QE6RPjDH8MGsOlp:j^YB~'kMW*QF(?Zwh% 9Z9-!Ѻ+JI~(}Oi~Gj-Rϳ_z;[xNc$>:E%Q@Q@(((R@Z ӂJeQ@ ((Q@ (RPhmPhmPQE)qF(QE%Q@ EPRRPhJ(EP %)RRPEQE4RJ(ayWjeQ@ QTQE%%-%4U)(EPEPQEQE6(Ev(QE((QEQE-Q@:N(u4SLu(%-%KH)hhu(,!YV52S@QEQEQE-Q@(QE:(EPEPEPEQEQEQEQEKM( ( (EPEPEPIKI@(QE%%-%QEQE0RJmQ@ EPheQ@ EPPhtQE( ( (E2((ijaPPwʣ|7ޠ #1@K~uvZK@Q@Q@RZZ(QKF(QEQEQERPRЫRmqAQ@ ҚJJJ()(hQE%%-%6((QE%%-)i(((J(EP %-QE4RъJ(#4EPEPhJ( CKHhQH j#RDh(**(JZ(3IJi(QE2((((mQ@ (Q@(iE%(QE( (EPEPu8Si€$` }(%Q@(QE:JZjUu5:uKH)h((Z(EPE(RRuQ@Q@Q@ EPEPKIK@Q@(\P)i-:(QEQEQEQEQEQEQEJZ(QE2(((mQ@6MQERRPIKI@ ((eQ@Q@Q@Q@ (QE%Q@ N' J@Oǝ wB֥XB1bKؤraO~ ^N2OҀ4TЩRmIe(,Fn7Re7uvP3R@уV6! RTh mF(,REM&M%QEQEQESi))i())i((i4QE( (((JZ1@ (QE%Q@ P(JJZJm%-QE%%-%%Q@ Q4 Urjv Dj3RQEPtQE%ERTQE0RJmQ@ (((J(EPQEr(QES6(QE-Q@ EP(uQ@ KIEH*AQ PE%5-4R袊uQ@ EPH**P5:[L(hEQE:((QE:RRuQ@Q@(((QEQEEPQE(EPEP(Z(((((QEGE-%6M(QEu6JJZJeQ@%-%2(QEQE%P(((Z@&楴M$u&z@  (qKEPQZ(1K(bQ@ E34&i4M&hi(EPQEQEQER@ MPi)i((mQ@ EPhJ(Z(((EQ@ EPQE(IKE0RJJ(((4Qj(QEQE%6MSii(QE0RJmQ@ (( m:@ EP(EPNpQERRE( ( u6@ EPH*1R ( 8T4ր'EPEON:EjJjҚ(襤(QE-Q@ EPE%(QEQE( (E(Z((Z()hZ(uQ@6M((QEQEQE6(((QEu6 (IKI@ (QE( JZJuQ@ (JmQ@(0*1RPmBԂ'hԴPK@ ( ( ((QI@%-%QE%Q@%-%QEQEQEPQEQE%Q@ m%:RPQK1@ EPSimQ@%-%%Q@E(EPIKI@%-%:4aDjCQ@5@3QE6((:JZJm%-%2(JSI@ ())i(())i(()m%Q@(uQ@8SihQEQE:(QEQESZ(J)( ipӅIJ))E( m8PR›N SE: (EP- -ZJa \Z*h (((ii)hQE( S*A@Q@(h}Q@ EPEPKIK@ EPEPEPQEQE((JQERRPEPi)Ԕ(EPQEJZJ)m%%-%:(((QE%Q@%-%QEh(UjQH( (im-GR (((ҊJ(QE4L4QEQEQET@E%Q@(((((h(uQ@ EPEPEPEPEPQEQE%4ө EEPIKI@Q@Q@Q@ HihNEPIKI@ ())i(QE%%-%6(EJZJmPtSMԆ4 Uv5aPFPQEJZJJ(E4JZJKI@Q@ EPQE(((ESiEPKIK@ EPE.(LZ( ZJZ(EP袊mpӅ(QE*XC@LhqRQP1KE2RR}Q@Q@Q@(h(Z(E6uQ@Q@Q@Q@Q@ ((()m%%PQEQE%Q@ (((((()m%Q@ ((QE-K@N4(f4њmt h}:NOO-(%-%-(QEQE-Q@Q@((H)MphZQIEN ;58PCT9@3N PNX.jњ 85@5Z S+U@*[ NUSP j@3Fjn RJ%-6EEEK3Q5hIEPEPEPE( ((((QEQE6((%-%-Q@Q@Q@ (QE2((P EPQEQEQE3HhRJQ@ ZAҖ (J(QEQEQEQEQEQEQE:J(hJ((J( ( ( (((QEQE(IA((EQE( i4((4fiC@I QFEPh4Ph:( (EPj:QE2(%)())i())hJqS*Je%%-#(())i(eQ@ (Q@Q@ EPEPKIK@Q@QE-GRPQE(EPEP(}Q@Q@ m8POON(NpQE--Q@$$ CK@SUpiԡn {PnuYJ@R]_u]_u7U}Ի]_u.7P7T;@Ff7PQf7P5hI&(]o( Pf>i3PuK3PPQfIu&hL.h\њulfFhLf&h5h;4f 3I4f3QFj<њ4f4&hG3@fy4QEECR -4uQEQE-Q@Q@Q@ EPEPEPEPEPEPEPEPsIE:((((QEQE( (i)M%QE6((EJ(PHi3Im%4i4M4&4 QE%Q@ QEJ(Ԇ4RQ@ EPfPIKI@ EPhJm:@ EPi)i((J(EPi)i(P( ((QER1E:(REQE>(QE(EPEPEQE8S4S--%:(ENOQE(EP袊uQ@L@J 6bPb3M$Ke.h23M$FꎊquE3@wQnq UQ,nuWFuVK,@nPuM7PۨC5%E.hEuEPuQ@(hu-% RNZvj \*,њT[@NsK@RY hfu%-Q}(IIb31F(:*LRbE?bIRbPqF)b#BRE.(2ZJZJZJ(((a4QEQEQE6((((((QERIEZO TSPRfZ( ( (J(hJ(hJ(hJ(hJ(hJ((QEQE!CQEPEPi((2hQI@MEy٤ J(E(((袊JJ)(IJi(QE0 < %Q@%-%!mQ@ EPSM:h(JJZJm%-%6((QEGEPi)i(P( ((QEQE)Piu(Ө▐R}(( (EP()(hx(MPNQE((QE))hQE;4Q@QIE-P `Pi4P24њnh;4f3@Fi4)4)4JLњ3FiPMњn3@ ())i((mPJ((EQKI@Q@Q@ EPQEQEQEQEQEQEQEQEJ(RԻ]x5>+-n)3K@Q@Q@Q@ ((((((3@f4Q@Q@Q@j#O&hQ@ IEQ@ JCL)iMQE)(QE%Q@ EPi)M%JZJi4IJi(QE0 < %Q@ Ӎ6 ((()M4QE%%-%%%-%4QJmQ@ i4ZJZJi4n(((hZ( ( u6@EpҊRR((QE(((QE:(QEM }-%-QE:(\PEQE>()(((h}Q@uQ@ EPEPIE-Q@Q@Q@ (((((((QE%Q@ (QEhC<[v8j D@KI@Q@ EPEPQER@ EPEPEPEPEPEPEPEPKh\NTR sTQ-]̠ KJJ(((((()( (Q@ LQ@i3IE&h%%;4(5%?4f&h5hILy8ii((J(EPQE(IERQ@ IE%!4i(i((3IJi((eQ@ MSh(QE%Q@ M4i(i IEQEQE6 (i)M%[(QE%-PEPEQES)i(QEQIEL .jӁ K4ZJZ}Q@(QE-(( (O ((QF)q@(h ((( u4RZ(((QEQE:((hZ( QF(:(QKF(((\J( ((R@T( ( (EPQEQEQEQEQEQE%Q@ EPEPEPh4Q@Q@Q@Q@ )(5 k̥JZKo@Ѿo@7Q=.4f.h\њ4f%&j<њ(LfLњJ(3I)(4fi(fI@Q@ (((m%PRQE%%PEPM6PRQEQE%74J)ZJLњ)44QII@ EPSi()(QLiI4ff4s@&u0L)(EPIKI@Q@ 4Q@袊Z((((((QE-Q@)@ EP™K@fTf'.j,'.j,&.j<$Z}Q@ EPIQ}-Q@ m-IEGK@(hZ((ERREQE-Q@ EPEP@N(Ե->RQ@f57P53@((J)qI@HFIKE%Q@Q@Q@ 1KK@ E-)ԔQK@ E-JuEG1OQKF((J(((((m%)((((EQEQEQEQEPP o}.K@7Ѿ4o ۨTѾ.T҇ [7T;@I3@&fi3@Fh曚JZ( (EPEPQEJSI@ IEQE(IJi(PRQEQE0RmNm%PRQI@ IEQI3@ J)(% f44f44M!4i 3Li((()( (Fh4JJ( QEQEQEQEQESZ( ()˜)EPKIE:(RQ@N84jӳ@f5iA E6PRRi( ((QE:((⊎$E--%-QE-Q@ EPEQEQEQE)(Qњ4fZuQ@QEIEP(ʓ2E.)h**LQmQ@-%-:(R@ bPY7IR(%Q@ J((a(((((i4QEQE( ( (EPEPEPEPEPQEQE( \Q@ 3IE?4f%(**(2[4f,nuWI,U2̠ QK%QP5hI3Q34fL3Fh٣5h?4f4@ 3M!QEQE4iƛ@6hJJ)(PRQ@ IILIM43M4fmN4J( (EPEPh4Q@Q@ ((Q@-%-QE-Q@Q@ E%RQ@ NӨ(h}Q@Q@Q@4QEPi( CJ YNPKuCpj4is@fL ii))hiE%(EQE:E%Q@Q@ E:((h(((uQ@Q@)ԔEPҊJ(jZE%->PbR@ (uQE4bPj:(f(sIJnhQE%Q@ (((mQ@ EPEP(KE%PhJJZ((mQ@ EPQEQEQEQE%PQE3A(((EQEQ@ &h((((LњJ(e.h@n[u|ThZ*=o (Ѿ$Rn 3I4 7Qu5hILy4њ4fLShԙ4ni)&i3L&h3Ii4&M!4Z(Fh4QEQE( (EPEPQE)PhJ(( QEQE-P(((Z( (EPZ( ( ZJ(Z))h(ii)h pZ}MӃTY,@PN,RY JZu-%-:((QE-Q@4QEQE.hF(@E:m)P@E:m))hRQ@ZK@K4@&iPE( ( (((I((mQ@Q@Q@ EPQE( ((((J(E.)(QEQEQEQEQE%Q@ ((QEQE6(QE%%PRQEQEQE6i((((mQ@Q@ (QE&h%QEfvhG7P5hI3Qf5i3@f4IMsHM7434f J(( ( (EPEPEPQER@ EPQKE%P(QmQ4RQ@(QE-Q@ E%RQ@ E%RQ@ EP褥QE-Q@Q@(ԴZuQ@ EPRKIE:(QEpLP* ԀpjPhj))hm(ihQEPQE?QxqF2(QE(iwQњbF*LRb(N ((QE()F(QERREI1@ 6Q" (bњ(mP掴P0iyn(QKLPh%QEQE%Q@ 4LPQK1@ ((EPEPQE( (((EQEQE%Q@ (QE6(PhJJ((њ %.i( (EPRQEQEQEQI@ JJ((m%)((J( (#4()@i)M%6(((J( (E:ePQKE%Ph(((((((QE>(QIE-Q@Q@Q@ EPEPEP3ME6}EP褥-Q@(hZQIE>(RQ@R Ԡ@jPhЧT ӅIE QE(KIEI\xEPE.(QE-7bE%(1MԴ,у@ EPEPEPEPE%(*EFڛ""+RJ( CKEEIb#(RJ(mњ6ѶRQ@RtPT&4R@ F=-#S1@ J( ( JZ(:)ƒRTtJSI@ IKI@ EPEPEPEPQEQE%Q@ A(E%-%%%PEPi)h%Q@ (((QEQE6PRQE%%PEPQEJSI@Q@ EPQEQE6E%2(JiQIEQE%Q@%-%QEQEQE4RъJ)qF((((J( ( ( JZJ(()i)h▐REPEPKIK@(((((((($(ZAK@-%->(RR((4PjXZh@isIE.iAҊ\њJ(@je5V4R4PwQE?uPQEJ JV$ "PEPEPEPEPAEJ(٥K@R撖EP(J((:J((Fh44PKIE.h%ZJ(QIE-PRi((QIE-PQEGE%Q@i((hJ(hJ((RQ@ E%QE%P  (E%RQ@Q@IE-PIE-%P 4f\њJ(RQ@ IEh4fZ)(ZJ(fQ@ IE#&hM&h4Q@Q@ J(hJ(hJ((( G3IE-PIE-PIE-PIEpython-telegram-bot-21.1.1/tests/data/telegram_sticker.png000066400000000000000000001220211460724040100235760ustar00rootroot00000000000000PNG  IHDRx pHYsysIDATxxTֆ'! $$ ZΜ3kł"o+^+(`^&tX iLzOf&=L5J$}RfNY콖LAAAAAAAdccWpquAA]Qev=+u/)+V%U}L}TV͌U>FOdlՆ=JaJmqPb8J1DŽk~,wIn7ߒ&5_m5jz_,"N {~;&t乀 Ke<붱- էn[u|O32^ Xy} F}g4+O Xu27psj<Flaɇg2ÄMS`9N!a2:xlõQ͏=?\s ?G\sΝtGJ生[v{ Au.2]} {}Xd.әQQ|_TL^jM1^:\s ?G\.=pg>Wr@AU\pOxp3GEσv4ƪoUj6,RuZCk$FEd1/y!=mDΘi }#~9cS*E8hg}r󑯬?;箓'&ka\]K .os x*1nyh|!fEHfqV1C7.X3N1rsciaev=?Ќ2yHFqH"]ם[| ordy~'6S&F9ŘB^busZVĘ ~k_Kю`f 2B5]<Yknwt 6eܴgqgi#oËMv6S賔)R&h.6cڊzXB[Z\xa "į)~mlاQq{]5o~ ;>W"P APk`2Ox]`tSk|lF EX:F"ҵͮqkgAS }.{ |^o,_w6ITI b R=i~`]~ ]bt^Pbn ز{A?f6qO@u;ѐgK߀_o1 ۟_v>b1д ˢ>VNBR}C?)µk5s'trrAǝ},9p]ss8wNYF5BTnT\apzkYٵd>6p9>bR˽ T.)N޲K6Y.nz^OZam @sJv(²KY Dd1y2 ƶ/_+|'&-SbRYebyS𻲬( z Zto{C*t^Z+xnnutW?QW!T몌),x7ps7l·1\6BF~o{,pS^Eʘɞ/tv b y2[w,ٷ0hkMjk)1hx@[bR(=:3eU;d!yc uYviޅ 9S4PչQv,%*"9r^[`/B7DhŸqutT?Սj`APWU un{vBjeBY1Xk}=N beBY!4SnA-ʶM_ Ý|iǻ_#}i +|-Yޓx2{׉^v< ӪcTn>#ߒxߛ,F՛c+dtnɨxF洄1rkyzZX3a];bL6P&WaODs IDAT*|>6[ b7}oeI7:iq}R-qnt^PvD{^y~!'OlCoڎxub#d9eb'=@xJb ITg~+ďoze!.ŀ@P7m_WYo/Af?xsk]sV~"rХ1W6Y":n}sl?JEM!~(|2eV߭RDhO-}bt~qU}8=_}߭[?=b0cA]Jwme.A7FmϩvS**[.0v"?df Je ;)|Lr,B&cEkb A]DN~}(y&G!?z*:yhfTwXnxAȢW_as'M}*._B>z2!< АJ]bŇ/2OdZ-u^9g`U;6q%BTi1yu'D+i 2(h{Zc :}Fz3Ϳb %bLE醷~t1X BLz#>S[{ (Y܀|d.nv?㱷T[Kx>iяt`LA,f,+Kc=]_cxi)Aԡp?1 T;9`5ӄl]ì\]iί1ǚbA!8zW?}}4K|s~@Ǣ!)c,&:zCH,[2TU$F\0tf1',v~* v1AC.BPP/ыJRExvSH0eЅia,RXl Y՚AOddy3w׶[Toi޺c\^"}<ںXhq,[7e~+b|*sᆱM m q<7; bٰznvG EQQ9rZ_/!k=bFu<ؗMޜ5q]/ ,*b

;J9-/rg5,vymv筸C%}j S+yTlI8by,m)BPŅ0}˷nuKΦ\!BKh Z9nv?yLm)BP3ܡL(Q.S!*HH(x[;CПe87xV J#@E`1Tb_u<9xvm)BUeWʷuB?3Z_.&V{֎POs(~\]6/pù=blQS6RSŸwl.-bPt| PЮ:iڎ0@cYYɲܒYBLa!*'l %)F6k,:N= [93hg] A;,GX,n)FCP?JҦA' El;Bdn63tPZ!]Y3ƶޞcdC{o剷)5Dž&T'IXb}KP yu/!i1?@&yBb,_mڶYMY!a::o!'!:_'._BlO~k{؜ P xa<4{jkM",y/ZmA6nU;Sy3 2m/k2P9g,`KuP>!Rk"1iQ`6 9Pћ&zn̖ydƾ䫃 Edntq?tO1^1>2B {;~yAKuct^B [ОHI*>kA2#eQY=-/n8wR_"E c1&s'^2yWK^u39 v9]M?E־1zw5~}BT%M ՁǙg\cm߂.K6N{v+B2g7H糃q;n`+S0йe1Sc%hjmC1ܿ\h:#WpC1Pg !`sBT~\ 9y?^Ғ@Pvz Ig- G0ޡL:=zQܼ7<ƽ]DN}^֚D?"y% ~_y6"/IDAT vJYTl9fW?h(4a. a!=ksN^}8R&V`R1m T&UcY>kd 9xҾ"!*.%Z}OY5&¢@s>|_4SvQ-x;f_#oPUvz3~1,^6x3QƖyr$kbP_as[Zk\o @Az6neYKܜY<,)4^/dc@!-"yH ǂOֶn* >hjG&1HKt8{q%EAj7z$'@AܓTɺ"ebew0]fü B9?a\blyhfm?NƼ-a1*lWbmo c?K7Eh#Vn0< jqpx#:cLp1_qdUk 6N"Оb P@w f^s7?@2ki҅c5╍W*l"I/ O|u lz9xTj]_ۼp➥J֥oq/v)9wx~+$(cyFN:=R@0[]w$| rQ]@ tv̵6/d[~{V6HԝbBʤJt5wq~֒Ad8F6|gjŘBNp2½Ka?6qN|g;h@WEyژOvbgm\h;.@95!x7H xX&8h7nIwВAL.7M_y,STA( kL\Igi}sɏm&|{<$R'r$Oc&p /Y#sQ8bT~60O}~~:Z_nӟ|~K%KWvHtVBF-(۞ZIS,TW"Np{8eB/v5=;C[!uP$Ɨ숰@´#y_R,ƕ$}ͻVS2GvYۊ\!*^)$]w ]G\0\w*:Ztc4m3D)cr&NcdcKЍwUjN  jk)E.}[n)[y<ߛCҿ* d4'b|YEԽCX4h̦'l 1f;}Esc:i;_W׻1+@~~˿2nm"2FP8<7Fks瘡c>2pJ6/ 1TO`"Eף^.=F/Voc z$*c1ol3z/sᾛ|NS&V\xe@~N={B"c+. Dt^={\ǽٵe-8cCQ?+6V?5^=Q҃M{am4CJ!, x$}^)F-o*2Y۶N|Ejr!E?!X r2Ii;/wn,yn,X0%PۤL֧q7 u}4`򈜅ʤsR?#g0sgm$-41f/u&?bBD咘\yNpW& gޢP&W R?&Q>ȍ3{ύ>פT]Fkhtcc_Fٿb>ƓPXx%eRyJ-yiR/~I}[",D) <t%,M/{mʠG3S3iRLMM-=ߕOO~S@  EBzdO>ݽ#nMɕ,)l~C4xa 1.ɽS~;׉ eبN@2C٨}F}z\6=-*է(%T Th"CD1#~.]o({~&cH @##y%̵;uu3=7JS0@'友>)!i4f=!RC3KlTR ;_A{dVV+|]}ʪ(WSRj}~;h|x&bs8'0AR.:9}&zZ/[,{[qd*Ő%- ?!7h^g"} ̢ےrsf_ƿ7*_+?{,Oq g-?{}d`GU)Z: ݐN.kfU*y?iiS/ivy󃿡bLA :|yd9lC:{7ѾnϦGv./NQoU}r*:ZYux)c Np|A\a|K`A͸oSM:-˔j]ۀ?yA_qڜ/ڜ)-SkhJL==>ve(lEV[S?WӃ[sz>[2EkQ *덐eSR7F7`d1AVcmi޶fs+mˠ~kWI39yҪ|㖽RC4ߙ[UOO-Zrې!UpAG1-\w60~Wrq>{ЎUɺBi䏺6r!y˞>/˿АEl>IK6)ycfwf ɝ%>1y&=W'wCY8޲,I 1eBeByC?auOZȷXMъ㥔5o^2/.XkD;3y^*&v1orK)ԫy⭲o~\;&ꍫ14&74FÙ0-uyl.bUhWFH#"DRIgKd_s,pIDAThO $/=c~mVqSeWL(~!< We ~ބ}:BC'Ы (r:^PMlϷuA~;SGOo˕EX *r/ x2U7Z, :g ⶚I#T\?m 1>UKk5EỌw3-C'-VQM}R/s$ݜ,vujZUIOSF[2wsi #=EgL6}"RVIiTՍ[cyΡB鱇$={AX?p͙6~'̧_H3a 8FNkuPk L_F TU:NGiRL4;g"ryg>^km7b%0ybӻTri?/k)~UavSRݷ3~..mt>ͦ!,G30%J%;[e 0=`LeR]x jX?ޒIyoLoɢv'ضf(*jy󝆞8 .q}dH<-j6\AhtއZ' @,<4Ľ5po#_^bk׺0dOvSa3R!k(GcatuTMϦ{ir:W-MwNG-z"afT6`<8sv5"sy 3o3>R5id:֧DfʣH ڝ323~]m#5t zz|v\54 {|-ؠ ~cKXeogz {q@7,L?Op ̢,;G)J>]ssi^:]F֥29Z Sjn=QCbr]|lNPlTX#D^(۶ra۞ۺtrUVJ_=IK[%k(^n2QOk9ٟ_mŏ[Tilk-{|YlEWeKft_UG=uUV@{3u|k*@Rآ ̃X׷Xl+*c k΋y?0s~kӤ|ug̣~(*:FUTFUTѾU^^'U)>F03=EpLa27/*e&>{A'?2I@: 5M?4}̟! kWGj膄lk{._HKVf k$Ç:~̯GI   mcߊշ1:^@U!0n /f2dpx&F/ͧuHrug&=<ږfFˑlVCBTAڳ]o%=.ƗߣZU1{nL!i؆t)ʢvkhR1Pfy0 , RKd^hɃOk_o*:U\Klo`߶:WV+%uV.uktY$&7@37`<@}FBL>F\!5}M^3ȏU9QḲ׾+Ui{z,Rw'U4RJNٱ]* =}N`e#_Bk˥;~v8XlX^ץF# 0??i|Zqf(NCSg K,r[τ5oRngّ ѢS%}^8!<n{f;C7˚4rXJkHG҂ohY6Q6O/lڝQEswQ0Kxy(A;c1x >KW 9że/XΕo|E4UKRr:_M5IihէƘ,v{5/h ܋U)̛_ݞ H,Vm>;!1ֺ|Zو.Mu[^:UFNZV7,]'|v )`^g|_==B)" B_?b#߯o2:6:ߓO ӆtP:}_-Kڙ'%|燈ௐfTX{t{&jVȧ3iRt=һ )J:[TC% Tgr}x?ēU'蟉Zre, 68߮glњA ]MoϓrHS^Kz4݁.,A%>{;vt~'sommc}lGfl#7!|ύ?[3wd##5\wE\|9UҶ:QcSUT7Ъ|=Y,{^rf m^oEcJtg,KG|56PcUIt]RmUKo̯߈ge7C_x1{v%zqUa_%Dd[ Xӯ|viҶ;=űR&[O6/mo}X]bmN h-ܓ*Q6U2vB۷\D{ļYp\ί‘kMUxai$ß5QYtwJ.W@-t KOԒ+VW/u }LJ=%=h6+^`:qV%WQyH@w~.&2㟑C/-gd~֙Z!IK֦Iע еT ;^($xq߯O~/Ԁ5a9ϋiVhT:U7UIDAT%6e#J)*6α~nUR}S NQ*a>=صD)ٵmt"dF"$S/$Vo5<|vBL81xK[v7 _\?8%Uo5i8>BCshE~~,\~#H?jo2u4oiȓ]B|Ye}Օ'3Nc b eBdòH_$FvcS:Mң%5VQGņN=FP'ձjz@Tocctc\6R|1:?jsEp?&WƗ-CH@cY<82o˃T6bc{.P![!=;MlORu>?įӊZy̒ZJ?+C#U::?yS6:  b|qotu,G2ؐ.1FD*;ohթrJI)1ڇjY.Xڭ»LZ0I)rn[9Ȇ\ET~W\1|/,lDt}l=5)*J+jᅺJh;Kfyr1CJ~Q $p\)xu]˛ph;r2w/zW] Kx>.W4i:.i[NHK ҈ 2+ohrL4k] ܣ#$*;|g W؜+6b$"X@Ӗ=WT#4tUt];EL9k@Pwt7Kx}0y4l ܒrg׀OMU%aF5_{< 16f̣GKZ*5eQmW6_k}bhS}Z(&U:uxe$WL֩˪|*S^dĦtF6g 5QYt[r=':RLkN.-kd0ڊ:d)"4@;f K[ur8y$+?T aE}2KƄHʡ h*_^UUc>Cggϒfto :Km *24 })-߂x ښ ɫ@ל/' Xy}ZZj=_bl[~uom<Ρ3?R+)WWO5 TF:XAZe4 >ys/ e_Ժ4456Sޛq4)6nOɥ'(st8[/-j UFqUӒlUTB=3&d}*KVp/@e.Wj]=S ~^ߟ,zz[."JN:5oD}jNWDv? ِ? @DKt9_̼\`,6 @0}Gm] ߘN7eӜO%~٢*!2%Ooϓ`5y(NH<(D"q\:$s#~>GOK҇?,z !ʤoUhrtlE( SU6`剃˯L2=GН 36ɝ?=ZJsZ|?AЕC,~bѸ>V?ýr,(*t##iEvњSRRN /SG̣ҥ%vŘpXu\,Nz9 M,{1&0f~#,Yz@]jsҒiZ|6ynLji?hwgK ps} ]ƪ|},O\`7ִ_)̿A!_lnu0V%cc~oD2 @7$/B2* ALM&MFG_!gV^^}ˆ7HUti/$nOR/R}~^UA~)kX:  *S |?}ߐ^a.q,Exv O|d0'h鳟J(ˢCPPH?C4Pq gˍ}g}Å{ECy(ּ,.K+ texMSyߗү%Tn*nloJm7a@0MJ{iQC kE0kaR[U@ХcSc6(A}=W(ؒ m v21rˆ[^%u_Ra+bn!GjRW?:Mt, @HyCk> &Ժe,BuFc׵i416֜*rtTu}mQPL69NfDJsPT>Z5{{kGTW]e~CR @Kx.AK{ Rp }sŵh))44`]1$ F,½5 ӨѷjfExo\.(OQ/CP'2t4ww>)37K)^oG̽5 @Q b @"<ۀ]t 43 S M2%=7@{wxyy¨;&pZu2Y@Еx0|c=M![@_O/.ᛌuQX d}ׂ[x2Y,Dh;L)ݒI9PHH kr R?i4f3+¼yATF=t @ē<tY ߇omʲY B|/H~ AP[JSYG,#54zs 6R<\b80j~Q/B H|aҔ,Zytx Am k$- ېgp!0eHz |49ŷ-Z$tEjj"otg Skp.?#n_ 8%,@#AK+OJEK |5 /%yFJ ybn 4vshO@Еe3u Ӑ<AV'=ӓޛ2HɂߗZiKdMlR:8 uC<b--=ZJYxA#]ŃRSmkB@(܍ ؐNYK%UX Ƅ]\߉c{^&uDЙRFP7/M2豝y͒* */+x-ٯ @F@gẁ 'j=wc̋=Y0#%~.<$*3ƒ41*@o:>X.͖g?i^䠅+"O{" 6fJXtshڶBZKQҀEzbC>FiLy LKՓ&\!_4)m@wi*v#@H˥OvR@ @Ou-r<&[n˜1 ^{yOKVZ(E z xM|b D.3CiAcvlzEpw.W>`g 'gkSj:'RDP Sx -3NޚK}_BkTyuT(:]l-PKڐOK3%-56ގ&r1O^>'&h5Ԛ<)*Ǵ9gT\k`,za[!^EMTeȌ(o/єr2٨,RT. ^I"/i5Rt.pBO jJ㉁b韩"@9[GuMXοӛ (rl@tDŽg/I/i"O` qbe&F\˦vQ|FYpx:Jͬk ,F$W~^8-@#[ﱗ$?RL>`C): NkF414u]}3ktěQ9+z^k@\o@#:)9ʎ"&*q|:Zhْ/sPm @B1b wO`>k=*K wҼY?wޞ+2[|Mo.9(=F'UF_(ⳍiQX£٫\<[~ӌ؂cf4 W@Gޢ#2!"@;-)9TP"Ob *ṝr*NK 0VZx6{w\nqW&Z. XӶ5ܹS ^X.`MϪ+Vp2N]a0yw^ACePoOϣ*ct 6S+6 y%svHQX=Z#Z<ye\J{U:;(]YMԌ OM֊VJIDAT`9MSj;9 WtYߔ/'9PP&zb-W?`' TkuĺR2q3` ϙk\c)Hi&˲ r\A#Hڤzb5PQm f,P\[e<?aO9&0p/z1A{.#yq uL~1#4 >) W"\M| kgPZSK%e8aTؓ%͚Ԫc«gg[=iuձRпh,10 噤h2+F7јSOu*UF,G`'K›5iF͘{ƭ^~&3sѿ5_+ +/dEwc^=5{ ڡR`D}0Uug.=\c0akWm/ť;kc\th|L6=L@/V7 uIyIgKF}^Lxh kg'TʭV *mK9Ak{;ݎ)'Gs?ϻg} iKܭӦ՚ 0΢";Zs{ԉ4Еdg%Þ b'he ?~l #˰h=c T5*?*i|lgA"==w >?gMZ KkפRjF\T* cٚT7[6Ѫ'n[I-rA/ 3),JO/o/{IiǪyOp `6X WG`Ӧs 6N%E.ɦTR=]\tIt/#g^M;$w٣e?gLǾp:EtAQkkeW6:F1x= v%^44cF>hjr}r\*kM|rX!?`f/`ϭ}.mf]\Z}x/h_q}ꏲFRh+3QX6Tk7|?Vx8l\oʵrɘ|^ 1{.Ϥ b)(glE@3zzxC>Q?y`?1/x[أύ:)txL: 85"S8bړWGxޕYCWg*@0cVؓ;s\v۵~I"o'F} 'ΣZZۨ\N,ã‹w'\qE%*g PLSў&cy&ݻ1#;RmS+}owON^7w{&^`,vǿ.:r ?ME$bT8"7rv˒ G'ԷtKj.pDfeզVS=/ :]\ykRK8+b4szBן t9]׸ETqЩ;CV~:( e.SxSi 8:TD%Ҁ,~'u\qt/rty )/gVTIym2/ AJG/|[Lœ[+4qd+K=W_\3+!:<&u\Q .RI5 p.apw)4tY =Sxpg|^{k+H|\z3l>-H==1RW]XQMOk6knZ\Mr7^{u #rE" iZ0 4*j^6|]7`r_s{zת+r5Z>z x>:MRůj/<"UL eÂΞ鹝Ei`ɘUOꤲ_Cnvf hzU \g&\NN`мwM]C+ >[ilY޵!fȧ/Xfg/75uͪHC5BLgGȤ;ёFsf\AEp,C@,9MVf|"{og|^5u?Ӧk,pXA Q-\3z@c1ze&*=V^v^~ٺqC7l/ t3P#hŴ'ςVӵ R ґ6 &<)cUn#:~avCJU ^Lt*|2$-?TA 8cqN@9i"eG?`٘<5".tC m"e;l/u YG[H:;=PR zvG*Yln3,-1u4f%N%[CVQ9e uk^Q=iJC:MJUqA]AE~"_AצQjF\ZIw#Ơ kw o ^r83>:9(xڴ2)*G"yȤ cR`!GvgO^^D#X4/9Q'U|F5M&7SsS O&WlS.yNYsL)$vjI2[`T5Y{̤riš *^5gHQx^kRml\tAh*,E Ց6${ϭɀiRd[hf~S@!Y nGDnew5N4dbTxRtu\QD0*r@(F anX{7җhJ5T9SzX4&Yk﷛tn'u؉~3I5I9f@9Y\Ba-ˤ=c *jf[6ӌE4fNNa˛KS*yn}6]ڴڃǗP".ZzR0pKěɖ^h{&DiƖ:T`n7QX:#+g>asx|WȲ_ DۥxS{̤)Yz:{Xg?҉'6m^!{{,81QXDF9@@AYRAz&`l l$- 0GsxecpT]6h_PEd I2݆MwҪTjn/68-rAu3-8XN!z쿹g'HLGR;P˔ .**g&Z>Y /~:zlKeVuͭ3^D8@?X"4)Um.3/H UaxsI1ylp]K#cDtױ+BV9)ǩֶmXV?u^\{Kg):`Yp6B̢O11Znc@ұj: oz D?`@.i ^{1*6KDCRL~*2 ,>gE/}[L}#;`Mc+SK/(1u4z%I9fs۱4oV''Jrtt^ ?xi].vi*m*莴\y_ 7݃=MoZ/-$&zM_CuWoUG$N=txc $@.K-D@vu3=Ml||-j 4( ,S?):Q17>jn5+CvۡW)=#D#_a)šfj#\ḪbE&%X$x?l ?۷CtN'Oy>]JH= [)?=MxǛ NWNaWO ^?SR< pIL6~/"ؐQM7$Q+ 4Ϟgn]P>`BpȚT"Gq"{xCU:Cn/+b ,ދj0ʡ0i qy$dtHO<)mrPB/X&3)$2(#}e<qۋkN> a,h[d3ZY>rgbqE$!E0ݢ}|L Ѵ y{54[FPJ?aI\G +݁KƑO}`_p8sl 3i7~ԤTa{8_YD5P_Dr\!{&NXk3:k^զA@hO4P/Oͥo]?Ez2S`ҦVѦU~3w>H}^w|ERɁ&,< }L6oGݔ#0~kPX|_x𮦠~o}x;t\V6lG?kԚc؂F=xyYu|z')ky)4J//0ثسw*{ԂtbyaѧIJ*d %&/Ȣw~*M77))i+&/pbk@wcjYhaytY uE_?(6p"n81Ӳ |ڤk)Jk} [a X£&,]ÅO-OVV k KnWԪ,<%/јUtAZHeByjcycl{{::TU 6mc=׆nHUdNq3 6x=8\G>+3lZk%6O:gr[i\G`9KGmh 5G9tiP74[uh:=\ϑwX]89=kALknأtqL6Z0yأ’ h*sۧU| W/UTEkR&! OsN}_&JO3-"}qcRRS^s_3IҪ)Q>/2»m&NWzo)&):E^ *O^rqٔ SG=1r?jٓ7|WyuaPuY 4긢 GbLIDATљ*:BV~74&dktQ@i7]x9w rritQ>"~W,?Mw 2WQBμ :r0Q}c{x*s.K0Oyug--r] C ]ߴV6RSVv##{}PW'WW׫ Ȓ_I#WfҤlEQM6PM-kzC+2qAc)s<}6VW: AacWoR!j[+-mmTX,~戟U7jN~tubQUBnw<1ou8fO]İMm؂^`LƩq"GM%k%m˪t2ˍuEWC7+~fYIڸ۟']=%ls[C½ k&tE|_|k~'F||o|t6Mͦ 257@B5'vLK"<>Y_y jOx膖B i8cشyʾ=g/ &Ar^WQ7݃Y.ػodg"pmU훍0 YC8#d0Yߔp-ߠhp5|[ 3)5ɕ'߰p.`@h;)Uƽ8wfmnN6dLE_6d pZ8۟ u|/K~eͪ<mps[9NeaVPTN '^j|?X) <ꕒfKCzuRyv]}>0t #Ͻ> /_TElPIѹmqŵÆq=8W;3l=|\F?xvSj3i_+< tc3?ebnΕ:nvPOʚGkڎ/v'm[4ilg^Q 3+z6._k"$:Qmz]y#|@[0M4GW@t_WFF;/_ǿ\x`&a$A`шѿ˹O羝x;Rx>J_gr0 }87o7@RNrCuC7$,C7BVg.wmܷwCI?m]|BV])(V`&,W>YE!+X5r8 ꬯Ne=q/^N*_M/@嘎qk׈>| 󃺨ym=ຠ"%Bᑿ5i5ADwo.k{wh%r@^ eRreCan߀,Y։<ǹBV^#/d7׉>zmEY_A]Wce;vԌyURL^O>RmԌϟ}ug}8 vswKq;B74_7C5Nk74שb _G'Gr'/kMre:0WD-y<}sg}6]qjRXp0դlo=0|ܫ@/סZjB^/-5K>oxg}4[ssvH1U8 nQOyLa?dF 6w+fo / Y=}8ܿr΅:L5Q=ru\!ᯍ>vèsT>/hсHNk9ιzFMby:kRc&E)K~pu::Cَvr*W*lck<}q_6C?V5N{ԡar O~rM ): 77n?d0>S:s٨}Mg},j8 rU%ojVb zM٤kHqo{BA!{X;mc{CXGSPÍӀH~5=A '!nc:S!dqcʇ$SR )3 {qE?S*y7tZ\tAzTVI,CqOf}p&hi(4.7hk'#r ׁC==C9ϝG)-U3+jror ?,K4E_\qdϬ9oEigOy_ A}NVvüߚB7+n6f&^Ӹo>.tC.=}bE9} AN[Gi13?%E֦t@D|4/}]W_4|=z'k߈5ՉʈR񠴢b1L6TJ*O~s>}A}]:b1r뤤CRAлix4HU1#Y/NJ'x^exV:D6g)ue~m7gЦ> > nr% BuRL`I ᾉ3*l3}qAЩt)\_4ɕ(- %r'D_/}0n)trPiS)4# ހ11/=>Y7!ra6n[TJYߔDD$_<ҊHIZ7>e*: :<<.{X~ouWZul>!i'I=D_}MGuwA#x8YLj7rYulGئV) 8.83e}=!HI^7qYAPOŁig[~,諽 ?ŵ'6*k}[kIDATOo?}+ξ)-Mr^~P\3= >دq_}=GiqyM]ߴV1KT70f=wp2ҮkЅnh Ǹᾧ> Ϝǽ)f_m** 4}KBV9ׯTQnYATu6~ S. IIeq1o@ :#}?ǘg$<:z?! L}Mg}A[<;}m*$pXZ+j" `Fc E|D_! w ZW}JW/C9 /65-&KѹƬ]B0?/p/-66.C!ø/Q`,@6j&*=ϊo< Gz c_hSkWO{ ;o Ȃd0=W'WD)f"sc=(3 @DN#pN /p3wAP@j@SU^^\518Qpmju:Ħo>ݟCB1k6 I~d+':N`ۥ> KP*F]wS?**:X[(6w8g3Ϻ:X>`N7 π Ya;F8|ÐoXxj7&:ryO, Mgy~6[vp6wAй_s[V5 !AV&Vg NKXga~)5ڴ_Jgy~8SR`n5N~ڭlz<0)))&`/<~c5Ikk_zZ~g}CP?T' \k8jgKE߄mrMJ*a6^gUZEaL-fS?i7l}] 7$d5 guURL~'F*3\'ju噇 e=>袑bvT6HyѓsD ڟ5 "ɣǛgv ? l KQ']s}KnH$U棃hjp8ygEMr!UdvRovt噆 l4]<*t=ݧI,sSqSH ?н!t Ϫ; :$8X1욧q7v}Cv]1 ",Yx^֗X^)گ"??^vÓ s%+T9;d͢ʓ9*9l|l3&ZRbDi1w?AP_W'AcCΗ䤽ぷ-yLihIR)I:)`86 )g=s|/4uijl[i*Qߝz;VvY :;꤃Hål=yb 4 %Ck=G@F:<rVN{?-fs[ihLBYRzS؍ tq2Ϋ3> *TQ`:1c3Ȫg~YAM Ad6YYwegň^O*KjR)U5)\@͛F_\Y)CU\~sS93AԛdχGgS%]*E*E~4ΰQIP:yexVjX*E"%M Z]=utAdpi k)^ð)^q5:lq覶lmz-iʌSiQXܦE$6DׇnRBŢQ3xuҝ a=z>KAS]>~q_|עy^t@tz1Bj9` (NY WܦE^x `wZAu]w bCŞav^7pW:A$q *ňUUAeYvेBPBtuRtnEehCeBp$-ncָͅ,=ph)~`DjU"Jޮi OlBEaw-vC&L2~J4BG)c֥Zheik[H:&߸˔yPpHavtیh;܆4Ib_עYWeK؆%c_\ =^vbbg4Aw:m3Koᦫ|?w?y+.>pIh+s긢):ɵ mDwh FpFÁ_8>px[~faATNֶ#:\pqprRZM:j&w)R;yU Mu "F ~=>9+o.mDR:rvc{ꔚ!ܦmqf#ڜ=APObjuZ|0χ޿7*(Ba&f tR'WWxb(OgFn&߸{^nDmx>=# ^!] Cf=+LbjnrN\U&̡J44GL{X(Gƣz|o{ۀDm N1*pt\qw_rmS'|ؗ?Ѝ-1MZ5iTT1/%y 2?;1Kcܰg""[Gr}q4im>2vg؆?~y3|mۂh6tMAA]].y`ߩΎgxsmOxr %duRL~B.v\|b %'={%>nqEo홠/wŏJnx&m C˓fֱiI9ͅrד S}bGf7şkyQv̋_ KTIJ4)5G5IIJ*MBi:M_lLO 4`n/G =^Q)3_#V5K*R'2i|Z5]p1/,5jύ~~ѽcgEMtagCAZV +AփXۊ/ٸH4P̩Q&|m5k%]WSV\|MJ|={ #;Atv7bV1WCo~f34РKIDATaɂ;4cЩu nnIZIפToЦ|M^VWV,A6^ZM{? <?yDq}|Iw9i63[ZyɯQ~U\]M-d[^{F|kׄ _#qrd|M5kל}OA\=0v \ !AK_h A 'x4xxM|HX+E2`^ȪeDN2"7 >&-0ɩSEԋ6p2y[?+Onƿ_~Mڌ@NТ-oMQ}ޠ{okׄ _q/  y+䨞䨾|4iK0F0N(P  .{j{N2-mig(ujۚO {`v]m&&AVM F[5Ŀ n7^3;wɿSn[~-kjz/ T6^/r5k! _y/  4 #g*)xCY) jC'^vSсag hHMZVnԏi=aDlRӖ Qu=⦍8+f=IP?NR27cFu|OY e9]Q q_'J |Gq,vZ܁vF GkSV z\5)JucQ^$QEIQEQEQEQEQERU{EԳ3p oFwݭ-oI\z>S:1V4zuiJ 0|?sx hnXtlSWwfg',w&} +w8UDrZ鐠BVt?*ڸm=%L^"ݝ|Lח֌= 8FVXZʽ3!s\J8XzO"z=:L +s긐~?twtP?͎+8#tet52kX5>ݎq½v HmeEܮ+vu*Ш˴QEAQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE!!A$zӻVG.úpm!SvRI'escf~^AV<^HjZ򮇉)9>fQEPQ@Ru-'Zuz?MԅP3Z>'#Ҽ_6>Pp & + ѥ M3j))k=hc ( ( (֥O~ y lAo9(}qL%$bG*{4`ME-YɍJ(KZQK@'֖hJ(&=wԆ3]&z$O+OGXY)9iqeӛi"O&d fM=:(((((((((((((((((((((h/ȩ׳*5_oJ?3Ϟ袊OJ(R )~ Z(KE-XӉ]FчOЪ¬XwORщ=Q-(aEPEPEPi -!%6B?j=gfyl [}i (-wظREti`gxP4fO[_XcLuUUO~OC (((((((((((((((((((_o[axE-WQYQ^E )E zO: Z((T _c[~Ɗ;+=QEXŠ(((CJi*Y,Pvb?Ful [GZu2RtE-ZO:4@6]}+=Q}[K0_sPhX-+cl'j/F(fKk)IҖ ();P֊Z:-X? Oa'gҿj{}V/WGWbu*w}u/}-?Ɨph_4kR/>k{G} >VRm_ʏy[phGBjؿ_ʍ^}~}+潫Qϱ`BAh_6m./K7$r<ЍAKIzZ#EKE ih@( pF=jZNt/42\cT?/WeGھ'nmVO~}+=Qt~Tן'c_Pvs9En彺M+f>⢤=)k܊XQEQEQK@ Aހ8[њ)>ӭ8P(M)1-h:Qҝ@ŠANioz^tKGJ^QAKE'ZZ;QڎNKTE'z^tQIޤ: (w:)^RHܬU )jO/"je3DBG#hֹdKO'?]?.k’JM.xQEQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ =kZs_ WLz3?OU%4 ( (NQIҁ}h/zQ02k<%4֢,O̖:ʭh\.)T|<Z3-J[ц })a 8 BU}CIb1^:>u? OS>t]< . i~`ܴ?S{~ƞ q:rO:/j;RGA@ E(NZZ)zoZ:)RTI֖t):Qҝ@'zZ:PҖ(z Zȡ3GZ"\kk©V{t!QE#P(((((((((((((s>=+SkZ|{"V\Cz*f׶xI֖ NtFyJ (P-G7> Rk:dozƲZ4)ӕIrğ0Uc >0tъ^ԩ*SqNԴR5+IO#T2=A4[=1Ә嗩_}nڢ$6GPA  UR*r­(ԍ͂s];3i~e ^9ƤyyU!(KCzR@tN(-bҖl'Z^tP!)-:K J(-PIҎ()h/J: Z(nOws?u{?(iFּ*gKER5 ( ( ( ( ( ( ( ( ( ( ( ( (z3?gǿ%j1N2KўhA8 ;Ҁ-P!J('pN(AE_VgHXg} ttx}.Ft0_F#+ pA 2+ߙv;6dQEr㚊iR@95۷ghsQUң*M,s֬u=sK~Uv&KL2bTg"xRTl}AQZ5}n:5Q]:FaAGQN-QɄCۡfQKsUA"*+(V{6gikGvFm9^{{t5ׯNqhL(KBzS;RՀiQ@ ޖ Z(%QLGzuRzRE(RtH:Zjȡҹȡ6Ѝt^#gKqC袊FEPEPEPEPEPEPEPEPEPEPEPEPEP+Zs_ kkǿ%j!NƽQ_3ZZCG^=hҐwBE'Z?)0 ^ǣje`ޣ޼JPH k*ԣR#X9AɄ {(e&YUA"YSхZQ3~mFFm=XI=GҽzsHDg B\t(QV@QK@'jZZAKH)izӨizPږ((E-P))Q֗8/Ɗ;<"\kkWwGZ*|o՞/>}QH(((((((((((((i\ǏJts=+ScCz*Ԟ8:RBzRP+ bQIڝ@P(@98"W>7ʚ^ Cf8{ּAGjʵԍtnj77^Jf???z+ɝ9S,VHԏ4GEB(-ZXx$P+ PEyD}dtl}nZXx$Ud`T PEU*+*ҍEgJ׶Hͧ(OcϡҸzNd'jAKޝZ'jZ( TRuUhuK@Z>RuSR;RK N:PJ:(^Ԙ4u{(7kkĩV{T!QE#`(((((((((((((s^=/SkOZy"V\Cz*৭(#ŸROJxl 4RuR@NO:Aހ A#UO򦗩a{kʻRAЎՕj12U9]LgZx^v8'^:W:r.Y*Fy>(jQEQEV46I20*C PEyC3i*:O>C^ۚXh9Y b*U9]laVjFsi;^#9+{}n)ԍHD*BP,RzS{RMhE-P)E (jZ(Q@ GOpFֹq?B:׉S~|C袊FEPEPEPEPEPEPEPEPEPEPEPEPEPZy"^\CҞxDO)"FU~ z3ZZC֝^ 4J;P:֎tQ@:Aހ}(RzP)8#<a|麜,S7_O_^U֞ pG9F5#fU*+LTx^bZ:gNT#֧R5#QE#P(KEV$7PR2=A"񇃟Hﬣf'Cۡ`f $k=ҬK{t]BR P ָyTsJ>btΒL,JYQI*gR,BY@MzQKWK xDVu3yi9}_$3 sK*M: :*sMI=Q-5QIڨC(ڎRP=RIրڝMNKҎԴP8-!HjW:s?u{_(FּJgKER5 ( ( ( ( ( ( ( ( ( ( ( ( (\Ϗ?LBj|y"f\B>ʯ/Fx;u4+u4hN%-P1;S;R@-Z>( N$p%H uSWVGjrr"N6C(&+?S,_"9<2Pz]T~'-\et])Mt>'΁/ Od̈́T7J+ь8GcΔe-iRdIڔRQҏJu(N@4t{WR?u)Fּ:gKER5 ( ( ( ( ( ( ( ( ( ( ( ( (\Ϗ?LԿBjw"f\B|qFU~#QHzQ^:@2hZuhwGJZ(RESE:ހގPҝڙޟڂX߭;֖4C 2+ԼUXN@.q)[7妀H 5ZQ=iU9]IgI4gr@%+n障,31_E??K\l.D@1F:a/hOO|]G",%a_QݏUio hs~ 菮 PuOA^ [BUQҺU(*PL='VNbp@`QEyǤ:(QEQE%-Q@n-⹁HHea_$Юù&?cS+ځV~iݸ{ iB+{誑x=EYNe,,UsUzSM&(ӨZAI֔qHL;P;Sȣ#]\SЍtxu>7nD>(jQEQEQEQEQEQEQEQEQEQEQEQEQE2ț5ЅtDK)"FU~ z3)Js^ԝ)EQҎ (Z( KҀޗv-78Q1>4iC;SR*2ApGqVoon5 R$w|Wm- dH#hԴ1zí Xh_ku{G! ?kC )opD6jGrs{TuQH(f28(k~.tE),m8" ~=ɻE])F*v:nss8,VKpXLכ2Ѵ S,b$n/jviyᩍ}\$l2G }$ۭ&9h_7k{GSЍts KOpFּ*gK^}QH(((((((((((((ms^:;Sk ks_STeW<#qJ~+- NZ));-QE}iSE NRtRcށޔU Z):zANj4fkOܷˏºWK]g'첏.`?ٿ5"u*Tz^6"3U煞E&ik#(|W࣭Lo6],a6?(gS^k˽.y p}+>}ji[nI8=}tRJz54tgv5k'}%ɅοC?q2Hʌ)+ 2q^:Шy)NJ^VJZ(JzR4K; ^D-QK@ K@foz;"F ~|C袊FEPEPEPEPEPEPEPEPEPEPEPEPEPZu"v\+=kڗsO^ʯGwjoƝڽbREQIւu(Q@ E PQE->QIQE-RQE@-Q@(ҖzޟyuFydiN˚'Ѡњ e1 ,w'oo?ZIcWFVV=A)ӗ,ZXԍX)S{V. n2.hYu{Cz&D|i;Mը|k("+Jz%ZNpLsQPŹ5-_SϯQNQz|4)N@AǽZ\*(=QI֗-1@B5X SOq#[T#gKqC袊FEPEPEPEPEPEPEPEPEPEPEPEPEPZu"v\+=kڗsO^ʯ]Hi{׶x(GjZ tZJJ"'jZ(}i):֝@ ږ(( )=)Ԁ(f@mh>+t& m NW=T8>52ddJ/.ǰi^>ү¤l?7'ٺ~x)0H)Tח6mX[7+x lĮ} ]?Uo*s|e)F]f{8.o-c/q}QH(((((((((((((ms^9?Sk ks_STeW<,Ҏ)ԂaE(IB@ :9=Z(LԴQIKPEQҎ (EvRQ@Z()h@QEhJ8-)P(KE zS:(QP:{'?SpM]X SOpFZ*|o՞/>}QH(((((((((((((gzs"~\Bws?'j_!NƽQ_^Kڊ?cJJQқҀC:֔qUt)iRR(:Sހ()hRu:-/AHKE-Md֗ZGPE#?qWjm|a]~W~4}k]?Q T{/>w_j?W}jq}Zch;u [RV_Wq  O4c^jZ?Y_iY_}f~aj?J3[VWm|>G1}Zc²?Y_}f~ajWm [ Gh7V8Vwm|5/+;h0WһVwo|jYߘ}ZcWo moժ:ȫaB5w Om3Gw).@9bj8W6]Zj)>Ȓ((((((((((((((es9?Rk-tuxDG*qFU~ z3ÁLJizWx))GJJ^Q@ KҎ@QIҀ:(-RcގJZNSKEQ@>} }2?k|Z(aEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP+s_ ks(j?BTeW<:BikN-'j^RtP/č<8T?w,y^ߡWsFOrN_/X/C>}J>W=WF??dh*9/S|c*X/Ck)ÊS*}ǫy~GG,}U4cK4}rz,}yߡQ GwUzQ/_#c*6((((((((((((((ozq"\Bwo(j?!NƽQ_^CK޽{E'Z: uQ@ KE@Z>)z:RE-P /j;RIڗ-RtPERu:w)8P)jlJZ):P}iS{Iҗ--}hE ;-H)iH=hNx;E[kȫaЍo߫=_}(QEQEQEQEQEQEQEQEQEQEQEQEQE|o"\BW;5OU% 4?xҽ{Ju@ GJ;RQI֗-Q@-PQM;-QҖ NN(@jZ(@Z=((@Q4v%- AB)R ZZ(Gju&> Q@:-"4 :)P(^[OgB5:UpFZ*|o՞/>}QH(((((((((((((gzR :KGj:RZAHLZ!ZSK;HzRt-'J=)QE--'~fOԮVV:Oa^x D.TF}O\kƚiJ?wcmo[kyf#h[VWZen^ΐ a#EE^z(Xxe֏YG3&),Bl>M}Oְyh+c](/n 2}d|e%(Q_,QGJZh@:Nb?ȭaЍo jޥ5EB(((((((((((((E G+=ks_STeW~֔PxlGZ^@)h Z):֗zN(E: G֏-R Q@jZ(=KEP8AM(ғ/jh4'j^D;- vZZK ) w&]í%Y59Tʏ#αQSTQ:~ E֙2ˎYv:RSmKsیTUèeQ@!{kY"v^;MPhB4.zq׵W;(jz,S3 G3VjΜ{3IT*):Wxz)Ԁ)ZZQ@l?ozzjݥ8!QE#`(((((((((((((s7CQk Z|o"\BkU<}iQ@ҎZ3IҝVKMN)juKIޗ)h AڝEkؼx.5kB MTbxWaX[=A&lEu!=߈Bru>Z}OU4QEQE*)dHiUrjBq\ҍ0`Si.rQ]LMB.OWs3̲3de%WXm݅--!8RԶ#uqIj/'H"J*i(OnEY$>(((((((((((((((mE,K4/r F*ZB8%:]۽0ё8W[KZt8a\j)O ]yf HՐ%RI֎(KE(ӨE@ ڎ)@'JZZ (N)QҀRuN ;R u)UY]IVSIKS$_ 5xo~aHQ]ZG5+pT]ƉQJ:1~++̯iSJ15?ʳ~б?[  2 kF$9U.+;4ss @}STkl5Q9E:jڽfn._yM_=TtQG:Υrn.id#aʒ՞V#-2+ Z(VLeoqXՆԺqa~ T)6o$wQ^)EU ((((((((((((((ekDussb$W+űد)[Eʹ͙G=kYE?(8(zG-'ZZ:u-PKE:Z\RtP '֗4`-(i:/A@JZ(()iQNQIMcZ):ҎH5 @G(ry>-z 2hzR-&>ь Z}3o nW ;^ӥئAf"$R| j(2a0=+U,v_rǚ[Ȓ(c(((((((((((((((n* Xu*! O(/BY~,|$sl]=^f?r}񬱔uVVeaA yd֌f? ?סůgµC<,RA#E,mp SkQ Z):0KҎzRAҚ);RRE-PKIҖ NԽ)hzQҝ@ E)i{Qژt@R)( Z(Hhh@ KZ"IF$p$'e]g<(ړSR0f?KT* _ ?>^jwE T>RמzBEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE'j)h MgX aeN~+uz/`y/ :Z:ъҝy^=\<*k%мzVc޽KcZ:ޠ=G\`^[f=x?3]%cx)*NM{-n,%#\͝ś칷R?uF%ҧ8J=/VM@)AޝE'֨BKIABIրtREQE}ihEޖsG4wu)QLBc"ҎCF*kkK e'j[V埂5 {C&UaبӜs'cIE@XW|=^O-"먳m4Z'z橍UΨ`'ct]{-"뎲à* Hm\47AjcqpUT=;԰VŠ*(((((((((((((((((((((sQKr]OU`Oڐ%7^/2eӢV?P㸬[dus $8]&Ei!e*4$y+'%Be n,rƐz8˭̥:eR~\6h1H@XFoƔsҽ]>~eQR:,2i?kz%FxKڽzYZG:3Zx<&uvIb? t}J{%L-b(3r>RvjK/qף³´\\l.~cᵀ{t~GƖ:^7Ք촞6NoL}+Gȹo Гsj^:K5:Œν]2[M*,yvvƣTK&R˫GE#IoWuYLsQ׵"B@1ND=Ypx[G?Ej[8'"3Vc'VRU{YTgmOnf>kbº51˟{5QYJI|Mƍ8)ITtU LVlè(((((((((((((((((((((((((((𥢀()h)(? Z(0-RBEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEpython-telegram-bot-21.1.1/tests/data/telegram_video_sticker.webm000066400000000000000000003726021460724040100251460ustar00rootroot00000000000000EߣBBBBBwebmBBSgRIf*ױB@D?Da ;#2R2Mlibwebm-0.2.1.0WAfnord WebM for Premiere, built Jan 10 2022Tk®ׁsŇScUV_VP9ࣰS#AUUUUCu RF GIB8$P>_>︻YzA/~?_fOڶoخD]_H!e:+>FfN\be^wBh`% iMy⵿bn:W0@f(!@\,g:{$VkLxx)X5VSŘmY⑋F^0Wq>߻rN"!S*?aL̟y.qNb?q~CkżY7>z(݈puwn{x,SHwA-5:{B :sӔ$6!@c4 UauP.->̕M9;'Ρ#D})B*t0p쌟BVKFM?V$F~P&'g#/a")bg}tN6Y"~mTEmPٛR9 f?uT[LEOC^(QH(Ir4?vGn?dkX'4f7m&Nj7O>$:a;7߳G0ALŽkiA<,Awkv|o`_/Z5TrLPK l*K jwSW{6 ')f7rS7[!)E5-$E*vn*FIH ͯnBl|ӢM̈́T7u#m?:e_"Sw_[.ݦ.DO##Ϋ3eqc?p2 jo[zZux'8=;Bp?lAK*ig҃~e@I;=0qWɀQׅ y#[efwkz;7Vح53PM=3+~xg5ZB6lEe2dן.S77T,[PR#i{1N.caOPN \2 jb4C`o%m\ V3"C^$yD&G<; 9 ԭVQ.EOvJm. &Hp/PO|˙+C2R(R. A;f%ȩcp|(/Z%&na=?V-b4GeZ(g~y*C hȄSZlni5|XlF4x£}0?ngY@8Ta݆f8Y|!$=.*|=HK,K=4'DK|nS$dφS&iX_&QQTW1=V`yK/hs6mFN K# )"eqnAiAoV0k>vful<%}RNloa.ZW,/C(D+EL1<0퍏F?I Ү^\^"5Ŭ?D4&u6N!2(yR(i92(~R+haleaU!ZɮZq! 6i)+\@(9HFypQ)]e29^<0Sj}f5rz򡎼z#^cؖЄYM=_g&SPCLbKHZ"ԿnցboFC7GòJ5qGDR.Iݸo+ov:!&orJyyJϪM}#nH@?92ž{@ *32ccoMi8RB~$-5x^똃ũ c9ǰFoi}VL{̫{BO qA!fڻÕ_ۅ-k8%5ǥC P](qExI-w30MvP7L@~_D(-) ;0*4DÑEBl7Dz:LꗺvR7'\w-N%gcǻ#gܧ!Fh/ޕ-P :1Z[{Jtȟ a'<6>|󺹎`!#~hUxPJ.4qf^6061]*鮛(s\Q]٘ cf]=Bvg ]GذÐm > rnlT&2C:E*ez7ZsR̔,1 E7G$w}.5.݃]p!ZD=fSs, CU:z[{G c&9`BG2$wD~,B`͊]/]Qۙ}Ћ#g`9S pH%]߄`/uC^zJ):4ܽ/\ctNzߘu3W=u´"!.2"˷\c݆84SR ឈT"[PAELΖ!=y-7^瀋w^zֆ=^+ -5Ov:YB CZ>쀠HkEP/PQ67_h&_U6#G=-:&2S_ŝ '9G-v;96[b[8 P;Lc&sE:ptHqNp,DV{v%GE3P\\+G$>1:NzC|:)>* OIaL~ ~{{Grn΁- fb xfHsh2u+Dͷ7"V~ ?6` ȭMM{HKNP^1+0n@g'7 tߴFp}ؽrvjɤ+bqB_;8l[.'[|X}Y #:qMWfLdpK73lrqnX^xSLܨ׀f-^9]ߞ=T_l&<urV&Nb±k׷Eb /e( T1lG7r%^M>Jn Ii&C-Nג0E WEƹ|JkVߔMr .^hO 9/ǡ-\!5H(b$(} ;<'W+OT&YtH$51s%xG3K 56RU}JiM7R9@tpāě6& jݠhn%]uC ZʁAD"l:?|p m8'D)4z`+2T, iM {qb+[)扜.1"Npa/מhו(bjمjQFj@;}6Xx>׈RZi^RyZ7 #y37t l ;M~Q7D]SY%>oc\9ŹSߖd^إ6{}K\t7\l&IXkQVyZ5FdxOGChD5E@^|gh##c* s2o_D9`MAjZDO0aUpe{$w |ЪlW/;;8ta1a##*FMDݹ.Yk=ZL; V LhNkJyI{@d5MA6mAq&k`V^apз}^h!Wj#^xQ,0EEkZۜyKPOwbk{ 1bbLQkR>'l"-5*o+T7_Hdw>^x Z IUP1_Wɧum\wJTw(+&=Ofx FIapKH ܿ3t(`0F4y1Kj`Ѩ뿷d`߿p] k}r65g Q(lb &:C< 7χ.]JL.6QcHDr> lfGTs֡q ( pR8J X `hw_am̈́UѲ*eJbᐹA(S1[րe!_H=;e#1I!&~-κّENվ" CN3~i"Śoȫͮd㳒򘄀dOCiO|pū nFT^B=Ї"Lfi `E4Ó:2ߛ^v+lJՄ:oNNPgR0qEv@mZ\Ha'{Z?pǏ)lWD쾈_U}fВb o ;{7m{ X|\A:EX]~8y'Dc[{CyUY7N a.q#k 9Gt:p;H?:ɉNo;I:/Cm'$arM5@Ċ;r`U8`ڴSFf', M-/.;U'O> Yt*B f#hª 52`%w_ܛ#"E{v0/.s01-,uGmǕcc?۵t) Dnw&GUm;dD⡾J+*NܹhHд53%1e rN lph:ťwHPN@K#eʻz\=ؗ:_X662K΋mIbbSuUp;@Љ2ۨ7ϕLhFPٶK`DM/BN5_kEfO:>#f7a6[ii>g:saII j6}*(A5чt?1PtPp*kc tʵ:Y=y|qg 7 {짟qrCeB0\?Y\IP1kf&'6sC hsV>Kc}$ׁDo.`A0%GǐP @`c۴Gg |v6QE{*Pv\/ 9/xˆO.ɫu&_iiT]ņw9 +\N {y96Ǧ{KIY%x>^:>9_1L_%7`E7&0(J4U8QHZҘ>fQ Xܷ1J!z@Pʽg1YK"kװƬUtN'yoў?*{̼:Ύ|J%8ahg<ȓ2G^mmQw3w>06T*L:7[jrC=fz~2T1`Z%BH}&V;N:p!yN'^fve#Hco͞#b[(z,橤OꝮ票Ef-&9F^0Cұ2aph%5 K>:;4C|ؓ46Om#[9 7BuylzW<-Хͯ%}fv ]w@7ֵOu"ׅ'^ܼ}L00+Ps.t &-F#śDB!^T qLq^2K &>IX;մ<2ZAzqauD{i11 /1-d,V2p i qs[~IOrg}Ď!4ʄ.fC@Jq@-W6ԉG< IP;9J}~-׍Erob7Y]5 Ǎ,F$oX 5ٞs6rKCS^mDTj]'~X>w]XZސybH sBŊ*l<^*/|no\[9L,INc,Z V }lw ,ၽKj,HV/߅^>jl05p×FHfh@'akzTFJY.{n>;FGz%g;1HHH )PxTVh Xͮr#~0Wd'ِr{,Y@|eAlf otq( T=;T儒H'pV:wu|EۙT%IKmN/%Qҙ؋`H9&7.z&؞Jzd&%eu=zqe!|/#dn>\!9,xVhTK= .:]~b+S+=QF% BU ef6l<1$P<0#Y?A!n= > ktDĻ^lΖ\D)=DBg""]B1}u:c@MrKY7Ǻ}8/\8ebמʽ̆lu:vѣŦ2"Rr$҈l<<jnWZImWEḿdt%jS*󦮨'O3/`RH<%VN/{W~>Pu,eQܥert|JةQib: ?FcӷS 3vd/+۠w) p"2o85t!lU{q;`pJIG<.mf?+M$׃Q+r>4G8M;)Oc,mY!&4KDˆc1m 7&f ݘ<cM?ͨEӷ/>p!NZ)uc;/ggFn}bP F1+ƅ޻c=ȓ~/5UK<fBhhroca|Em)2Bul:y3^"zZOT<3LɫGW[PkPOI#7G?5u=T|m6ul0 g DX꯴Tȍ"1t/>)/^/^'AM+jwMQW&m(w;dŎ\'JEr\[cD=7H'Um޶y /FARj!ȧ^,z]I1ȗYǷo)y?2+ÑWhN}  Xg@[ 1BGga6^0?5's U{5L| +C=;n6O7/Bp8l faWYEW-X9:e{XM!agVon (7 ѽPm}Ut C+ bipj zC C$(V٢oJ爭'-b5lI?, d? `ԙr7e_ƺhnF{.Q[?ߧkq>KgGoW;cY,=ϥ},_Sq&^JH >9W8,4Tu6~k (dP( yK^J}MqihN~z̹*U=>f:{odG"M5@x Fd U i=#n+6[߱]Yk5kF:P1V[SZlY&|o.%@3[#{ázpl=(6 _%mQiDHUl߄uos-Y!! B-y)3WTSKH'Zxf`ܢ | _1LL.{Ѷ\,Fm>P5x7k>6Yn^$J="B{$&qϟ Α:I߼F7(㲕.@w>#OEg&%@2 s1Ra-Q5_흪PI ߉>QtjB$wWlGep]Z1Q5#% HI¹>,X1a!fr r8>58]@iVF(Tf)QiCęSHM3tR.WCh3L#ђ%Va?=wǺX@D+Q?䍥,V~azN;WAq_E%3rG7~ ~g L\A$DQoD8Ox%Tч5X$׌c F|`V):G;ϫ qzF\]b~-:9Wp .1 {X۱I梌$2E7;7gd3=z6Ֆa\&[c4!fWm߀ !j2Y:ﵢ7 OQڤ$t!p7YRg\9d1ȂdS2{cJ{B|< #.Sp&?]5h>eC̛g;Vf C{s󵮍wS{R>0frQ3am0tR-y^Οʿ'H]4Sz6KKg Bxx}dͣJp-'1y6E/kWz@ppAkS8 &3_[W-ȿ"HDK70t`/ uߛ؛+V}rٛamLVAǔ`WU:T$FL'[':]'B+͍wj;{e,#nP@?@fl%:kX・lU&, "JESʭ+QaiŖVDۏISht{~w ?dK[PCe1 jM^X`.\(ƊL;05g؟Ŷ/*Mk=+A)9?J  R:U ()u? h`Q[qK-@ -Jpğ (-΅}B"$H!Gדi(فT>?яZ5O ĵA/[DIhF1Uo L ;s-࡮Q6Ff.VSk1pYb"w D { Uא0D5m$EpqZZ/:]uqts| ;i2Y](+U#?%v(|\Y?^cE1z\rnLfëҬ!nFpF WV'V`?,bHCǶQh~^q8]|ؘ @zŧeͫn+5A}Ú1 wcX{mߢ 9n?::w({&M8ZXr*,е1Xm1 µ+H[GwZKKu  +=,7eBV.}M% /s#RfF%jn&kՈG^ 6w^`XC!3.ỷ/U;?RA+ӏ+$xLuB#1~ijm'A#p g^<8HHH͊˜cm'Y,[h;NoY-' L^##  ,Gxۦu0Vh?#۞?\HߚݧL2uyª1E"o?6}ΧŁ#LAAƬd*w8V$S.I͞F)0hzNJ\xzg6۟:LJ׻`6Kml}.Fxނ :{+]%RYTIL&KxN?t壍+e,twBD-,rYnL3΂s(OMӝGIXWj}Qs&$9K^$(]빿?]q'Dh*lX)aYcE;E@oi|zȵPC& $FsQM.RtS/"QGcy'8$3Kb_oξDդ62u.Y\3:G=lt%KPS!Wx>Lb(=kQ0mO"lK%\b*Lr0 `x&| H~_qäyQlS5zbh֍ X\#KpծaҚ9Bf~]KCV%>5JGU;DAR d<+ſ3ɸwMQ%1>ܞOynb*7YD[7F^p|,Q .ey~Gk-OZ%bNdDO⌑w~V?~`4cyA!s}?<,U.XVUXQg!xijbj»G1H&gQH]L:6_j#vTd3;J2w6B wj1H aR;$Rzj=kBP8-q:h[8C%LUWI1S#{D  :]{$>P񰫘Wp ~&㿬H;BmG52`0pyCX}]OFm(ڡR @+C!phgub]C$лև.`L5?#mP:)jd]jJlF;gS^À*ƌNeoܷ`BBVN\^! :pMїO?>)_Fn]x2Yκ vHhz {}۪ UoPڃ͋!G|Nxgw 'sGC+7|pfFwp>#as8xZqt)9E.朿'Nx=yy+}i;UgGާ}=ropN3 .Bύt%ÂKhԪ3"'3YlM?k\J) u+9=2w"oծߌml W>d IfU0Hm yT[LQK~Gn0&}m'=ҝxT9hJzz :A!-; sxL]Ivk{jTMR[{[ȄيNN( 'miĶG,#]rw߫ǃ"t܊1UOrr p~uQj7 蚑'grHҡNmb_0#_j3tt(x=OG Vb-̆zaL[BB;<8#%+), aW ֟l|eR>l:6^܉uBK>SӓK;9QÉl18:hWK#oIkB@d ZRT–%U%G+b5Ʉ?Szkd@6jByUgEiA#/SvMc:s4cuh^Tnx騸7@t+? `"TSPtE wBAj(i`eqi6 ˮ32;7˫ט-5R}Y@_,Ojv{cv= D]B@I;bYcCt(#N.^l^j]*Xڙ#iܷLDz~)_s0S)?;gށڋJc8{gIOYlѵ#efI;8 'v;Bzx^TGu;n:DhfqG3'|8+*-dI q~|jh9 σ8m .b oQ\*0[ߵݜI͘i.n TqXwڡH8Whe hjV/W4 8 ~IKN\83`Wx9At/F3?3ʂ]83LG%D0(;^돾WRC+c}ߕDyf“ l LoJK9 HPud(jAr; tH%E7vE .IFͱ̽].`7"RNGG?ɻU&L>2M^DC]S?1;l,/ޥAq.v.Ѵ>E<SQTx$Ʉ 7-ě(e 0'E%j^*9[K$0l^1&3vIf :bnn`QnmbNhLy 7bQ&)١S$(_Mk:_5DB@5Z @dYew+iwDbaZ` V :΂iLG_LiZ10PgTJ7~|GG=ś{U@U¼ȕ|3ؖ\?܃ׯZM?O*XdzD D ?b F@F$6mjshz,gmoD7TE W J|X2iVp@4 4v OeYP*W.])IV+JW|5wҔe%j}Ԑorxo:xZ5oK7xkuKxOa]ϓ-)bKY<{W |W?}]+ɉ"@%?pX~X~!D`~tRjp4͜=J!v[bh9V-H R()2_F˱,|ulpr}c'[x4«-]i02Qrav ѭΉ dBA9mOS~d`XX%_,QeuR j7!Mzy#=ʄ{:E͂.2д8rpb}YM%@IP_(RVrO '."y"4>^yp#WK":CefUz;嘪cm)ŃvVzL>Z Q ;]Us} fѬ 7}}̔<6E>\9_!]l6qlv$*7}&> 3\+[ŬX꿣C{(I4{_? c. e:<Dq&DB&Aۭv3ݝpz{Ŗ]ezŹOk[8C!QuJJJIB<8$\"'pMx㊙ =h AEIkof})_/ى`t}3dGŕXƇlSu&>VRj{$ ZPX}Ļ;S7'|8Bz y ®141ƘC)/JC?-z|L/n ?kڍ-0Ywi5g밷NelZ^ ]*t/tmt _IMѵ, GpN)=l?ugjbVy*_?22Tnhg|rq} GEDgsXda~Հ7ۀ,W:] p615=d29a(r'XpU`X (S^Tt_X"tPcn*O̊;^z"D/>kKGP~,nϐSPhZTȩ8meLT[]deNrcY!1;׎aEf+W//CA;}N.QެoP>! ̂wmr#:hQ;5э|@3ٻo5b%ɹ$׃H=[@ /|['JPchFv 7֥Oja{? cI#3 Kn_'׮Glmp|%lHPj8`lxae2}-j$/b#ƿfd\=7pYź0n`s2TC=ʯJG.M.o5@ sF'l=(y}ܱ&BXמ͖^JĭM:Q1"GLm#-i!|){ P˂pg P6l<:Է]K렀sfuZ_N퉾.9iyx-JPEf.M1%mf*-Y^`75J "\VX5DGiC_c!k cjn'Ӕz1I;W;1V⚆Ȗ6n @&va;H5$lrtGg\0tZ@CC-lӁ$JzМ "~X!(*'f9 :}y鋠Bʃs `1ŜEIb&/?MRW3qG6 ظhm&FN@LY<,@בXX>l•n_@WCCo-؜fKh AEIkd^hk^apcս#w%52@-Ԥņ_ze`q(TA3돁U= AEAw`$j9e,xX4v_o4F2A)وP ^dOIh{6MZtE #nנ4>q)! zP=yEK^I`E5#mXKWf$h1\%8Ћ"amª: ^ \ n{Ͼ4.H~OH W DEɀbJd#*/ _5yS(AgqCtQiސmVP3:M=ߐ6%{$DpTBm;=28VOXR>alw"鼢6|dmn+8/=Xi#K}4xa+#="Jr H+o&}$d `ء]*z Bt|UGZQq҅Q;NeNZpM\CWh8lŌ]Մ4dϮ x qg홋|E>{m@Ն3oWг4 l~ah9džTiG$4e.).tU$Bp&v\h;JcLҽP9+ݡ̾.֔V X<n9{q^P5A(o=Q׵oh}rI-quԌ@ R8[ '*EO4˼Gjfnrb1; ">+HR$}ppZ 6n.TޞAj/VlM*X"N^[SቛMe02Ee0x{B2=nI_ϫyPeY@)z1Qn=ExCB[} T]~%" DOp&I4 t2"PKf9cI?-&dJyTWq)\j~_h-9PNOeàCZSVjL(,VBX pKUlvgH148#Ad` 2>D@iQL^RfLJ*Ш!K%׬7m-JPzG*)lxXx1ufZTȽfpCˠaal@I Na\0"m;z=@K񜃮8TtQ'2(?o&,{'M>V$lp|"I cG**ePaXT%MD'ȣUB'ǫ 3PաWfq ]{^UTBwk z(ޓ\&\u}A'CKAILU6?V[~|z,4& vܵ:8,cG"ѴI[cU/ t藼(Ƅ2 TKU cOoʯU?lt>xfD2g4EgcҤ j '>=qpCMhWktZX ĹWBM\Q_a'&$]+5C֡ FX|Z0-{s3WoG+] j!wJP~i̙2YK$^nIqɣM(v(◑bUIM=r ^pF,vLF$`8Oɽ3ۓщP]f-G)/ٜ*7mW~ CV<(Hx&D3{U|}6X Af}ɧawm()s)r(֨X"ԗ!%!H+'*6m[Czhⰼ 1`xTF+Ep8Hi'&t''(I5~5b6E3i=%kKMC{!Khv{$ d1W"" ,_1R̝ƫ~j$m~?/Bj& 9&_p;>Q?0_rLu٥|3~P6VB>G{e*yS ,&2m,!^Ԫ%~g{Q=D~[YMy3nQ &Û!CpPeTr4w]QkY{1*9:@j>ycp;htgίp^sIWع9 jV+P2w|r@wWO͸^%Q[t ҮtO!SoMu3)fO:n@J=uwhgwa- sNM]''{P=KE[n\za-Fvq[ȏn0jiWS1B _tAi%nX% * HN/c_kqSz M`7́=RM7Fwp,9 JN1-Imy`_ϕiЋH-52s$6a7SuaԾH51ǟzu lj=@_5SvђFSuq-%kL#N$A?9~WOəI83v6b6y}1:uHW,2Iٳ̬(-U'ݵAHfM3zO "ڋ҅%=C&+MyإO&1K'~\hm~I)7%@ ocT/&3AcM#1jǴzfb 'zG[Zex_Wg3"y86&aXsLّfVVEn+t5C[R*#bu x~DE==!R[*K=I^u{qUnZڀJOyx fQ0V&\ xi b:t;$`MQ$>xP(DaFB`?8, eXfK|WћFFeC7Ja n؜i=t@ʹ5mƓ-}N6x䌒] "imŎjW.oj 0Z*`|'B<6TQ^3jN@2廋ݣ%֎" (FW3V-f\~ ,`pɾWΙAJj+/cJW6#e>=ewS!v:Ԓ~*]RfgciLOgbWq@qV6coOLG¯aӌWxڈZ$a3&/o+xnhhۼ?#KC>c!I0eO4eonZŇ5JuӶ Hx{UKpo.t>9>K=n(8f]b[":ށ*q $ݮ2e4xt]Jml߫,t3XME-Ekw$2Uڶ=3Ak_9$5 =@= `hkU]4i@ Ga] ?2 VĮh16Wkgf>mEn2G'KHdbf+̼`ݫf"wݸgtB"Y?p(c@ ߔpk-ۭguuPyeMLyjupI£OɪV 0̿% & a'VLʕ2-o6{,wn&NBt#Tyt {Gӊ -u4ڲ^]&T0Hd!( $TպLاc`0Rjҧk6O!%~ +/.*"PGd긩F[m 5%P2C6 $DPȼɣ\1adRA}Ju.0[@Hڽ8< _ӺJ/0np3; ئEuhCs=4M?xG1R#@xBHr_tpEڪ28 9.+ ϛ|Φ@'9ǚHohZeo Js_s ;n[,;j1u"Vf clP죀^UEZN o2GqSeN!&}3eXV V= [FJǬYo6/dzvpab(sIV>LE0TBݡ'^y5,AJr]y'$Tފ1,fp[™\F^:1fc0S/'"îWed0Xz,y()Nd7΢!̾߷f,gᑠ=F0?իwGl:V;`oG0QE_3Wq(Qh10nNfMsNrSu2HZ2,ҒA%ʈih5p90ߵrD'6Ii'"'vr;a;0X&=r6cgJ]ŰD_onG.,'SY,F^*}eR >,ĚAL}WDM c2t0eѷ*v(x5I)UK=*<&9=q^|h֝:١k!Qͮ$Py;,&DPdYGqv ,Jc?WMD ĤYGM(*m$ehFZс`P4+n.G4P^"nBs(ԃqԣ,WI#KbPXn%dQ,T.H9 X?$^T*5=J"W-ZtgI-xžP:ngɅi\ɏI˥( ߵ+50SPm 6Jϙ q h UlyNi +^~=ʡßX-;a >eR#XbV!iVgMWm:tw2gbHTkǻQ5OQyRd FʕI$s"i[ZF{P9MHqOWI i_n΂ HjaZɍ'H7`BVoū3eoxIV| =Xn,Ed}o"7*{N:{jD~?݋f'F =—~;Ce^/cvxOM+C9 1oe*`țePJ9.(>bJ޳Ya ӜÒ!#ՙxhl$֣[ os;UG &oME.|䈽y'+F:w%LK?UF*E?!0fn6Bl_]>YHxt `)m[G*yshȄdf-7,DŽY!{K)ӘYTxLY)@Gv(a6TIfn$HD0.=vG24)lW>d"? ַ?-~$שּθ{kDQ''~ z@(џxu݋}(7HȑKUI}A1G2A p'VN^?Xi :;A!T(t{C }Ѓ+WnOF6 c8#<>Nh0$,IȎpf]c[G.ycpؐ+^Pi$9~GX`\m&)Hb9Eʏ@hr@O`/ggɎ @S@O`C.ExNJꐂ;O]PC٠@3^DeÎӊD3ݨ.u@O`gg*塼}@O`!.BHaQId*%zɪ3^PXt.u@O`ggSʡ@O`.Рi=f.u@O`gg}@O`%.R !G.PN"#$愋Tx!K.u@O`ggB A@>`ۇ=@R?-pV OGRW$ؘ܂ nєIrt{6K_S=oyXUij˿7$vS^L-,ㆅ\Ӣ\A|El_ޯ(c*6@{FDHk$6Eh{6%1Eira2SsglP1G?yvt݈\>f=phK)-n?͑xWñdm5e8n k2$Pmu1PK4?%33r'9>4[d@'Rb03wx3ܯ<(S/#VeJɔw]=.DVZC3w[8΄LPis@mk}UC qll$u@O`ggРAAȁ$@>`R?+@XҰ󓠹~Ik_@&xr]KD m7Q׀2H7yB&3#8Ed:B/BTT|WRPUZVX& -8<稴d452{ʅ<йx pLun졪̖X<3B 0Jd /?CvڄVMJ_J:D=~Oqhk6):-~xCaõ,3O\*!&TrUQT8OJ@O0 `ԠPҪ;+ג(54DI5m&P{a%`|~nh*DI⢥l@ {m-,d3,[-f?K~^I:B@CaLK0-y&)Gc 2/4-*}GWmʃ6>!Dxe zg&J l yO z#IP"bJ,%I^l3zSBlvXA~24d/TM5M #|OrXa:^ `Zr ΀PN-rŎimoҔ[z#|#sqHgK~z[kfR^@=M,ݯ@dRG=|U}\[d(.N" bwUk%1C9}త:^,@_g>CHH9Hr@dU,\B^s˙xӂL{Oq*sev/s.?6p@q4;,%1̓,Cq`ޡ`E~n' !=%=KIMvU"₽ԥ|i:3p5tCTcS4&4} TʈxfM+;)GGOl|°sL:! sVEQhU@NSMC:Ow67^J\j N@㻡#wZ&؛gyT$s"quZXug]Pnyr5߈ڇɉóxzozmQK4F~?T3;'}Hbj3ʹV":Ê݀d鵜ӋB֥m3_~a3b)1P+pIFb2OOR9s.ES MJ6Ŝ[=m_euAA A@1>` vVPK` {aW) zMQ]g-G NZEHM;+:H!X<XOLۉL⠗  ]dᝄہ^"j+&LV{.bRHeѣi6 `ƌ׋qF7$!:f7B=ᣁ]1.\iE9&:`RzPkn Bnh[\ / 0P%NM&!#FҐMV.+:-x)f˨WP^UK;hP퐎PyE@lt|_N sivN@Kp+#bJ ˣFpp`5[l*U ڭ -EGh(0HĈ]wd70VH\C4>C޿HqyXP V~,ro|)L(R3ZStmy)`v?Ӈ~W9!UmV&Z _󀔳.mH鉇$3ÕQȈz~ϩ.<+AJm(V-Q꒐ͶZ2{8CK=[~tXb\QV O͉;ͣ@ i2sB%{ <~XJ9AR#n`IGyrJx8F[OjM7yN-޲^VVwRF@օ<6I`Veq=|`]( wFkD@>$dhˇ4v{QXzu\HWI:ĂEH#s CZɅ-:2u+Vo Tز)/֞Ü:/;lw##{un(B;@rFʼςث"1 .L_+W/i4 z0<~v]ŗsv9 Pקm FY}B4T>4=/grlDoZ^7٩zKh]:rStpFqhwdΣ}W=ImܥIYK&7g]IPS?szD߈3pJldKsBZ4l8Ittcm3r/q/A+X yQ ᮁyx^|m]WQee&':VpTǫ ʑdVL3Nm't+B-%Ȏ /GEfk? d\ gPc+m5?i^bs\NSr\t B* ǑDGh՚`l vq(YLNR6Jjʜ7Ԛ,cMd \Mju-֖#1dLpP~RFPϏԡHgZ$3^%P+M0$ 7,VaZFv>B,xF M#p܅k+thm$mi@E基K2']M?*G@©l/ae`&IvD3X8JR([P~~ Q4*0s-?wZREEp@i^6e`@\ƾM x""W8 g;x CZn/\kIpgKnc$VJ9&KSV7d9[ԱRl]qPͱ-HغhT3*BMƢ:oiҹ5#wBb!,:olu smBlPzw &*vrZhuAAA؆@>`[ kY_W'ʜ'ʃ|<G;Y W̺(0kQחz cvԦIHŋ]5/#iYn>n*yLa9Z$idd.01p&NO/s|_P]Lי`75RĄB) ,d=tSx"lб_]YetFaHW8RI˗v,me} wz& 6,[/4W_;]Yd 1 NjHӧm/1D.*?C>K7\X#X]*lK A̝ U;o#m"rL%ɫebkwLPct<P : 07ma] $|m6C`zy_Y|H&BSeEn1xDv~H> ` SAkl_r(`S[ Nw@p]8Yu> `P dDE&!df[-x.ʠzk@zBMz} ~/}/ߓ?'_~??#{/_;G0.3gMܗT~GHQV]Saǵz)bߨ}<o>\ZXY,/%b$2Vv5L9nYh 7:'"7öۢ.`N(-e@H`eC㟭q;.1(*8v4e_\hp' e;WؘeF:{LQjz]B_`SWjkZ n `{!$, vKra+;zF<0T{'_-?`Qi /Y bw3s-KJ<7Z߅t|#zθcem*ob\$aXxk<%:*,,z%zoIo <ئs~ޅ=PQsǍ}ncٻjHĞWl ʣ/Yd,)~5kx{*i[X=K{|*?W v'LѕK#kB=.#ByV2V-W߮X]2CsP \'32<֮`˩?þWHjBlΜO5¥.MOdS$#KEכFB[术!g{dcf](4ϯ !fZJ pcI8=Fl~|'2-`qlLv 9\eolZw?*̩  Zġb!u9<$&&y1# ڗshF'p81ݻw~,-O@@M MOHL@.0Н !՟Yx&uO]ܜ+nJfZ Ug#5C^Yx2痽pL%g#ZK$˓ On ȄIim;$M@s:^^)­+#%0GQ2/؏)x ,襆[n[}Kk@/N,dK-):y?5HTck}%cbc"+Z+*Tk`r6L/2O5ADt E1ӌrETx.dxsN>VT]̃33O)>"䏉jg~V7:k+YC*B;p1yڶCe4d -uXP7Qr!4ɻ2ٳ|'z$ev8͒Ǿyإ* cIT}B$[ lX msdN8GT_7Nz23 U Lg7 zZduk^ ج-S;f+ .'=t;>>MW=Ѭh"wlE"*&"_SVaaӎATgcyDo/}ْe!ӉK:u`Y|?xwI .j(PCG8q1dۖwN;{P9)f}\ܛ‰i|#D#葆 ^] d ?a]د8V-X;o9ȰĪAҾ>)(A$Of@. R) V#Mlw#jQ䂧ׄ~||NH(\,B|Bu^f23 Ëܪ.Q&%3 pNwi6^Oө~ܡšðs;pfSvb92#`D*9cռzגJ즟Nc+B@5eB]s~ U!{;ty5Xxʽ_]W=$}LY*Qqi݊+nZuDڍt#Uv!>G{T~´fg{$@* )+fwt ou Fj|d8γg"!>bө~*q,Bkw>ǖ:3YuU^ؗeGSU\pz<.+%"{d6?]qTK~|*?0fs:Y6v 4IsN5"2#kI! xV۽}*lN D<pW۽'>m[_s"qW+H0-X:B/Zs. t4U6-WQ? td+at|1@ЅT%M ]v8J_T$IPz,C(zYN>K&͐AAL)!ou p}|Ft^B)`Qhcb~93q[JZ]YirR79p,cf(%JyZ=D :f!1r[g2 Ը2B;TLQzA\=C%wⶍĜeAFD5SلnWt}PZ e}C"2~/= /\_jur%-UPqtJjXH?Q`1[HA?!E8ZؓsqMX5JⴷV"(Y5*(ڊD(r[y w}@ꊲXں% ~9 !|/z^|K"WKmތ7礆s)! *#%"zJh=8> ";u"s/䬇>:FFqy]` U01ٕKu K^cv }#'nBgHea}2 *P6WjW1ccXN&Sbs'KԜEKjҜvMX*F-&grي,:)*,N_҆{V^hJy&|I-?Trhmj]-&-Ṵ5:[5) xO9W+ `9^> -r H9~`_ky6gX c~4ͦx^OR]'ɭ'R?OLLW°6ǻqNX(BșQQφ\3$6[ސhwI#[Ao6[c_;AG^i^횹 i /8w;&潼̻] mHaTtkeb'FlS(;).Tsxks{wYQpu|Do<%& TTQؚ +? (MPu.T]Ƶ5߾ "Nl2?tyJjVn9CD8r%%AkHBV 0N=Wm:|$϶}/٪CP4,bRa ssæݷMz#Qk,E³k0҈/Dڮ\>zplpC:A)e^1wlǂH5*ԝ@ڼ8}l>ym%T*||/[fvc=Bbr2Lʵo>}]cxztkԁVs2d-ǕB%w!KO#ηD7٥>XG(N䰡B(ߍĿ-aϾ'? w{V52W_?X(v̤SCN VwzG/Jh\0쬜r !pW0 hj^E˶!kIbG#yG``JqCh]棟QGnOKM3ۇ;π7munavl05_tq k$@;JEuڞ]ۼGOQȽmNx|&;MlgUD,TgB+cq cW|iM#7>dig8|DkUoHkۦ-9{^4G;OƕF Sҥ?‡]SIS"N؇o<jF~Q&竅@ SJ`iT `"*Jg,R8?!}h~a I-,;^%fDű Tgvz?t"wu5hH)xƥ]aM23=)PSzTbVܚN^J}*RR`Xg.@a^ Cmz8\EYgi0Q|BU/)AXZ@E:bD_A9U/ʢ}A (Q(s{P(;rx[Ԃ+SIhLا_ǂ%/f֊:Xj̜AIc#LqR;-F cFbY_aw6N“F+"c _$e.TX ߍV}N]q@gs3cUFH¶ 5J8#?-czKv4%R4fՠq_eoK۲h>teJc~V  y7^o–*P&ˠWYymJn3-d]t6돹v0i?5zj.2c1$6Ι`g Tɪ5!QK0;5Ѩ4릨C7a ]M4vK8HiQ-8 Z ]<'R(aCB-j{*;pE("je?'nzHty*uZ&#L`gu,T.l؋:L g~d6e2"+{0wwri01c:.$lGxnnj,iU,ʖ #KM(rP)='cV]],7(D}sOU]a[_y*#ĪB `&!A_ H83 |n; ff6Zc. M(cFt.j?p?:'C*܂ܙ#%&_cO)z|AO|Q>R%i8éGA'D-X:Bi/5DS b#?  "B1ϟv wς>A*d9g5wI 2PN0B-T5e?M;MC>`hǖˠ}[>{&#jE8Y[~[tS ,Q*}_#dyrp^ZybaC+HAdMːn^fVT̓\kG^A 17u Z(aule_gHT) `w_i0eiIAÎh 'ȍ"LR{G!ۮxf^%(9mraU/Q_olȟC`~m:v4uGPe$^Z{'ɏKUrKp7(H]-nKgu],.bRšA H18CM2Æ5Y~ȌmasvRJe"9"ӕS\-,OB_AkVlaXX}8U<)gJbe + !JtvDžթv^MZxeXoJI7 TR$A ϑn" c 픾֗:_.;-a=@Je}H)}Є(9:~{L9ʐ`j;j :o(|\%&`sX~&/{K߰'ݫѩJ`m&3i;o?yE>h-qck \&fpo _hݤVn#3ڜfvT[6FJȰxY@d8d'0LI$=õ)@4:d:'_jSswFto*UZFg)loħ@s؟!PFsCڰ~ˑ;mGLlO'P]N{_>]w;l|9LSJlc\4M/VqQ~i~3^5٭ZPB^, >U;a}F"oяd~!Z+Sոe@Eq0A4cֺH k$VЫ8oiHcC(TbKnu/N |H/fE]+QP{owaݞm@ڭ?䆭;@}09/ &)%5 Ϋ@N5LHN@\G\%l,SRx(;5ŨM7e{Nn;{ O~fp3JA0{<@s޹Jo &B `h/NTM=[-x4sgQQ 7Vs8Ce[PReZY~=ߴ j'uբpޡXeY rAB;봯zZ\ 8H\Jnb )8t-LZ8M|8>Ah;U3ev󳥕ӏ ע]?(͖SR[6W@EjtSt ţk:t2I_ PX{59A?<4E ]g*GoA>Dd _HxI'cZ2q}[K?ךeQ @ %`bt ">h 4$XK 귮 2BYlz<%0`𫽄`P V*D#ׂ95ep0 -ºl06^&# _NLyy!qI@xbCtgI| rmHҦѺNxzjW 06}0&:OMCCd-l7|@ڈ( ++?|AƗ0Dw{j{}0q?^E+պ\z'Fغ L;?H VVpDYDZQZJ'9L8=H82 i]X%cA8r{9H@N%>i>`(koilt^ tW G_I'-Ťd+|^KkdG~.dMpMeJ6)d ^~6/!`*bLa.JGΞ42Ǯ{p?cl^FB0+w#6Vw,8\B8 @qaBuM7Q9Ə+ ;ؤjKC vc LS1}3<]a+rs:ᎍ'Tv!NYhhE ϵRf >ǫ-Q4yV=M?`ʪÝ3$ጸ':C=q"9B<2 %=R#[dG 8vȦ7hmʯ[)ƙ >trx>LYk[T<:i7ȹ!5~^[*GT,jFpɜ%uODOAO;@z:>6'Ĝec/< $a?W־ |n ڀY@:^h0YY;6fjF*WK`Z-K.gemoiafGJO' }4m@$̘_u琳ó{-:\Zp-2\ht8d.=lº9;kGfD[(qDcL$?BHcwrG㤿?S?d\Eec[@KM@u7\q*4]n{X=h΋Y٬\VGUbc:r<#Q8yu.Ya)H󼈿nI[:5>zp(W|RhB&kQ&=O̕ZGRΛAm:vEJ!.Vdm2 Fm#ކ&Ok>+sh؎ 7mC83jEZI˯ i]I0)TWdM$Q) [5BՉDG Th^;-;,ƚymTOﳹ؟a-+P74DY_`JٿULSJ)kԹR!S﷔3Vp"aDј: Ʈ ~ #HZs,OYa8X *gt`,CUńk)z3ZzZ+WdwfEHrdy߫d|/7D&Rf]3y|&:/(B7B+rae<ς-trT%1g}+N}ZgtҤw5 3tMYClV 8ק4R}P] j' eR8^̍:^[M 8+v?yHa.mh#PJwR ऌ0e4.Ӳ+ &GZ&c,'K( b ׿Rv0 uP.of2r@Hub d0ʴ=y^s~T~V黆t6&ju ns<,+KnUЭ2Ѩ -p`03xaM(!H}Ί⿧nΤ,.$5 25?x!?OdYpW4jդ½ RDڍy?P\ v\tM |E%)}m0Ϭo*;M >nP@.ٖ9oHԓs73Âyv[ pQ0φUo=TdytU my:^dTDfr>a\|㝀7VɎV%/t 0>?hX#7F? pןExR_kw/ ^V`Ų9w)+n>Y=mñ>xg֍SS!#I\vk# 侓=ŪY2#&;^.QR<_ ^fS+1KSՎYrI>HwztEx${"7df@ _=8 3/uL_I15m.z=BMBMi'}V%"}wлz}YzƆteCu.'anjT{N;Yz(CAQ? WsbuC  2֤Wh?q > ksPs`A1> ` hm6c(QB%T@ l}+3Sy+^lEO""m`R0ϞIAα.̑1t\CP>) X/B)vڳ9gqBl9Rb(CCHOX'ydg½w0T^K{be"h.Z|ix ?PfR05SKk$EqL8kݿ:G~$tE b$lzדġ*>J^VJOIЇ{&Rt_\ݍluG>0;e:[]+%K^ޣ\]CGo U(Zކӭ]u l$rGjoЋEԙ #)G+(Ggu><UHf/ԩ'Ht/嫍`p_%^/pAYVD5ƥCVWk8K GRdpR冊BMxbinB:dDptiWNg[VO&^wɧ6{jxk^wq#yw0,{v6?S6푯տPaVO19z[bV>!{s()>9նSr{4Vl6d+# p Akxh<7v:խm 1x; 5Si\gEOEU-. noBM\O#XjHy`.t5etw3MNc ףe{28& Yj}‘`4r !Qpâ 9ˇBSW@M8Ξ:w=$, .R:'jqdi(k.[q92g 7iں,y5{4쯦:0yq᮫. x|G azgX9.mk ':޿v:Y%xf'g qI4<0`aP@7z@PMy9`mΣE{|.ByQ)ǐ~&0Z b 9KS5ӽS었ՠ- Y,$](уe)Ve B30Cf#y )0 -?6QK9VxֽglOX[ioO(3]E HZ'KT1ɝ5HG"-]HK0C3[Cвlm /Q6/ ?uTdOҿV*{AruAvAsAmA>dWP<c W P j#<>㲝a0öQ)n r]F{a Qs"Rls%"t b xyk*kE7XP,:1VE8[8#`Շaq6 O'xgcGzrU?OHʢlMO-jWhW%yOK,O~7N*]+N}\߰w8;6&F6d@إԌ gs@BlMG8WT*xt̾2Q2p2jp3R z>| ^[Uhe1"Y7IwHKHCqA>,e;B?{2.o~=Qʬ[ ҽ*o:J"v4ሞնfU$q&zƹZy U ء]7r$ֵw{}`cNڤ}f \{CWbB@V5&Ѯr˹mEx+6B0}|Qicv4j$W$c =y&]3,~;Cp$Lm*dlNj ]&6oe/<;@@(ZťByf1]{2S=8Nu(cc,=6`Q12biؚIT>#W=ĕ7!p J3ȺcEbm-EuW1 HMsXp|78](H8zdjmS%hh2˲iP%qu'%+TQy?WO1$2/%:H@MGRJwd~ʏxMhO:K@fnOk2`#m DLuc4xL+ޣE:l/FN?QX3'|Qj> U+mqrPIj޺ žTA't166'xFF5R|^JFb-Tl_ :YM q,hJYP }_-!#HK\oQ;PMI Td[w(^GU)TG؇.Eרwe2ݒ4x.Yn\ V` *ޕ>}k&]ҍV!kIڅ;mNZ9 ]]C2FMf-s =f8n@ t!KDhg#!V4ۏ&JA7DM"x pT_/i+YXDT!+{_lʽBHsqBsY+ggs҈oV[u-%!XhQr/ߴ&\yf"g+|LԲ)Jlz^rwߑAO A1DA3/#z /MJ79042Mq̫ LI<9BxAG@5"aRM.aQ)j_[Ux_DPzcVp*Hf4ta51#U`?G陭n%FhSA+n$Uf2T'm?F`=$rMH-B!;=>uU=%4A= Ț R8QAzr3)}푿?bނxi\n{}hPx^ `u?zCZ6 RH5 Ö99|?ɚ2R^D 6s#x ?CiÝa+uQ\5f]ytqLu{gM'! 7MU]TS٭ F#_eBKPGPKx}}SXWgHu^ҏQTwŊJj~*@w&'I7$Yo)ʣvn;p \drp tk硄 |'CR"l34 Eb4ߐ MzwvjM4'.z so_^De[_P2~F#uj q5Q*Cj InFe1/Z/é`ToΟpRוoyki$t )vG hKE15g!8#M1s{1|*t5s"1~[H=9~صkK[N=cJ]Po8z2ysֽ| #0Db/R;b:P ZX `PuBBBA> gs `[%@Q2+0y𘓟_I5X9Ŵ[ %.@9Oq!f)LqEvU2'* d'i!cRQOn}uUU\4,(8|MgU͕D?zDCyһ]OblPݑ]xZhյu:=D.PڇU/@<^r\gQX܎uOIXD783X.TΛA `~v?:s>=/dH lu$~wP[ f~r8 ;"+d#-G^uI/] 0_4,]AH5v Gx|T*\8]xxHgf92zC%G, 36E=&U4$(1C:>+Vh%acmw8d3A]K86D `]y|yi xܑ`Wbz;ROL&تfx%B~'/9kC(8!6xTT?O[$}46Xd<Wϋ Af/ w9)5I:~U:zbX0rRuBJEGJZGA>6o#*} >4 CNp8aoh|vWhU: m/&>j%D[ 60K?zlOp`>=B(5h޷ps/3n*b lˊQטHfӼqX͞7k$Ā_u Tiw:qDE‹&*@Fy݈P_'ũ/uߺĖ \Oʫs<h.eOOV_,~6(Wmp&_g{.CsgD¥y9@!coKfB+tcaz?Om^f@ u)i*?.М"g6ϲ-m7X g\ɸ\(@D65kذܘ³Nv7(YvGsL͹M[+ADYhH- \~^Gů0.__cY]LZ)3 wt>VD7652e|6+?5ͬp޻fiU2X xv". C6BߍV+fwOh=Oqw@=lc7- qECU˓ ;"Jꃒ:_O&ŧ$9h/AOocJX+ۑA2&q!pZ%/?(t@);U`V30FWGPO Ç7bvm4VСZ=(g(&+(8cwG:b53x@BX,6_ GhQ,-ۆqM1%.t9\(sVV?=٬mPx@=,wkUm.?K3K$aӡRsjz!oK P"j)Nek[^#W\er%|-b2#axi\O7s.LoJeD!xO  hJwo8DŽg.Nam^Jǔl4&9 7|u,\s^TS qh_(8jbDr<3/Wĸ AuۃZL#+t #G+Bȁmϝ9IH9;ă>NLD)OZLô7I[z0 0B8KzRZ=QDA^Y6bh/H*&8ޝu Ϋ7GټuBƦBBA>`ς>JRdMI7 .^eEֳMyތF,D⸞)ԧ|Gqy?pܖ;+R|Ƃ SULsBZ'Fp8M+BQ%,/:W)>Ge=5neXw|4#d9$"IseE YmַO{vRȈk\N t@5'>uMla {)?,c:>y.3>Xx`)")8\a{bԅY6(ّ;Em ^TXSl 1ǭAZ$cgKjP䶢md I?aεR` A\[]NA7޸O22 ~|k$Շ$\IU;7 ٤S 'A]RtT:{"|â7JА2 (-j.⩨r2JDd7$bPVLJkeO%S :ݣZȼQ#KzY_8e 9\DuUέJ2a$Y?"ijpӋ!oR>/`G{0g+2Gp+1 }"z$c7BJbj:5VӧН}f}iied)ci(aUqIաF~A>g Uo`Eao[ѿW.w@* C7 D7eUŜEff|n#s͐f0h[o=Q(&{Ru"DAx"iÙ Prφصf]bG9 Mc;`rDYjCͣJwi["jĀ kcs/\+ꀏLOyruz%n Ms_3&!߅dxRw^ Sݲؘo7OJ5ᦟ͚^Aљ:rz]菾9:j{3Rx41}ۿ'H쪀-vq8#7' ;D!Eݢq\<ߙwXߥ8,F?ad&=A!q63 9\{k43E5%kT .J~jdj7Ri5+QfIb⫞˂ui=|* e48y#gZp es?uMU*tc/?+`1cG8xOT)v# JwO=k䭺$eבC!^^ ްW^A8!O%0vplZ.3bn1م- ؜![P --ը ;6 yQ5kdVe"uptPȢۏm`$0>xaq4/;7O=#_n_kוFhl ūbW 54:buM=EYq$^2>qߔLo[ba*GGc/S_̂EIQԭ %[VP[vds!)򊨹 sgb"K@Ya0t߶LGj՜Y(LMvά_ щ')W>YKOmON((L`xlnȇtb8aXM('3YeB9'>f$|kdg+`yZٗy3@IRf'Խ/o;zf_ܖ4x#RNMr5p88+ВݙȎW$VgyO-g 𸄀_B{=w<|],A.0 hX 3Dh$GErR95ڵ)S[]fyz4\tfB$Vt(V  fF(51>lawmpjP{%\$xB+ ]AuSiK5〣98O[JB~pX$32+QJ8)bqm\Q%ƭvuoWq|֑RIDcw-m?0ǻx!3\uيZ0.02ƉWT=%bn`>Yo{:LnA<ܱң;4㉿?M–pdewsjg|pP>䡂/ (8tࣿ%=kr{>Kټ;y2Uh`y|o EˉԱbYxh, `ЇdxQQaSy^ Ŝ|gzY\LHCA>0lTW ᥕ7L@QDtPabLsT(6]ÆR$8@W:#c8|AT^.Im?3QOBi*g)&e$ +'la0q"iJY}f\l$[vr# ט F5l+j`b b-Lk'@c91ݪQ%]sMW_ ^f&ðs`ElM E.`JO \f/譴TB,~; **C,PkdkKx\ X_JLPڲKݜ֌Z?]kO Dc:j}2TF,ҳjm_~rCJXҌszf.7߁ߎ؁du{R5Qf[%tE/8ܬz O"FPQ . ÷g=hR ^7ϧnŀBL_o) mkz5lHY ƵOnsKݿE^w$uEKlؕrVUhoP~*RQWϞ]w'.ywREo0^}ᑝ ҡ=ɻ>qyxwBC5gaK.SxKJ$:(kH U)yLA|YO} +X "@0gH'b$L:+tYXK[gvnf'D$~fBAP|\agL;WEΕ~*w*s&"/.2ުq$y n4Wj8Nǡ5[oC.H`!nJ^E2d%^϶1z})к#qtRi?;sq eӢPJ6rVs̤ |a`Vp+։v9"ٛ/2*JBҜ~{N @oJU aπ1Dn y]Q(1*oE jQW. /m3Ώ?7&+ N}z2#C.*Q:@F`RXXFp^*g|v>ً^baP&,ޝ PcdY`<{ fY?^[$;z8Ʌf_s/4' TCjIgɧD9^q.F_Xb]5f ;ٞ֓)S0Wܹ[$,g0>#Կ4=6zfq-7nsƌ7bo/pǿp"iZz2h%DM/zcdӽѝcF%Skp<T0a O骵RR)et7t:v "$ZFa@<"z~d]53䢄&kIܖArZpQ봹2a5ȑMuDHDED?A!>`a?qRBkD% bp](r1+H(`b@AfEJ`xJZ}/'8 d{jT`J7@6^~Z tJ4g6w=X~:^ .C0{4*wm TL,ExxIm+7r\p؎2gc] D9L>MKf z>~zZ%'gLu (o j[6Nnyhq5&MFr 1+Li)ᡎMlH\!o,gh;$1M= Eex>N=U(V۩e%lnT0Ԍk#xZkЋ!H |PO]Gsxsv 8Ѷ*ȣpլ;(;̥AY{*e`Yxhαvۉ[Lˊ䄘:BoVL8Oit%CO Q;uc]i俅p$m]xIUfp6+d%Ml$FahCAu^_!nr4$@0A"ŸyJL~MzzF!@PV`-yD.>=Zj!p3(e[G3iQئ*،ɴA*2ٕbo Mc$Р׾!@ʿ4D,gAWj,CD1S**lx%]8);_J$_/fD Rlw1ü6- BhlL3<:[FNf&T40fD}K ]%}Bn0/#_qA^ǧIĠNHAQ>VkLE6=*ɹG&1fzZ{䯊cAʉ|B3]pO& S (-}ͣ=ˬu^ս+av~ ևC PPا* bi2-A!̱` qKGbɑs@2e>F[RLs#؟c)k!ۼ/0?B:NBum)ѐIO&ܨ_dHqNdlvc eXmgI\ pG^r*lMh-X^coCQs *[]L Mu연ov"& q0{sX88QY{FA8дjBr5{m7\nfnnNey( Nkh )>(=-b46R7PvC(} )inzatEzU+{@MS]iYs3q5-(2]y\0Mv'{Scg1'1 ChQx_Q -YFQ?B/CѢ8JxEo˺Lߧ^6RE|K#5ʡ[}i{9$ h,¡XyǸ@a{gn@yRIB48V14nu&Y@XFjm).ʋqமUsb軥\ߒZ$EmMvs'^OxJfACK5Ѩxye}|fw׽9ҩ";qС^B,&:g}*J=Zِȷ}FF?5N?nM ] 8l(e"8n_+ zn"dw-s3 h$^yƹc 3h%SQ*1[k4t&~:2ΧM0`CAgH*CI,ᗥ 9(S"U D:.'kI1HЉJ]Y/u*|3Ӹijo/:Kc"?}KN[':!^W}kH3W޾vl]N%Iuژ,|IR +"c"dr@ɹg3A i7vK]C^zP]DHו§vU%uk)ۓfe{ƒsE(!V epb&pq);5ʅodhmeQF(Һ9~sbdSv %kC}$su[ TO#YUO%; ۴Ѿs㽕g{F1%$Q f GEXNa?;cRi~SnxkjB Pb#dg ^EKڊgm/륪+@qCz"B>Wg4dgh&+yy'W C7""ͫr WƌuTۻjXԺeJܴUYskwOIKI![\U~ޢAZxjTVhX>)M6#dB~5*Z!TyX{g3xB sza"t1 /p֯(tO`W`YD#`|2}S V m +̼J( fVoؐ'ҳUV*=Os~׫,t=W$[ڣDёF+LX n㲇yEE`0+m4XY拓E?c_^`=7B칶JUxF=˔:M1,Yg<7_f.qE#+4.B􈔷5saiGPlOk dmB!G @Ji=EeE/#VkWZ)u n+Ԏ׋ii323E>|FI>uEEE AQ>8o /w_HT,P&-`l7ph`% Q)05ѵ@Z!lB<`ÕvEVWvFm ̨BQy֢l qړ{ϳ< z݀K&~B^r$U" TZ麷ZĽ  $`zGPZmƸA1Wms撅}6zzwl9tJcqάg[qv^ Э'C)Ӣ3 871DtDo?< ŝaJ06wЅ~\ TA!~4$o!M[;Y`šH$Oq+9j/h %Eźg(@~l-42G =E)C7MX0"p~i6c~`Y4Y`9X<HL 6:y\3EP&yӈKÍv7`&N+/߆P 4HWq֦wt"»AjwH5= ZOV/k(e}0dg]J$/ή{X $9 I4s[gqDHr8%*W4ҏk]ΕX{#H'\tQS=_HjkWPGLBo,MWSA*=M$-ٵ}ES~=R{I)B]K)pktGGIG:[:rHJB7l,$c J:^m4'Wp~'_#*7F I)tfYoQd "I(:!F4!·퇌f3ɝA!9OޡIAA>"dGmwqS]W)^7ӠLJ޺@SrK D|glP-moUxhҥT/. "W"^+B48ֹ[&.xd`] zV4CG Hxc$gZp72 ƵgDC^ tϐ+U':yE7V߈B5ų |EeTJ r̃0jDTď߸8 ߥ^еTVl ˯  hk^dzttr7G F8BR*DT >%s.ƝBȸ:Ι[ &7 B0*+R+^!Л `[iUSV`dd.3d|8nN0 })|3} 3xn.*tѐ~ ST ,}67<'hNB;58L'S~YØ?a:Q #^b,9"=KÐЬ1;Y`y^_&RJoxe+ vp)R5.(zlMAC%פ5c*LS!`:`W*i+)T23"ILhW U{N0~Y1Mٝ㟝1ɋna\슄gG^8-XXdɴIFNǤYJW;8p*D&S1B) Q&^V-dL?w W bPRLɌ}f#W%;K-rs&<޳Sѿq}]YW4>b5Om- ZH<,GăІHyV&KF.<|e 6 #8'מgCH'!&MG|A5p Mгx(n@dg[يv3i8;j>(n k޶gEu?kO|f=Zb. pcq<;[\݁!NnPңSU~=Lَj ȑͩͯWȬ6I)ш1u#%cœ;5z]C#[3S_dSD>\)CjWWG$9y.dy:2|q60t79ey\@?M_XvTDar ܿhsLuF<; q鱨PR/H5P$l`AwQKl~Mh)*]*yd7c1@;;YkX,^q8LJ]0̔ygW B?z {P|~OWɵnL sYIs֯'o־i7u !_k{uԁqZ(zz|cfV^ HCJ͆m; 8nI5K٪TgiT%lw(Qm/VN-+Qږ+{is }/Ӟ^F o椈Z_HV8w% rz=ڜhپwos~aRPRt@k:$ g:UvsѸm:OI܎G;M.p\s T#H5&:^¯ܻ~s ig$Up2EV,xUc|,`罃nYZ~+HQrWm a#n\g~eYB1U 纙_/<-0A2{ۉ*5_;]q8lcA8 ._0v-ؙGկqТrEWw:8&-`B 9B[.QzZ} \R3\LYqHB-h; ŇVƼ&+b^qr}{.ΨrWOEBƊSj~fHvB!{jh\>*_ݽ5^±weLm!0r"#/(mE&R&Z^NBi&bXbDx!1<]XZ Rӱ!C$d"?%Zs#P㓀Un#"3\?L?,?G+:lUU/?xA:NcEwp?>ק<41lr=C%E)|hwtc Nr~rQ+J5꩷jGǷ uF!FFA>`2|N۟9qO@T$Ԙ*I7 8uؠt>iQ-x#V<5/ck];҄,fI)$A5uӜA'`LZ3DeEϸt0vN1KqA?FG7N=z賃#աj!MtК{Nۊ&ʴ@ڽ>_ftlr$e=e"su}uҟw6ێIz5k29KuJ8iK"+MSzGRN-#._OBZǧBTSVa{S/0@GClPnj=;yR/1s'XOYol d]qZA#sfV9+MyT|O۬ h(7luh6џ)$8gr߼CXP'+4e\Bh}1.c{yؕ8ߴ#Y:t쬿Vd[P>{R8F:BŞb\c k'y6B<[_Mo xa6Wqg߬&${ ]hx{(qaOW2Rɋ0ӹǛ\7S2/@/=>В7 ie\ٕ{`. J!-p$Ocq+ ֍thx. vYMLN*F0`Vo>ǧTjE;4%#'p|[uZYJTeBs>!epS@q=a}3ʌkJq3{e_gqʐ/V3ĝ#s&I0PJkA>*i (ӧ|:.JeɟYS'%Sh!j\՗YOK/ ? P7݇,>n% N| <"0AԃZd#W=8$sC2:*x Juee%p)ϳ/5)Lp3*#އ1׀R7gAn{ VT@+Ш>z(݄jxfXc ͉yrak.,wxۛ˖O<>f:{d#Km6]V^=Q_ o>&<7Hb~k6$XS:TٝQ'@6?y`?"Cs1T F|W#y+CF(y]o[AK7{dSBQW˨W9_)3 o& f˺T6"CpTZTEۚ  $} \Yo.i '&UI `e4xQq[X[%8?v ~¦Q?Zʁh!٠'3GxB9x~eԐՍfC?M:Ҹ{V`KG9s"lkc[J-Ү\ci ?Vs l}95 p JL ^b싊p0^'edzt @iw藠faMOC|dC)vbf͵YƬ3܋rlqv1d#;뙞܍8I4ѫˀsTanXvG^cmW(qx]a'`;m6Umb Zh*3|]DaxKAa,oGSk4x񦔮s>9~m9Wp,RDJBol^K+ s|7JRnћH,{ݖpsWC't p4HkϬ&ud` 7!SzFI\ʤd?X 1D|Hxd4>K+}2n^ۡT/K c#Tz#.ǞV(!ub8 iS1Ƀn%)Ǫ:7Ȗ4* M7H*KB;fr6!/ؐzyTnyzC%p^i it6GBYY^"L4QqF$'+ێRlWE_ SS.ZߔA4]$*}vJ5K=f*O!#I?(I ׾]}P˳x>i=O{.|{AxɋU`[E΂ R" bs3Xhh>.1v2H{UN,:sYb`hrnt+%J$۾~;{]Ȉwy\YQV$!F iFUa 1;Hh8jA^72ѽ'(3ݸN]"!֚h<"G #'26KN0uߨmYG`uT^`4fo3@b(jH{ #8.RCw?ou6փI[]N 8=uEEEA>@jh<ޯv)%bPw1'@ чy#љ,=6b['_ɤj_lv4kB3|f0#4upD7vBS\9Ȭ#P|ǹ,4̺-94bI.znX;A96}$i^*Gl`de(p"y" =WPwp.+ <0ŮXMjՌkPXaN{@uOnͅYO8,//;H̴yTiJ(I57]L,7ηylr3hY^k^]Ir}dff} H@lK2}iʵ4Sj=&ƒ7D\EO+Bg5!qll0o׹ =a^/H\=KVpة]#aN8D|nRИ{0+N4;y"KԕF*-jAhF:FaOkOVmE>3P(ψrJ1"N:Hh "ŐՔ cN{^uNa qnmTȻaeM씾7IA;81Ȥ@,]w@5&u(nD : G!n,#8Պև|.Au3XWwuHsT@fQ C5ZF؎`J%i=SNOy'XKzU?I:?r]XBP+"shJ^ ?Nij76RW>c^ m–!s7rolKvTIĝOЗn+嶖~Kd*G;p:{^щS mVӒ'}wC\Q珖Rnoѱkacwyݫ } WL}X0Zj g*Kۢl[4u df“uTh:=%כ s}.x.]q1s$am,YK5Rj~aÐ}"Qx8 ʝ55gsDNEMA@ 3vIpAIIAq>8h2ms`u%c>LTYn1eW.SR /TN,ߌJpP =ujz]^lJpF A&͝fJ)Bk ۔,sghxh2rfxw9Ogo5rَ/z:B67] O>-Ȁȴ憴yj߄ь5`^Z2գy#oGN< f'%fg̖@pVf\aBr6) Qع$5}:J 5eʸ.ajLAIʍ~:Q{K$6Sw)=V3_kW(Cx^gttz y;1'z**˥sYg'dd#e+l<:^J]\#̙KyJ0e~܎(j c+zaϦRY`'0Dz!$<'BQf3ѤmCӀEK7u#oLXDr>WrVHSyeE$ي?I9߬=?bd-s:E. Čg]B!,.d>ȨOZ 4ؖEq԰H"89Gf%@_|ע=wlr6)*sd2- *߇-NY: yБI0旗'e#FGhFoQc S15ʂM BzaL|fpC}5Kͫmn#uA\I5 Bx'< ;F5Cy\[:ЧeӔ oP/E~pqOr^.@1qS-_L5_k ǜ5c}zK|MjeX 0AͨaxzWsGS,_tVS!R?%N_Ec,XST.)#0xE4v:7ԀԎq|$ rH`V((CP haXvgpJ*ScƑ{#>kkqJP׋~NS~~Ai\9^&DrE3xP7OXD#gBq+p8CMOWm 8Vz|m?Y32/@gmtuQe ^`sb@{jzS!\2zn\ƈKuRvջX2g {҅xX(1{ 偪|Aw;-M+gz2zK:63^%_9|$a:q :_z~yʦ֞Xr LUDIbgJUYm6jYQ3)V]ʿU6 MsY1:K3px&r esa>P^C{\e0%WyZmANnA?KP ɬz.>6}WۋГ[W,s`AE4<&vpEaT 60J(hĞգc嫴xG9b&Mĺ_|U&{:W0JB%lަ[@BK t rn(J=űܬ3I"ke(dG%oBk2ؒ̕6Ry_ G0#J%zY%( TRzGu*`r`'^3_1"ZlƮ\Wi)=w͘je^ߏ$fa >9cߣ ˰Ar]ݘu,owVUN1^Ev7? N16,eMPƽ=-Yyi1sѣ򆍋˳ vu|Cr܄ggx,):OfIkƆ5M_E 9>;>Ϳ]oЍ%ҟ[Ef|WI6TLUEbb!6 qvr7n6? 73_HdR6OmmW7o>C{:g5z%}lV=1izZ6ِFya%Kז ݼE#m_bPcYF .3Pfθ lpXH$iFl` *6b~8e o ~I!8!yTn(k-=^&u(u O`D I#@sWjnk^DF%AQ>:hكMSpgv/ÏW F_sW3?z[O*|,ʼk뗘\VAE.O:HDfJÒrgny%P S-4@WH(6A0ďÞL OŧeO?uSX*'k#HȭקR+ޙ4ׄOUxg1#,l mUBʭNP0#{sیrG>H["QNb|x5]:O{F+>y"9Ki f^ ̑=յ+L4xSgZK31?E:JMKqEK@>+5?(3!R85-7:^o&<(Z~u$?ZM(tڳ!D1{zq!:rWyOP=htQ:h}PFUzz3dtdŰ6G>"DR0J+%ת5uUk=G"s'`AI12B-z<8?7Os/8Q<=6F at=(>/~N3A].'"W 1@o/lh%}R0Yr}Rncacy?ˆ:bO(0Q%fIᕬٕkAS8 ˃|C#DYjŒ(ޢ3wuRJ*?Ninбy*@N%E/!:cDN`> EQ쿽 arX4}iiI ܼGo p,_,_Wv}XX)W{:U*\>l?Ĺy=nxkM/wSu۪]:W#BXf- X +,4#V?_t*Gܫ@sA;ngdZIz}gLgBBw/]>zumbDzYEgmz͋WƐLS5o[5džAv i^]%tv壬"󧡊ƙa\{B|%hWY÷D~uaIl  aۯz@'Z}}xO(b0yFҗD9h5o7})RE\&T'J~C#oNa'Qf  3/j:1M?KqU4Q:~~L緡ZmJ$CHmNҹщ#]%wmn{\鹝13o|Or씀 ҁBӦriKeAʲ+ro7Hl!> y뺷c WQg7|N?pqkaQ++Ddyzӎ5ZG:C\b+r#L, y7,y[;:_c%RT%Z:BIbzD:4_A9C k Vo֓P]韑]@$БFVwrCȧ ۰5 ͶӿQ &ė{K̴3O#rJbYreD[ ݫUhljݬe6ԵpmcbNfG /]$V&(9>SOp3wԀ&D ;wa8@3u vf~wL{!M2TB AZbbIԹOEg[VFb|\hTP\,f: `twPl k)t k^'!P!~YbຝT93EkjBV{&j 05΍6b8&d+h"0bjágPx %`o uf%b8V7emXYIϼz*л#q<$?[7kFS6,e–,Hh}Iij/Dt/KFf_Gx_`Ywy`A<l$Ēm%9Y/T@hQ`WM^H %z.AJ|nUu`b/P7O< 9ui"@-bЋ=G:t[Plߧ"<ƾ8u0uvD'LXREec}i F>XӭׇTK-{KMrsfYooZ$Lב;ȟ*o t#/^?5{oH.S~ »ys50[1P7v8l\ Jk|`6g#b'"X.6 Q\֡kĵ`8jbl7FF>Rc iy s;:Xʐ޴T^Pbظ۞G,pR&CV1( `/Ӛ"G%߲| m'A Dɶb,)V߮zv?_k bſû.LGFgLU)|m0dBb>&ė[ %oύhު-]fKkFmBxkǀ4tεRt(V#0+g`Ρ;\z]ʲn"& +s}jCBCMM*QBQ-.OaiNg F( HX-CiԼX5$2-W?n-6FL }Ö:Rhy2@xE[xNN|~s]ʭov u[w3)";+\>fRM5$Baظr y\(kSfJ>j1%쒉[5{ֵ 0>NmӁ Xr؃2QYp⹌h!wv{Jf~u!6\d`#>D=9씧ɲ-\%>6c!A!`'/Y۶Y*,?Ԫq=5xauUa!$DkҾ<㇕ @0y n^/{Bf[xHlp^P:~&q4U!ag~fn?3"KJ+ZKh 7w嬼ތA4<\.dec0"[E5 Bp2R/WpeGLkK Mu*!RS+E1uf(6W$~rע6qc3W[m9Y"Ӫ`TcbUli0xJ}e"x8;+"?Sq\IȰuebMeh\f~sΥU\'J 4o6QyYwa6Be0QhPGҏq-: = $e=IMG#ӌMhi j$j ^|e*9KU,0kHR/ٟh-y␌ʘ,쵎ǻYŁŐ_7LU$y'ŊqrjތB&-^_,F4a#WMĸ%a o/.`R X-\(!f$. h$kET7g.́Ks,BcHK$egc)4 x&,ɰY g@'A};c&&ؗ@>8l_^%P YD]0"6WEmo Է+ZN"~F"95/8b-IϹ,9DzM0Hхzcu-QY'\5b6NI8 jC(wMfo%B N2kz7Лvp+Z0 J~Z؟kd7z&n+^:)"䈽^9.O$%=Pqq 6}o* LSC8=9#[4Je/І*m>&l ذ1_K#wR{(Ώ#)QFXT$$-ia.Z $(Փ$Rs+#yuo}gtV ?閊mvꂞw?Uʳ|<x Q2ٺkqT3LyLl=_jPkw߸-"p1{zd=v~YR@EvLTةԐožB%GRp>g5OVO<5?YdeߒUR[ˁ^vA a EYēhxœإ SPÉ7x/\M<0{0Ljm'BlyzYaRqQfu˭ B15{ &peQ؏=}Ĉ~U?Ht42:bN{sD MmTZu0ky+wd( I+}@ g6f } EIGe6ܹ%-nbvs~MZy_Ћ詸vW )A%s1)24^+ >!S%7"Mw(.lh4gjihS sXHj`( i1biPc? `MFR) ;a4r"?%' T0#u4#m'x]JW=7acTð囏1оe.~UЋQsֶ6( ztm^p7*LZ:r$j*uO3ÛR-jG .H~os]QQ S(^@s?zP%^!c){Yj< S;KPt}0_G>QߊmvDF$*os)x!A"Ő\vR_i NP>gK-4d ~?~:ps Lɸ\ɎD!v1$x~{dh55q5|JqO~kCŀBŵAACSbG hzhtXH )!҈AxMa"D#HR$*ZALCrif2ϋ+Q- z~7Fa+m$P!{drihL:q';F O`ߛ]bnZzc̀uEEE݆@>`7D~)OKҢl#{(@-l MLX_S<9JHTG}bIBt) Sєሡt@uScFЧ4bᢂ+fI(D)N@&m:LSc0}JsS8Xh6`_ޥV ܡV/Uln P:ea&O*Bͬ>߯l`wsȧK MB7R+bbRp?l#ݝv=@&3}X;ٍD]R,/ 2}+cЈ^.{LC{qvr0 CU$ ~@6W 7rVmCk:AYt"l5v#LJ1Ђ} raQ oCr3HO v 0>A(MbH/4;Z\8*NK~z8sg4 bO Kƣ!S+KLt<*B\1K=˵Zp G/okUD)d\nko\7zU_ ]  [\ a0ڐ2b;.$ -%vm JˡqkJЈU̵^4jP˝~:bKuQ²Qƈy"e~5mZaQ }AYؐT CM /N]-Cv[ *I@@e\o"qP>_Z^ܿ/)l- A`c4q zx/4n$&b E"[$Mʠiw2E'uا/9ƚ h)Ә_/'$B˯6UC۸~0}VPg7}T8ÿ&+II@5 P®Sl>7͸  c(gdW%IӢCkĔ*ppcl` R 1b5]ăDcۢr9Fa}p^ 2B!A6r?f`g"ʙoQ么&.1FLa]tô< ΈEVʝJLF+(8]9i %KA"@EgƲ$VY+]{_܋Q7 tŞAj S]^1婞Fԑ[u"W)ٙ{ݻW gZgy%y9TSZ9<df|, s M _K֑ۮ5=~!i iU0E)_YR} A oL]̞N;<\#7\t]+^jTii#_/} 4*͝i39- x1`rh\dIz B`ˊrs:o?w?OFχ>/dX2]\/{G((QM>utZ BZ~N7hu'4IEPY%`'h46/r>o`H|" o oh)*_ׁH 6UEVM-sLw^nwf+.*; ftP?!VLN$۾JTnPՁMZDY`Z`ɺ|ouc]# g*M#R :J*⅏rg$h[\"RXumF3joh,gǕV+<$h+n0 Vƙ"#]BpaA}|z'IM'όOK|?X6f~[v)MR5}  qvbG@{yL2V&sHp+g̗W_[nSu?‚no*~ !- PˣI55[$:uO` H(/t H׹ @a==*ΆUlH6 N~c9/kAsa`rtbyY>D65F΂ ȶ*HQM%呡]Uu D`qƶb)=A`eiE7xuǰ9ovWgfQ\Itf^%INC lq##n95taam:cHěn@bƣ/md.>eMjV~n95So'%GWޗ{ w(36{!?7|ne(ɜRo~fPZk!Bì,-M tBd}@Z/1j[Ήӊϝ%/C{F'%2x5h?v0eNZo@(v,8doIk ę:#+?Cg3;oSW87Ե~lf^*~c[;\#"M61R?bΖx#TnZ?AK0jos}$&>Q>f y?iFz^QF#1e\&Hj/pJ0GkUb*7R9J?>^~𑓧ьB l' Bؕ3ǧta3DZk뵺-];3ʔp_B((EA!qQA7b|_V& c[fm?%徯uq Sb٩#_*ǽsP7/fR0D9mϥK|\xkrA7Iςhda44Xo_ Gko*tE ( גK?Sl+2"c$|~ ( o3Y[LNNdcZXkz||tnGQ1vՉX Q3n2T4!ܻvF'w!`iͶ-z̙_)WdD|ky5A#Y=⪕_MT]R>rsOJF1@oH􍏏dꤌ9K6O[w/_kuWW Yخ5w&T*R6~"5$0J2UAOy!"Iaic Sу >=hꄚ7׈րl,ILcbi Y { , Žw OAstdʧ2`]hƩ闇~[Y+VC.N߯|pf%SF#uUphoǽ7^j=0?9'Q|7)u%E=ٙG&c^l@L @j|h; lLESgcP5]%Gt DWƬFr0H";^ȋd^iᶅl8X9)Μ@mGD,Enst'0T3JH0F1d[(800J]U ?o1r8;pܞYq)C!_f8>!nt`ER4MUC{V!"Z3rA:hT;ˠYSX}H A16[Nl;.r||o *eS*EhTAکӝeMWs ةn 3A.sB~ m jUm ŞԤ信nH>m ̑C"ۨ d@^S~:1&ݚXo-+"X/ZKE 8& J+Uש5)fͳh@~n ?Ѯ"N"|Y< PmDלrˡ}gWI?niHEБXBCqߡ 5%-P<0. ʲ]Jǫ_x@`K-}s^, u#w>|  -.*˳Pd|([yYm nsf ';Q#^AA,hI~~ \r~U'km]8jC X_{7.4mio[ߜ9V ~7|KNNK[?~4CiwirjpNR=hh=35JXP\}KюM5K?g;͗Fᘩ~5vG2eMXY:Cߍm0׮/)8Ʈf1Ч;)Ίp ?7 #f{ )6U Ǩv!O 1Jknv[Xqp2(e}/68vʚuω"E3s wXkv>N"Z\/?kx )4Re[EWo; "*+}Ir(ѥڒ\NoeUf$`CĬ߰fVQ9` mEG7sCt-~~Qc.'fab7Yr1r`܇L٨gC?J_'J`za9(BICtI-|}"!_ L131< 5ШKi†05,@aVg-v ]h߾H?KfjruqE=GiJ1/^[QE5:)}zJK-`I 4KJQӢg&(k+9O l8-\j:yTMGE6>t Δ]&1C|RDbyOh* ո-(YuSحFB?NU R\p+bRfD4(Y|C0Nf_`3=Y6_^|-E6i ;tVz 0"fiJegS/zE2$hf(DFdfԄE#!]1m ayPACrUBCMLF>´;4֡| :Jze:|ߢw=МC ]*~`Q :9=dI(T8HG6D3q O?bUD/JqX&KxʆeZWؗGϱeP+nNhnC ?vyM3hy#7qhM1{I}J6 0 *Iȵ Υ6<|͊.w_iC=]u{^ɳ9l"oF5-C1}^(wbYܐZu$5lQi'͖t =M=;ӺВ*VZs1_v__mۡª$rrGǫAU2W|K֞VW+1|y5DJȩ\O@!~OYNhxe"Wq@W8`KV?Bx>KUW$*;rF 1@"xڂ;3>! s0((rps9DLJUXIGi/z]6.l}~F5[#(5+˝@ڿZd^rbt*;QgNЀ ϡ1xjӉ(:]q3NoJ _umu[>os,9-!d)4qLi9.f!Cw:ڇBʼn} iYӬ .Slr-sdzs$dgk A]H$YfUby_#Ap(ߴ{cYE+Ϳ)Z3&JIqqN:rb| 6]ryH#k(d뮣Fm^i+,?OE}v]mѲf>P@>&`_<5:)g,'d.ʲK(;Y.8+se͚Y l֠,~.sE7pf^` K]l5i{,1m_\gQva`bKbb8egNSX! jo7 b%gjY{*"Z5"5uvyM+!1&;;6.֤[cr.¦d3.4RѼ:%j:Nh$ T2 R~L6)`TO*Gvq2-*oPTliu'9;G;./q+Zt{t%<6 VFeE';Oy-%az;JƬR Qs9>eqq'%,Mfg1z_[z3L8!S5O'wox2ty3 Dk?ofשkEu'UݠĞ4^Îjsc9"tlM.ӿuC0pts﹃xJgyh֫*;*gZ|ĕa0wra1M/եZXnu#[A;W`NL]&(Ya~3SChc^̑lAѴ- s v!k0@YN4jm}Im%ϯM@b_T@HK!Qfcr8;̝vg)(ۙVɧA?d֋ov$nM*`ۏZ]f8<v/"Hix~<87*0!}XՑ V;&W< icA5avS 8ZvMQ8UcP}Yk&Y CakR owٔ_D>PBB op8R6.̭/:5^!@e*V;GOs زôTk?d[2/: Gy"!w dÛ(/[ 4n=YOd pP HX~k,;f/&@hm% Q]G؞3 fе4Ž7P'؟!|(TTo~T6o`Qh~yU+GGv k[d9huƒ66vS1U5^>$e/}[W7PfC'eF h N5W @yoX;Ֆoxl/:űyX;5a Wl]|^v]5gٴQ"AX.1QSI{p@o6[nlܣoIGf` QN 2 SU8劘'=YHI]ֽЇ\oj{F|1:Hލ DtcsZx9|* G56Lb/@E(Z)FW*z53WgYFc8[l#;aT;q! ,`vъq(ڬѶs=J칂jdJ 9]V=ZUb(.aGSe(k.U)pLm ˩z5r ApϳB0\.͖Ks†‹LU:69靽Zc^gS Y:_Dp#{^vfk ->lҳa5"Ɩ֡FC;@Q>4hÒ1-Hc"[^ Q:{0@ok:Q LkW_o/XFGEsd@@K0V4,q}~fJ<; ݫN;– {e9AT aVK)i57&kF GH|uKcbTw]j]BVI(u5вPM4uCCC@>`Vlk^]O-U$ ='pE빕ĩ Z%Ģ1/x\'y4IOe%&tR|MΧDqkSk*&-:|)e# ZEoH'-qUD}C4S6kd6, (- M1d[[P\P~. q `nW6YAyfsH 1GKZӭίrmHֆ3PKLv̓i= 'zP D!o6T*̘qR7~P{k󳷺VfNO%Q̮Y~%@DS^&i- #9T+Iv(&S=~5L;A.@Á@A> `8R3Ljߨ E%~\ lGڴ/;$}ɲZ8gJWDFiSE`[d Vyf`TaO."_X2Dv@i&%4q`#MjmR~;p<3R)~RNauچ@lO`P$1`ԈvSj{a0osD˙T<[=G4];bW%}|4}-P\nWt/M4Fl'i M@JxyVi9w/wVK.y5`h ;, YHCȭ^#q" NX>c#4҇3cL6%4 82k ~nK I\!PjаFwsk-|sG5G#[8Q+?A?@ف@Q>`SY7 g=Fu_%Z(~rc u .}KM2Cdum0r5l|يX:iXv:?!]<-Ɣ![LED\RU$GM>}rv_=Ecֈ )ŽQ$=HSV8#@uܦՆ@1> `8(.%Hrձ-iΣ=ps 8fɅ-@fw(z+~8(Au@ @>`SYɂ4.T1ަkIƖ+\$&$5z\us.%tR6lcM0^k ) 02  l߇赀=$o `6-⸸FaNfUeLt07M*idBpltbRUT -`z{ R+tw68o.jc2_r2&@ w!&A-ƒGv *EtC2DOnI*k"/*UA'`gkīM-V3|)8.`XWlֵ 1ɐS`p@U92/&6fLDY޾\#PIڌ -|SpMTo mǙ贵׸pE 2Wڎj~cAT;rq2RV6\ő1c*h;&SLyLRm/} pu@@tO`9B٤̤:mrrQS 5>`C;dFʻq;T&+mQj.03Dn$]A j+-8.0I` A[A_@>`cRlimȵ_0?$y~R +0ee9mQ\\k̘Cozbb]B5-EOg1}F;XrCjj" u"CDN"IGn ߐ1 `t#WC6z:ÌĖEYTʠAbA*6G_w+wwzlkŋ `0R-W*k}t20#\@ f'7c?xA\PUf@C .ֆS&\UOyjj=5eBt:(a\~S?3GO0 fʚ͸KڼT_uG@fkА48'0T!]qb% y$ t6+noާtz%x2 n_\^ezR"6O24pu@HO`,31=At3| \rrC~ e$_z}3DnUUa' OAv_@@>`CSY,HZ[?܉AY,(N僣 x*mI,#\ܼkkPrMSx 6~{<u@HO` 96g:93D\lסO` e]a@XNuO` Uq{8TAˊKoF@z(pgK@Lcۡ̚i+"DEau@3Ԅ}f2pBcut)l1Q:cR\'-9NJY {'%(ږ1',B`Pn8Є )qS3Ys$iNyD"wV_Di7 6հ?V8GReGĊ2鏸vuE[-MK\t+ָ> 3+zx93ң|(AT #qT{_eam,5~a@ @oL͖`v {W[`PlC]W! lj,Xil \4I|Td$@i:qͽN>Mg, ;sá:'W*'JV|Q5e]`&2[5*rxʼ@@hRY4Aa>6h\0Q@4fb׀AsrA=k0c!p*DDyќT9&\C0BYQPA„nMҏ7 sjrں =Z, P:|}%Q%摁|@d'g>b /f%Y`OsⲕqkTl,J aΙp彠@ɸE#`(LjC/sZ)RyNX(˄&7*NQ=;^gZ:pHw- # 6#7S^g1 }g mVzJaFl `0nac֮wL&lAe lXߩŚ*OY_J"N{BPn.4A5:%'ZldT`h ˳dvu̦{T?TIMoLћ$Gq<* >!uMH~ٔתS릿aT+loR9'6)49|D72D|AO `ԗ!{OwljQq$̃ȅ 'E%b?VٍͯsfdW`K0hN"jP}d]r%nhV؟}%꧳Y 'IC=9?`%MH103[F4\N_ >I~7#l@-&Q2="3:pM 6,6v܊ѣ&' )R_TPGU8]?>DEe01V?QmZΤM1. ]#PFRڔ9B$mAѲ:7jZ!(ɰ)7FiL(e `z_U{@mUC7Ö/IU \_ <7ґ%>Ke|}S4o옽KUЮ˳+ej ?g ;r3g,T#$fRP4F.%=oȒjI 91VU0 -dG)QR\g Ur,N.Ab"/rH4| 0kueZ4fWu&A>-o"psl /OHS;3c& 1cSg,N&cpđdm}sТ«@߽W~IuwYjEUQ.%TOwpm2RSPxXnّ@۰- Ǘq`$oz۝ʆcI__gN޾EL"lȲ̵rǥƷȞ=絓Ki'uU5=-˸5ΔjJu .݅po|vgpjTԻ<@@*tZzT[",3O\-й K6c\ R K~nGAN3;O8uQ `µ ;_Q)w^P|yLT?@_]wm|!a/< >5Ξ-ttq^,N:=14H'$F1f Z&Ka#`9Phu r4;b;[,5>)qQ2k? Ł!q~nOO,8gM1$Cm Ifiĸo 1N=@jrs%?m~ѫnjEC:=MEKr8OG<`^+aJW2zU"F3}DR8f0$,}&gI@HE` ,nGm?AsYL9Rb1yKKԟ5T9=a;m̥PUܔn2&Q+E55mUh8[!ƅ3d܌l'g0!֙WcXƉ2.2lyS|J2OoYwgދV6rMADƔgJg=ZץŇZ.?8_VjvU$qy*F`"y1s/RKg:U|P4PضF/%5rj CVO4=t;6"Ż>G jZӘ>(@ZN]jl_"&9a(V߯&YѺRflu ̐ ;#}bW?LfO-k=- v[7ElN2?J]"u_2)I#)`+ UhuOڭ dڐ?e C2q+Q\Vtܪ',;.Z i"ÚPw"f~U-ΖR3aB}-퀢, P| q5$SA5sFKXǭ7. * GVD LSMbpeOBiUؗ詡AeiNV5Jc@/8H Tg8seM$ðs&Lw{ϨXA{f5ɚl+G |YE++]"Oj|Job[ w .u=*:] ]=r޸?'NΏ&ptZ2鄍;4͓i\f | w*1! TS}(,`|*ٯ 2 tɵY%(W:4B1@ [X @Um0dmY$i󭠪V>Y\G}UN@jGa6EVn>[ 5ŒTO̺T.kN`Jj9G\wq-y2йt Kl.fm/m}#Ov=3(ñnzZügVe{0d TC_/O(갸h2oJ6I\;g8q( *;/2%2u"_ ?P8]˩,ec B_GOMo3=:@L7rc7?ij+@w]__k̨lhZSoF+dA!R3>]-0m%>:]]?;;&{tPu}5ALȕ=-J9e.{ ~it]YKǟ/;Q,H xa*P>|*\'v.VͮHTZ`$2A9.%MЌ0?ā@#d?ѾKJPyp&c>TY1L T#bIAе{@+[̉g;7jGg(MqWZL2tA[)Qn Bqg#}&3o.%'KꕢW*^0}ڑ Q7n˹lUvJ`eĪzU  !Jɸt[EV0Ǥ4(߂Ui`lWZ ϛDA[lU(A"ylZ[SxaE&{b,+S0;ƹ[/2s WG$TWFK>e ۆ'aݣDP"lLx 2Y]Ih &]5j"Xb%L28 C1k%!)޺5X>;~?:Nw(*@Ftz(V歲Rr|.-o}0w}P IZ&*3'Գ6[H^r'`}AѹȔZ;=4#%8JY?=t;1šNvx3ܬo_!Ok:xE="ښ ࣣt+)oOTD;»,Bl%reơO6TW_>4Ϝ3` 8 fGdDf2@s2 Dt6=@wwdMkՂ&{5@jG7 InRs]V-H⊙>H/45[ ,DEJJc\,CڻMBO6 jLSgvcJIx8B 8y}mcc#jӖ '>&*bo,7+c]Ajw]'X rwK&+- *eZ*ZOG7ƭX+MCNp#i>,k3h=~s26C%CsOPIFTe яoRKJqM6n61#9hءm-'"Lab4C׋'=28շ!9NJkkmʊF<( rٶvv "$fA#!(+Qa:/7GoW䴱V`)"\/\З;YIK[I9(U`; oxlq-e2=U &d¶]uW GoLmn˫!V!8띙uΣe{RaM/@1zכE.2C&F}^gkoQ7"Lޜ~D^W~r0* ;S2's`//[}榳UsMbyM&#eiP'f)Z=t 9ҥ͙ӃH(٢Κdc~QN0!om"ÿ~wE8W+vt?t P([`]=@F>Ѐ+@hq6nR.Z8 Ӷ! 4gm!DdEv .M't-S 4:L+q[~RI}d39jTО )d6N5H4]A8'1f C#E@<5|h{ViK;A4tn ~ӌ͹Zpz8TpMkomfAXfr) !KgN9>jA;>/fR.%ƒԯz rsժȰB11cՏQ"mP-gU"=LxOl+'ՎSjW04x,-_+J K]j/l;1@*U?1Hb8)uQrUНBΣݡ(Ed #8>[ۆÚHH8riFjO^gsC30"Tb (es/Wtde󻮮uvaT9b}E. #Lzyd'ya*k|C43ÂS@.t]In+Pe%Z$!zj o!s,J_*sگ-5 ,2`JW,ODLC}WGiqoWG"W ѡ!SFEtq.?7M't:!GK.5 v+`JkJD9UrYG߇ZRf ``Þ3:|Pp]ӢHQ龲W4`Y.MG!gqO>UjS| Hh ;`PZNR^#@CkA:;2qBQf{N)Ռ.W]@]47aB6qr3glu7+P Ȑ G 8s1=Q1ܕs垦O(u=TSw'}K7Ջ"آmpOr u("gVv ŀ-Fzw_"K Be$#aeVFz9ϴEY癟?#laj݉8b.Pny+H J5- W܄&=TlU*3J :Pfwo' 7?2duWۚP.p6awJg#jAy.q,vy{ӂh7ߘ`}?$Sn*}e($QsKaG8Y.Fֆ  T}ij3w%({w6w9P3Zȴh**Vv5p$% d@L 6^ASRFݝDs:Gq-O]8eaVv>4f$>2 /3WӆkH[Ŗj2zo_BSYJFA>&h{ +ԿkW}PCR3ǓG^bN5e1/ܙ<sc/apN12kH_? fiH|D!CT" 6ay=[pp%qꗑ}O@JPn'#ЁȀA~*/znEyЫ CHxe icp7;eAzm~K8Y!T\H>rg\C)@PZ-QOS O?8ջT-o O1lZ1yQXU[ "J_\7 rACxVXQhJzN}s9pB0xcUL^Ƃ91Uy Bu  d/P1uGy~z"[=LZ%N%ն`4aK\` ;pɑIyN62MI-I0@Jx504Mi%ʖ0+'ȿ GSxҵFh֚_;fɌ麝(bkGct|~)ݳDlՍ { شxs8 Gz}+lq f;cXZVY\+u{ q҇$:4b,PLN8d~Q??ƢsNNrF VBuU :SyP2O 4q` ,X;Ȅ.׼r0A3=T/|L<QG_Ӷ ;,; nјRZ csb3 0Et7TYBŽ.W (_˜ӰEw[Xl톬Q.pn^Ow^ rM}jeEj"q* lK2nOfg\, _cWa`*sAY~l#F @%+UMS4*AaVF &uá& u^Lg;e8dj|+hcA> {Ea]S k;*):t{1PÓAwƺ)U)KW.B-Kvj<511"<5!03V}Z,)I4qD&""a\,Q+g˾)&xmjq#ٰLI9JaVgM?? 56л5R*Em{<cY=Yf}R?wR`,7( :F7Uͳ?4rB3SWkfCϞ)K߽5 {jH|`gEn柕\?1^7/}՞ [eBR (_=o=v{#䕶k:]Y~wCjF.,ѶC^95)"\=h~)~aEAe1n͓J+RU7ׯ骠PuCCCA>`7u @KT/=Wsh͚)M6 a9}lRhߪ]m0SQ<,E?6srZd*YB.,bv(#N}OHEGԴ<_2֚$T߷|0D%KਜZ!nrbDXߔZ7]^1*|sґ`Z-?DR8z#Sat,Ӗ=@~Kp#I_jIl6kOW"K$Ds@@g%z}+~Fu#>n S`&F4EͱR]T[o-ܕxo<adU1nFv~nZQYQRg ecvE1oamFnhCBc]]ـ ۅn hdm-T3Aq\4%55 汏&U~'tl:?hwg%pKͿaև< x:>xRK^fsX'$G>zQ$ɴ5h(-iVv캅`$1 Q)J43r}୕8NwQ q. y݂b]qBzUcK(LJCsJe;(&:ҍkVA6Gy@ֻ ~f_pcDƩi`M^GA>8i Ap_㝄V~x[ACdo{&͎ZlV2{Ҷp|)*Ek'>euך{d>nK./% UV$ӡZ-@5cAb6MwJnwq?"M _OծGs8YK]0-C2KVP2O8$(+۩ɧfۿ1˷:?0z.ES\uk S?s(п[2L(SVCqy[Wx$0Y#^}f4=;GWv3ݢ^;~2O 9b 'I)[ Ph ٫.Ԫ2[R >l:{ N,r;O TNڑ6`ȭDkky b+P]5< Gqgϲ/"BD6K!Ѫo%F-yz1v, ~Kilɨ/⊒20`*;5épb59d"?GWo_!C\(ʳD waJ7f)^&"Gs#q r6)UQI-C]eյ'fN}T{}RBW˒%%Ci~Ye^kgbKQu>f ^ Xi0KsS(jw(Z{`%e8$FL<&C[gNM I7OiiqpE45C d[_/&M~va 6)-)L_VLEbכiV t+&3RzQB !#C3L4AJ "\+`LB_/jDE֛]:,cq+YL_S+'jW|0 n?)BA/3T^hЉ;Za$^==H<:i|-lNsbnPF#vu"3yӼ1\eZ /h3Q<}:-! b͘'+y9X6$ϛp AGit~io1Bu@~1[_2U8n+].GT7ը Uh#'~,FgL.EX`);(BbS)^l/[~mi8 t D6`xʄXѶݩRϫurfìA# {O"J88_؛ژ h^n3;E\dT(tް ,tO{6v(YB[/1W{am"XlM؈f0uB|;*1Z(CU[ 2~̆>?LH"@m XtE\RW6vC.UB2+¹w[Vi* k0i+̎tHd \*0v h(>ڋ9po<7Lućh&WY0%+g~ >C΂ tYJUi:z3% Q<ƾTR̙99 }?ۧ[U"%L,T*AEYZ++,z#ljcAQ%5>$ 8^PݡfH pz/򝒾+k(uEEEAa>`&ƛҏ95?mH<#ɹ!A";:$KK&G<*Ss& ^,܉X_>DS$=R[﹞{@pc%4C VD1y>=Ԡrv%zMyp)i$ojw;W^_\ ÓI%T@ܮ!]!=k*t!gh%ܳ5NkXW+)yO8dcE3ZTCUKGUġ8 QT (RNܾA~V<`yrz0Qs轴1txP|BS$]0jJ]5̒)QȚH:ߋο$ M o"p;<>^ BP>8ox3De din.5J.'E ]uORZfϱ񲨏 mp#oU- ^aF 2pw(ҹ ,F7MmbûYv${!,G+?:&jCЮ\[ImmLà/g '*HբںggIayNvH'3eW׎'tH' n5xaz(_j`T|e-29-qodq|@N v }㐙M7/0=9ǍYHSj~OILihM;e5nfk֧n 3b:eoo$͐ TYoFז*9u;x&J.&'\:'z9 b:;?܌TFLHm>یn9RVu=.6u(b:`4mYD%#DPdcI*)0ibaFjL֑TuY;-`"3 4Y,CbL`8 }XsTKlFK^Fd̹^HۑOlHNtFVn;#5/1g"/PsiCa"Ǭ8{Z{lk/  L6Wg Ci&?8Ks_CJۯm|Svᦫ*,h![Z941 ip::$AM( &^8p "C:VRАu@2TUq3܊h/~M;I`f^-qNYiE`*br5KD-PA"k^[gZ ) I$"ĹԚtx]xnMB ԛ+3KL*3/%'2S(5z-NlE\m 1T ^q\YbmQB>/t*\UXmZDYgjw?XMކt~`J'xTs`G'囦},!tCb"wo6l?j騣.L{X>@3lۯݖW<5a2Zwi%]a@fKlv}P1 \ulXg!d/_j]i6;Y]c$uҔqS9 Vet>WY]!"ewQ!({I* \2(.|xgq#Sį6B?WS/9ԇsԑߡԛ 'T[~cOv 1\ǧDLDCІ˅\܏tWT{xK:"94Xx7>1 k7zFDW#R? \fPc f.p"Ar/ZRn g?ybE5ꕴЀ@7OT9jT£x4$•Vo %hTk3^n΋ ho69p9\XX1}]YR& _K흭:s&+Oա2 P4b{u^g{\X+ bR-M$4Ѷ,qM{1$ݓ%< "Ih;9 Va ~GCV.l|Hv A3~uO̚zr޶v Mmڔuwҳ"5)>C͠eI̢ĺr#ҽzvR?$ܗܞtgRWHYU6Y;.i!w?`0CMM¿Th!v>4q]ip(hl2Zsv"i_z6Iܫ-@]$- 3qAs^8-mTE1GvEjeL Hb&X~{Іuf+.F x s>]yxn.;P-HIJ6CS$ayȩ` ;J"B]͏R]Vw+o+qoMajBmJ$ʘPh {ǂOk6}3mwf`x,'2{>QF-Weoԓ j{LN] 8-?@MTOG^"E{9uF}^ sfF>{QSà YDayNh,l5!f\~dSܾCd* 2ؽ4t(UlvSa2aeF)DamhC3]k!V ՗O'b?N {_WX_ /jN#tH2ROEK[; c Q 0rq5iN |gGdNBތj|Q>[oc0gj@y!3'έ LW Ym9SSlPaC )USa&’Z¸ f,_n+TU )^RX<[ј1?B% RۓqpDM=;p8[o'f5KcLuOgջnnՊE \=~Nxh&#؃"R48(W+wp#gyjrSjuFFFA>NmBK>|_[; >Yd?<~=ޕSH*ORv'/z[Ɛ/.Onaj\H?5%`5M/'p䡇C R. ;M0Q% U΋Ȍr/$6|C"*)hYimE)z(G%^Tq9Ũa_u^ O$KӒ`8qW&xP~R;G\T%-ڞҪjH\EnݑsO/T5]Na?@(LLU)a"3pf$*QNצ-)k*pVW yˍ(T]oLd ܠϐL.2i)xj/Ojz. 3G%b ]-1*EVfb+HJ K]BP5_\ܯnY*a>AɆd[ql7G^LF݋xu -u34rmz@bxS!VUUaĖxr\b?Z_\`y&IO2GbbQ5 LqwW8AP zn1C.9<(m"GI#< "T$Z`l@ň5_zI$<Z)Tq~7`_)1GL' nJ $55"~}[Z\gcML6w8a=p1u%{.GA͸I؇5CR Ejh  4J"K_&JtlK~_`2إf>pPM!)3*:4~s<EQKX5>N olஇKx@dġw;`}ԋfG]JV)f4,MKj 9&pA¥{x﷔ {j>"DE(/<)楰vYf[c =d* "輕rDy$#Ic{;08>nyr @Oǟ(-&'7{J-sɏ*l9)֝hI컩!\T?/ c)H0UjP bcxטvQ'E2ymVw;߿e7\Ge6'4AƐjڊZ)"N0{~DjI'ס,-q BAeEcu s3k!Uuz[ Fs3C<&Ȑb4h¦`#Z, :sI~E6}V %B7 #~]}6LgVXo[BPވKxLfDX) EuڿMHfǫHʍQ^bֶֽU [F1֠KF)Aa>&h^5 :i|_6CS@g{IG6V5^AU|XN Cn p?ux*G U5&滥 _X/{jUI#XJ}Kc,ʟThSRA)5fYiTLg5΋bq](#B@Ό\QK_dH":Y:+K#0*|+ɳ]<[;yпL_ }ފ$J wSP곀JʢW}$e]9g*o?v>iԒEwcVpURF'eae7}RŠѤF ?oPX32/~|Dp/̼vv{!o)ȍaD=rћ2~W5ͽOڽ:َRE7=Q U5L2% '[AInE_tMl*9Jm,~Z07R+@1tTRa4 W/>2鴎 |?R{50l%*= sC>أM'W$ȅDN[8FuDDDA!>$cѴs~~kEcPWZgZVt,ު){V9stY®Rl硿f׶EZ?DvܾYҋ^q>-7< 5A73EKcM \WBe2X!}Am9A0L]>w_wwzk>-QT6Dž&:4ceS7C=t:c4m\"f TR1>*8M\38*6r\=Dyux٘Nf/ku`>ޣ)3r;+ ܥˎ>6K^պJ\,%gմ^WмF :C-Y+sHŚ48&8kv&ɮmlٶ4NNؼшljWxAcWti]QlPحUDuI7Y ^({W %F>5r߷GZ)Mi^}@ԁgr4LN{Fh+I|zEl%d+ӇG`ţz dls '1"ŘGlvdT __Yq+q^62n' K ZNDڊX`Y:b-BlqXJCkq*=GBm%Z{휩ICVin*HPZ13־}&%FH}5= L[ UȗW8=5hP }90g1!2 1e }،_?CMȣ™JD ʌ?~`~Z>eaڒCbk!Y kKy~N7rj뻭fn!7DG pd)txd~<[iH|E l_PS M%ġљWn~G4A2zɂ`W\K%:س7orCy`l^K01M`YpkqL2[ee`ڑ5:YaskA)@́SA> `g@\ ;%z5G>N*Q`u,kۈc@)Tfc@x v^nABȢ ֏URyL[lřz60& 7Me`Jq3g-KF^};f9]QL QGн&S$$Ppx:}vSI-:!)RuҦˆA> `Y5Xə/,9ckyg|dgJ"o #݀Hd<)@}AO`(Y{%euM;Wf23'JRH h}٥qJXư֏$hUϸv(B21-uA>`c8WkՎA! =Ȝ0\LJa~a1ώڶ|vVz# pW#r,:hCѨrg@wm?+gQC^$ށSࡥ O`Ԁ\o/VZ5 u O` o䬒BoEu}ij}DDSkpython-telegram-bot-21.1.1/tests/data/text_file.txt000066400000000000000000000000161460724040100222670ustar00rootroot00000000000000PTB Rocks! ⅞python-telegram-bot-21.1.1/tests/data/thumb.jpg000066400000000000000000000052701460724040100213730ustar00rootroot00000000000000JFIFC       C 22 }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?ʀ=w?STφȤg,N|[vbbcۂϷx|)o'OO_Ӿ|?=tRųs|+dYn<{?7.o=LDO}Ѳ [<u*JXƌekY}D*WSϳJRR&zz_gxz S.Vi !pIJ|it*E>Y+wO_;C_14fk/%i/Tk' Դm3TݶK 0AAW8=\-WFydO?Zh%kՉz?ì߿5Hi҄p2\R? x;'&ZA+~m,%%aݥ5y>ok/SZ0l( n-nmRXY:*z1g]ʝ9v<4 ,u_zKdz'}kSSφZt_ܾױ9;k~ڿpȨG ф??,^o?ku~ ࿇0}-tIoP0eHr[,.[78੹5/V:xG4%ǎ|R6bqeq@F=x㱼s:w/GwV5{~xmMҲpѕ;h繆ajݖ;{pUz+M_okïJ-jΌ3?'#*v*pn [w-߫h 8B֤zEn3+>-6MWHI .DdWXҧRK61f ^l\xC":2 V >e&e%fI[ߗOOmWş 5)34 U|SnlGSۊ4ٽ5 Bw[5_z53fCUµinv۳¿oCS:F Y\Iy>\ H*5s8t]>~cg~W nT~/_v^еj,Em%ԸUzyB=_3qx,<5~&\ֵ{Xu9zėSGb́dhQhb^a*bOURmVUnRZŻ$Kuu`qrL1TuOɭt}-B|m׈>X֡m\e0r_b: d86e*YSZ6MwC.0|.3(,DVIiN^z>US_ Oot[c-),ssw癤rzEy=gBo{_}8Zu+ SL{{gE*uR?B;x xZEQ?g>+ GFX|Dy%f=FㆁHRRc lA1%X ?T-S}=Y2ɺ4KTR=2pSƟ/%[ԞW1ٕ7psn H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-:4iTXtXML:com.adobe.xmp Adobe Photoshop CC 2015 (Windows) 2016-10-07T22:57:57+02:00 2016-10-07T22:57:57+02:00 2016-10-07T22:57:57+02:00 xmp.iid:c8521908-e271-0442-af86-9a3dec5fc9c0 adobe:docid:photoshop:a11c925b-8cd0-11e6-bfc7-cb6206bc6d0a xmp.did:8d2ae8bc-ae57-9344-80c7-b693ea8a20dc created xmp.iid:8d2ae8bc-ae57-9344-80c7-b693ea8a20dc 2016-10-07T22:57:57+02:00 Adobe Photoshop CC 2015 (Windows) saved xmp.iid:c8521908-e271-0442-af86-9a3dec5fc9c0 2016-10-07T22:57:57+02:00 Adobe Photoshop CC 2015 (Windows) / image/png 3 sRGB IEC61966-2.1 1 1181102/10000 1181102/10000 3 1 640 360 h cHRMz%u0`:o_F[ IDATxwUy*vη|'H d Ƙ8=Ú]ubwص -H4((Ms:wWUWuC_[]W]O"q0 ` @0 ` @^bY)v[m8UireZհ]G)"bHq{#64iRREJҦf2mhRhRhRH)x/x@p\UUr[j-퍺Q7vnJݩXNr+ v]ŬK̬Y1R|ٴݿB$HHAB? !9SK>1`HZҦ6$ @fQ kFbUJc(TBڨ٥4nf7l%% b /Dtrw.QվDԖwo\[qbVJYSOCs8k͑93Gr9O5 1IsQU{T_,Bmب9]iSnص#ХԤ4 kBR "(q>BGbGԝ8l.3Rz.mRZ>eF>cSӃɡ`zf8;115i(. 5])7V6V*7׫7jkm+er\WvO4!4)|w}[ [\wŬ\ŮˎRbM SFtҵɡhvv$7;͍StG @knnUnVonVUK &֤BhwWU4{m׀!`nv\MKLU츮ɩXnn47;7Ģ20k bҥbVieK ʘZJL]օ6ۄt8<3n+ &vTvk|zl o$slz񙡑kHYղX_(ux~Tuۭ[.l_oұ驪L̾Da  |H&r\e;nv〈gL=eh'HA) W+7./WWtiRB I?=MQI8}w(6}=_7*e9J1=47zb'N@U:?_⅕nJ ]lJO" emݢ6zpb3ϭ0w)xv7㪩̑w{ϩS)p=娋K]^Bp]f ysmRԲgRMG!K3!RwLd>ɽѹM4u@JVp~7&u)5Iʕ;Rb<{>8g W̮RR>~xg}C]3/}o_/\^.K)3u¾eY)ؖ߀g ~ǑJ7U\Xf91g:||f|ǿq덛R͑i]w'uyn}åBxf3H.?gOL# `+W>+W8k蚼c׮qR ^Q򛎺[ ά+ G}w @RO_򹥆SRn:[wpok'RGwў,\K]/8>4oߚ/r)=mHA5nž]/uM5N?|@$7޸Uȧ.<}#&{URp&lT=8SݏA++ת;5xu i]f) Z+7)Gs'>pHDrٳ)]˦{(MG}T .(jy'0ZO}fq8gjR.ȲRbS~;̴ZOd>  @޸Yzjm| y/u \ *T&g `B?7|L8f ڗvWľP*lWKBTo]-[cy3@yzQ`$`J׾A"z@LvxfL G{+bB},oeiuJFcB  >oNv G/~Ņݲ/hˁa~dE)]3u|>_-.X$%94oL&y9C# n3aB_8_G @˗V7Gr&DľNޭ0uMwK[gY}fzfZq|;2o.{qX[w41W6?W+u4R_D&o0! KO0s3kq-H:^ L;#9KKٳ-/]PΥt.Nm+nA}?B\ϟ%| ./U>BJHʎVϭM˲FT=G^].퉁ԟ~SGƞ<<2ffHcDtqa˫yŅlJKZ":ynwIȇ|c21߼s9A>KWjə!4l̑ RDK?=>,8>=Gg}@0m{27^vÈ-2aisR\ި}0Arkvebj2wl2W3ǾXg8O3=>>{o- !&ؗBnWi8ʅr},'TA^j&PPfc?ұ6l%D8/E8ǤYSPxJ$]'6ܺRvW>1Y֍>ݱ筙‡&i"h8>}]^蚌ޡir|t :XZ*"&z)xtþ/Z)²k| QW+wҵ]k.}Oϕv';P~(dc{ϟ}FQyPƊ[/RL)]>{|˭ف(N&*_zW 5\N$aea`xD_ũ}'sZ=VE]ʲ^gISJ}$~$0԰]%œR=.h˯B]F(0y-p)x; >7ȫx)x4IDBv\|$ Jd8]ӳ]O YƙP)iޚطK 73RD` *&ANTU6ΦtM1R_x!xZ4CI6_bAFI1*&*k{xr7C)cb0i10dJؗ|O>Rw]fcAM=y!)CRI{P|6؛tAo9_N)8b_\4ԧu͛.'Ka_oӴ@Kkb[ 7dDPOLyɈ&\)8A eje ])#{6eh|t_Y_Rɥ\Yb&R楙p)ꠅ1=&0@ 3x R3u)/|ꭏ~Z+,͵rc}`MԾ!n/u1*w0x ̡/[ŜKi5#/\x^1eLξ{j{Fmdߧ6I4 `7aL15S?OMZοo\_/^zbq^ D]%yv mQ'{ތI"҉/͙P#g Gjvjjb}X3u95@{[' DS ?e߽V '}Gr1z>YRv\v]֤R0 MdJd7؋O(P`طKqKÙeq{: /ӄoWjO̟}lH=vAD"qgnthڷ$& .PDfny5MG&A)GHA{~:ō})0إ žI9=◓.0Gofa/Y g0!4Es[JF>5YĊ81$U1g`/֊6p>Vn5΄6R0&'m g'΋nNeY}E=}v|* Rp8`!2~Ï@c[4`5< C+ Sׂ~}}_lKA_! ^MMJA)x(KDa\:F)Kh "8iy1J_򠖂B 8MGn覣v+  u/J0:h!^:p#n)8* sgn즣Ļý¾{jQz@  g`!`F&p#ȫ[ "`@\ɲFĔC( N/=2\8tA@ Pǜ蘣x#~EOi> ώ'})ab@ WBa"Ă{-`kџbPB@~D_G<( mЯ/C'޼11-1NnZݙju*~[kϲ]#pjخXc3̪98`de~}bZh,5ˤzqF]ӎpĶ̭ 6 S I"#8<޻ $#o0z.yƊQ8Ø&,Y2%[Y&  -슖%9})ND,}lUs\?,18KKw':rLԾ(O.M K K]聣&@Q O 9HAݥ(b^uB)x@ odcC{]<"Ed0}co3MGa_)8=)hSboީ(o7"`Ч1p3C0M fQ 1M<DTL4U$ٓRp߄A)fVDDR !&5 T  P x5 שx}}D.Jݶ7e&Zu ]ӆ}Q􅅓 q8)Ts5΀!`!wSjvԡ3scu\;k|&}<7VL@-!Rp5Mr;[̾ K$$7?աlzX htӮmKְ떓6sgf_zc9%*wzrf#}[WsMk)6r(0y?7Ft}#} ui䄼 brͪ5S'f^83Cs#$XwwzIkxR:1a_#ck'g`$MGA3ϔ_ߋ_;jXɹ>==rhL'd;XS/>}3(e)80Gz't{47F[cN&kW7lZM]ͧ_|x}xp ff>eW)]! 0xCvB$%7L>jڡRj.U#Ǧ^8Gώ durJ 2 m*?16^i:7vW7V?4/U̎˖Z{tzO|DZgO˛qP"ʔKt\;ɲvdo }wc) ]URlCءOQ+D7_1ogK!f> mO Fkw&{[C߫1԰z:e}=z`J2ц.@Jq mdM->$'ujr]Uw?~O4K;H핒n+x)x@lSj(뾁-DZY}LTfQ>my߻>{j}CR"Dk5`Khk_}CD0&$iB AÅJM䑩ǏM]O:QfU=2_hXn>eFCc}YM}oQvW&qj̚CϜ}CpP*TsKRa-5)NC+$b6M#~ϲo.A+5V*6Oܳ'>0Ѩk4iPX)RF{d4MGA1':c_׫rsx'M v[t7qm\2.,8*i;8unyQvWQ;fu㪚hRd%j8TR/* {0=@ܸy zNn E&Vvr269{ϟ=57:M Ð{u6d7 @X/0yz=l2C> |.ifUH/>2}ӣٔ [O )I7KŴ'h_ND:X;8y"MG;U)v[.?ϝ}ȸFTwɲUM.Hbr16F$ E;tJ6h!Ĉ_uD ǭmFs~ѹG /]BDW bXi@[;E&XaBg!nJc cϽd@^d{hH!4M r\n44!j]n2@nIA_G 7vWA_nh(&umW;̩}ϞNkw&&ɩWJQհM\:ej+u˽pk]doKٌv i ` )]j1 ].-b]8:ulz@mPz^+U]W2Fh}vkIw+8, `!1gC/)eGarԵ#CzJ5M2F}PYBY rZty~f9ɖIH8;yW)QULg=yl3ivx#ɀ["T)Ն^)V,!H$9̙.t]V'X үp/ronA庭si|dcG'OnG7(MJ!]Gӳy)o; p{p6M!߈%@O%p6DK+9*TR#SC:2އ?zhb$KeG'ҤBrZYBf̮ⱡX^ i_CUga"#w M Dui;F+JfQGs\庪ڰi?sj{N;0mN,we2IٰݕbuTgbn}bfhV)rc7b5k^vWp .!@9bsGrw/b4lV>;Cϟwrvtw] hm^YL:7(%I͍dˉ4QDWKj;37^_R `O126Űo Xk4lwnlуǎN zw' !tYlU-Vl7:5)fFcUL0hbqf d .D7Me_@QdYkw3츪nBcG&>1هQá*m׬iR)^^.kj_"r24mn"?MJ%5?4 [][*C]ъ@_Tmpo^qr* е'gsfcY%*b2`jf-UK-ȧ M{)i~veNixʳ]l_?7wnT[=5}'d@AirZZ;9ʦlڰ݄giD+Rs a_v+80 4#o a_un;㞜y̾79M%jCMҚxvqT35 v+80r\'nU츪f9iS{G==s|24}kMJxP].VGi뀺ʥ)Cj׻su^M2_ߋ  ۯʅou{GTmXfSHS3ϝy؀)mB]Ԥ !UkP-,)ko,v9΋ ^"j=_],*|7&Nuh_ u#PM5nFs^zd)CP]ݟ +4]ZZ-,nZHώ5M:Jƾ~meåkŖ$Zg4&}v?Gy`4KDY,B߅fg+U뵕bqU@*Ð);}.S |'Qo|ς6ٴG#ϛJjmnTB\ȥM't#o"|/mW 7r]Wp `OS~w*vUn=qlǿ̳']ͥ 6_qRM)S`&fXyKtAW 5KJum_@ߢTnZjX?Ȁe[M~5)im\ꎮm΍̧fEo!2 VkZG }'K'?w]9wQ&j gyZ4P(5<j#Bh7?oUPN  r}}6or~hFTbOE3&rf;PIfř }Es6 MZ.__.Y)þ  T j3'g&F|-+5kPݨZR(j3Hvj4LJ)+ߐ:%BiX3u]0;œ|NͱbD7B.媕Bem#PK%pF6gPXZ վܝ CzIҊnC)R )eҸPX*Tɰu !f'&G.oit%hSo?mMABl-7?=_](m M WtWp@c_nyJm]LDqi ڍPJ˧5+=L2[{stmT;vW8050:v7*zʛŶ>^ХKtu e+[݆[}+}ŷJi96ڏ''&Ulݲ*iJbn9R ]F*ejs麪.-$[<.hy~u1{љm ;I AԵ?N ?sbC풫U̽} {qQiVI'3VG&P`KLJ@ 7`_p7+ .eL=c[߼<190>pxj# ԰B!&KZfJWۍs䀩kȾV7iolco,'q dߖ dٰ́7ym}ct ujnӧgG-͠I!H+BZL*5O7JAT-Pnf/Zmcf$/䛀g-? -k̤7jo7M 90=|`l &f:4)\LYDⱡ̾Ѽ亪(4?kR4\u暮eq] pHu )DJj)]moׯ gN >03#1`"XXu[&\RmP[n:{xz$75e&Wq1umXkk HZDa!A?ǹOcnqjg"Cbިگ^\֕O~pɣSO>49ph<'HRMT*m=ULD3,3+o"-$__][*֦اt]q2p@K LJϤ4ƅۅ_}SC<=7ؑy&j(r\VC "MR kRq\M1A'iwf͵Mkw+x/t@Ĵ/ﶸI !4i24"_ڍiC;89phj#ORNd1,vbR붔"ΊеىP.}Q 5v*\6\C#~9Wxvo҆XٸP'fG<>y7%-5ٰr^"2ڌ*L M}#nnM &Xt՝o*3ϡc(g<m_;z/ݹkRdL"rWkpV:0gDΰZۨ/W떣Ik  US?05I-'jOۮKR^*VM]h|UMG@l?G+}Oۭl4)4)t]6]K }k4>89቗^+S3mn1O-;B^k B%kߢGL0_䛠}޷;Κ`,={c~qori= j$fߤKq˲RN g9<4 ]ӐPqsq_jhl4\j$ZTHW􁩼{:(0` |iu&B(bxw0xPjYWpK^yǝyKSV[ $(ĀOo`QӎŪuiIm˞9<(*7 PJy@g㧔_/jfnJ13͌GrEET*v*{\iKڥS;o64 b_d؉H~O$//+=Q,OdY)[;- .VmmnDtkTngu>vW&,}޾m2[ޙLIQˋEUЎR'Cef!6e}CؖW>g[J7Wt)Z-ȑ0 C4?ot4R5 ;nvk&Gi*%O oRvB|\֍-Sc & o_@moDwDݻ˥Ս\8<54K.3UޖAzGUm3EOh j9s$c]70_o3E-{fFRj9kU*OwV &\\ۨ =2yRWp@ h:pRm 5) ƭBY4`Ou*;Do6luY `Nh{P3mMG-/DR+ THH\taKKҦB؝ami$soX6lM oݮ@&󼥋O p˾m9mh; ٴ䬮 C _8kTO9A(BG6? RQuqhn/ivW@TߗoEg$MJݾPt]埈DLI 6rK5!RdViX5 E񔤠bmPٖ$1GJuQ;ȚR!H0KwR)]Wzirܴy=ncmXMR2h7{p0SDU_f=~7{'_6KB P Y{&5iڕ^(R[h Vm+]ZX_\6oͲG>lcͮ-{p,2,W]Z(X뛅iA2Ս.̿} ؜ nh* 'DsRQ|)XP6hNI `Џv2d|7qgq ];{ݿ% R.7{>o}\߼ 1dlpsf`@/n ٴ%]#վ^ef2{ZD3> )jU_ƪK 7s,v}i_ƻᆿ}24mKٔ=4BP+ ߼ʊzq[%߉zo[zaB$@GDtAdUۥ_t)jsy`9J)f&E*TOsUk;wPSuNI ]D7V7+\muQvWa AG26'no ԙ(2.ͯ/+iCohJ.դHڭ_]hD)[Ƨ <{}n[[zaA  g 7|(qA)l,6\td3Ǿs+ 55Ɛ;DnV {P9~ giwE+ |C'\+؍t7uM zW? ?O?DNnu s{8ClV$]@ /?ͷ<|Jjme9۴KA;մOex_ןWD&N7myRV63e)DÍv .p?KqgbPYX f&Sה|+oȦ C49-UW~kՆcHTO{!HCe7-& )–_o: J)j +HHro^Ϥ `⁌27Vu) ffM3 f\/,*)M}7_woľoػoyrnf#7Yֻ'a΅kJ~k53)u_ճ7)]bC~_W36Sf;nY-v^uw+8 `=m[sC&*WJisgP*3)n9 ML3>)U/o򅍚I!i{3ɤuqB+ @<◺< niw}JbέlZzT22,/{lФq3elJ_x/wGV;@,̯gv4 x 㶻7 0+yNFZ u//51LKuϾ~i!6s,+3ʽym%"M!6nnX.Mmnio U\ DU]yg}Gl\ʸx{}PKJs;1s6oԬ[WrSӄOwyl0Ӱ_{˅J]z ۹׷j{9 ]A<{&ehW7nnh[[b)Ϝk6m>w+K)`W Ls(oя )6܍S׸{n  Ծ2e3yZe̍J!V DBɡ[?>~۴ +6q\u/^+807ܥN@޿ M{jaoj̹Q^[D)gci}ӿr&UozY p%ÁEi\0˪ITX(3psisT͵l[2< 33p.WvJrӗ/02{FoUL3{DW3ϴ-*ז6/*B򁌹Q?~B!6wdc3&P_?uXm=&v+807xA85TᆇiHa8,sisq/}k˘BD9h0h>-__,Tz/ZYN>Q2Qz.f_ivA. (kkW ߋ5)s酵} lHQ̹16o\Ͻq#Xkeԧc(}={9 `fY<=GMJMCӹaZ6m3&..|{Pɥƾ,gf!H8oޅ[k Jق#}l Z ަ38p%@0'ЭvMz4;21rTUե.ێ{cWJ\pͧG376]PXX/w)Ӵ5{z ƦviQW.,RʵԵ-J#B)83L=c_|''Dx\[X^DxA{ eYe.N5!ҦrT])V,ɤ[ >Zo߿3ŚzjwӦwJ z({0AC`q r}!PM LC֜>;y(Υo/;߻wnum9]-PTq ѿ7~9Z{N& fΘP7/|ⷾ|>ZrVDt+80b3m<3β.on̖kr~^K-i@DSDFHgX][ F>m_ן֥v/VWPZ.B&4:X nUР0 Gu`vWd{8/W)C̾vaŧ>đ3'gr{[7>_{h>ř17y_0!Ng0'q1&F+F T . Ӷ~峿c32)G7/-nԬRDuHD#h{2{ Q*vuMN,}.J)@}$KxL Σ@ a)Yt ]ihlWxr)80!δ +1d!:]+ MstL(`晢A`o*h$̑/PF0'Jڼ::= R`w礛x9AYѾNއ{5!Q N({}9zs S./r)x<|./蜄7D7_D)x4 { AogYRi_`uY5M1&Xc( G8!ˬKIT!S:jwq&dNqRp㴻ڮʥ\ a8k?g뷻#~8}(tIr&ύ>l,oߐ#~CqWk(OWq&e݇y0#>|vU3nv k(O!Df={z `G dﵻ&wdyckշ=&}L'9\B[= _E<iJ!uKJ?&>ŏ}#˥ZsN'8=&'ݕUn);Rakˇ}i| G scbqu`3Җig]>ƃY  fb>_Ocz`=j/=_wˌ Rk8)@)R [nA6j `S]kW./KbBRV-LiS[V !Sԙ1kwɷ;}[j&#͇+%jRr ٲ݁ls;o:pK ׫5f9,xW7n\-M-gq:?}bFJ*& S~gpuX{Smbg_3M: ߥ0R=~8나4JNuH|OM_~A_7䦣&#kCl)h ٷݖ:qw7R)G d@ B!N= ~) 1tDxKQ @w@ 

@ }#fH8Gݸ嗉{0>]'ӆs{ cqru_yb(ɹ|p]uLSt̆.'k0SC- ?j,9<=ɡGM8Y ( .B⃏,lqEx p׶f3va̦>rRk77.gSS'@.݇fG%a}}뾭/JgN۷7BGfK кi_!Da?vl>D`13NTq]Jv4!s'=R{`r?Uѕ]RR?@F~}g<1Qn9j}Ǐ|C{0}3%pf/>r`721ޟ{z4ިY^GjS۪۶VJ>+^ھ:M~1mjj]xow5V~ŗtSحOK -U88`)xbg(! F>W{b(?o !ȧ~k {8Bx$nc~wrn su̦R$ -kO?gQ{ `߸ڷ./ 91j6qƂj9zc83ϝ;kW?KȦ`aܨ6F?i`/Qϧ^15S(ϲor|c?zs_ԫ닅\*ߙoͷڰӧfm|ǦNb,*nvkn_Y(TV,M]5Mפ mG23JJ9.;lGH1a<:3'O;<=2#7V߼reR*b^Ff1Qi)ŚK鹴1K3ù3?=76KZ?&]Q{W,+JXoTjZH ANIOOC@fzdÇ&OΧ0fT[,TVKbP;m%"!KCH.5L&1mpX,*KjRw. ]Ȍ3сa)0 t|0 ` @0 ` @hi!IENDB`python-telegram-bot-21.1.1/tests/docs/000077500000000000000000000000001460724040100175655ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/docs/README.md000066400000000000000000000005371460724040100210510ustar00rootroot00000000000000# Documentation tests 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. python-telegram-bot-21.1.1/tests/docs/admonition_inserter.py000066400000000000000000000221261460724040100242160ustar00rootroot00000000000000#!/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]}" ) python-telegram-bot-21.1.1/tests/ext/000077500000000000000000000000001460724040100174355ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/ext/__init__.py000066400000000000000000000014661460724040100215550ustar00rootroot00000000000000#!/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/]. python-telegram-bot-21.1.1/tests/ext/_utils/000077500000000000000000000000001460724040100207345ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/ext/_utils/__init__.py000066400000000000000000000014661460724040100230540ustar00rootroot00000000000000#!/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/]. python-telegram-bot-21.1.1/tests/ext/_utils/test_stack.py000066400000000000000000000077671460724040100234730ustar00rootroot00000000000000#!/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 import logging import sys from pathlib import Path import pytest from telegram.ext._utils.stack import was_called_by def symlink_to(source: Path, target: Path) -> None: """Wrapper around Path.symlink_to that pytest-skips OS Errors. Useful e.g. for making tests not fail locally due to permission errors. """ try: source.symlink_to(target) except OSError as exc: pytest.skip(f"Skipping due to OS error while creating symlink: {exc!r}") class TestStack: def test_none_input(self): assert not was_called_by(None, None) def test_called_by_current_file(self): # Testing a call by a different file is somewhat hard but it's covered in # TestUpdater/Application.test_manual_init_warning frame = inspect.currentframe() file = Path(__file__) assert was_called_by(frame, file) def test_exception(self, monkeypatch, caplog): def resolve(self): raise RuntimeError("Can Not Resolve") with caplog.at_level(logging.DEBUG): monkeypatch.setattr(Path, "resolve", resolve) assert not was_called_by(inspect.currentframe(), None) assert len(caplog.records) == 1 assert caplog.records[0].name == "telegram.ext" assert caplog.records[0].levelno == logging.DEBUG assert caplog.records[0].getMessage().startswith("Failed to check") assert caplog.records[0].exc_info[0] is RuntimeError assert "Can Not Resolve" in str(caplog.records[0].exc_info[1]) def test_called_by_symlink_file(self, tmp_path): # Set up a call from a linked file in a temp directory, # then test it with its resolved path. # Here we expect `was_called_by` to recognize # "`tmp_path`/caller_link.py" as same as "`tmp_path`/caller.py". temp_file = tmp_path / "caller.py" caller_content = """ import inspect def caller_func(): return inspect.currentframe() """ with temp_file.open("w") as f: f.write(caller_content) symlink_file = tmp_path / "caller_link.py" symlink_to(symlink_file, temp_file) sys.path.append(tmp_path.as_posix()) from caller_link import caller_func frame = caller_func() assert was_called_by(frame, temp_file) def test_called_by_symlink_file_nested(self, tmp_path): # Same as test_called_by_symlink_file except # inner_func is nested inside outer_func to test # if `was_called_by` can resolve paths in recursion. temp_file1 = tmp_path / "inner.py" inner_content = """ import inspect def inner_func(): return inspect.currentframe() """ with temp_file1.open("w") as f: f.write(inner_content) temp_file2 = tmp_path / "outer.py" outer_content = """ from inner import inner_func def outer_func(): return inner_func() """ with temp_file2.open("w") as f: f.write(outer_content) symlink_file2 = tmp_path / "outer_link.py" symlink_to(symlink_file2, temp_file2) sys.path.append(tmp_path.as_posix()) from outer_link import outer_func frame = outer_func() assert was_called_by(frame, temp_file2) python-telegram-bot-21.1.1/tests/ext/_utils/test_trackingdict.py000066400000000000000000000122021460724040100250100ustar00rootroot00000000000000#!/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.ext._utils.trackingdict import TrackingDict from tests.auxil.slots import mro_slots @pytest.fixture() def td() -> TrackingDict: td = TrackingDict() td.update_no_track({1: 1}) return td @pytest.fixture() def data() -> dict: return {1: 1} class TestTrackingDict: def test_slot_behaviour(self, td): for attr in td.__slots__: assert getattr(td, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(td)) == len(set(mro_slots(td))), "duplicate slot" def test_representations(self, td, data): assert repr(td) == repr(data) assert str(td) == str(data) def test_len(self, td, data): assert len(td) == len(data) def test_boolean(self, td, data): assert bool(td) == bool(data) assert bool(TrackingDict()) == bool({}) def test_equality(self, td, data): assert td == data assert data == td assert td != TrackingDict() assert TrackingDict() != td td_2 = TrackingDict() td_2["foo"] = 7 assert td != td_2 assert td_2 != td assert td != 1 assert td != 1 assert td != 5 assert td != 5 def test_getitem(self, td): assert td[1] == 1 assert not td.pop_accessed_write_items() assert not td.pop_accessed_keys() def test_setitem(self, td): td[5] = 5 assert td[5] == 5 assert td.pop_accessed_write_items() == [(5, 5)] td[5] = 7 assert td[5] == 7 assert td.pop_accessed_keys() == {5} def test_delitem(self, td): assert not td.pop_accessed_keys() td[5] = 7 del td[1] assert 1 not in td assert td.pop_accessed_keys() == {1, 5} td[1] = 7 td[5] = 7 assert td.pop_accessed_keys() == {1, 5} del td[5] assert 5 not in td assert td.pop_accessed_write_items() == [(5, TrackingDict.DELETED)] def test_update_no_track(self, td): assert not td.pop_accessed_keys() td.update_no_track({2: 2, 3: 3}) assert td == {1: 1, 2: 2, 3: 3} assert not td.pop_accessed_keys() def test_pop(self, td): td.pop(1) assert 1 not in td assert td.pop_accessed_keys() == {1} td[1] = 7 td[5] = 8 assert 1 in td assert 5 in td assert td.pop_accessed_keys() == {1, 5} td.pop(5) assert 5 not in td assert td.pop_accessed_write_items() == [(5, TrackingDict.DELETED)] with pytest.raises(KeyError): td.pop(5) assert td.pop(5, 8) == 8 assert 5 not in td assert not td.pop_accessed_keys() assert td.pop(5, 8) == 8 assert 5 not in td assert not td.pop_accessed_write_items() def test_popitem(self, td): td.update_no_track({2: 2}) assert td.popitem() == (1, 1) assert 1 not in td assert td.pop_accessed_keys() == {1} assert td.popitem() == (2, 2) assert 2 not in td assert not td assert td.pop_accessed_write_items() == [(2, TrackingDict.DELETED)] with pytest.raises(KeyError): td.popitem() def test_clear(self, td): td.clear() assert td == {} assert td.pop_accessed_keys() == {1} td[5] = 7 assert 5 in td assert td.pop_accessed_keys() == {5} td.clear() assert td == {} assert td.pop_accessed_write_items() == [(5, TrackingDict.DELETED)] def test_set_default(self, td): assert td.setdefault(1, 2) == 1 assert td[1] == 1 assert not td.pop_accessed_keys() assert not td.pop_accessed_write_items() assert td.setdefault(2, 3) == 3 assert td[2] == 3 assert td.pop_accessed_keys() == {2} assert td.setdefault(3, 4) == 4 assert td[3] == 4 assert td.pop_accessed_write_items() == [(3, 4)] def test_iter(self, td, data): data.update({2: 2, 3: 3, 4: 4}) td.update_no_track({2: 2, 3: 3, 4: 4}) assert not td.pop_accessed_keys() assert list(iter(td)) == list(iter(data)) def test_mark_as_accessed(self, td): td[1] = 2 assert td.pop_accessed_keys() == {1} assert td.pop_accessed_keys() == set() td.mark_as_accessed(1) assert td.pop_accessed_keys() == {1} python-telegram-bot-21.1.1/tests/ext/test_application.py000066400000000000000000002557711460724040100233720ustar00rootroot00000000000000#!/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/]. """The integration of persistence into the application is tested in test_basepersistence. """ import asyncio import inspect import logging import os import platform import signal import sys import threading import time from collections import defaultdict from pathlib import Path from queue import Queue from random import randrange from threading import Thread from typing import Optional import pytest from telegram import Bot, Chat, Message, MessageEntity, User from telegram.error import TelegramError from telegram.ext import ( Application, ApplicationBuilder, ApplicationHandlerStop, BaseHandler, CallbackContext, CommandHandler, ContextTypes, Defaults, JobQueue, MessageHandler, PicklePersistence, SimpleUpdateProcessor, TypeHandler, Updater, filters, ) from telegram.warnings import PTBDeprecationWarning, PTBUserWarning from tests.auxil.asyncio_helpers import call_after from tests.auxil.build_messages import make_message_update from tests.auxil.files import PROJECT_ROOT_PATH from tests.auxil.networking import send_webhook_message from tests.auxil.pytest_classes import make_bot from tests.auxil.slots import mro_slots class CustomContext(CallbackContext): pass class TestApplication: """The integration of persistence into the application is tested in test_basepersistence. """ message_update = make_message_update(message="Text") received = None count = 0 @pytest.fixture(autouse=True, name="reset") def _reset_fixture(self): self.reset() def reset(self): self.received = None self.count = 0 async def error_handler_context(self, update, context): self.received = context.error.message async def error_handler_raise_error(self, update, context): raise Exception("Failing bigly") async def callback_increase_count(self, update, context): self.count += 1 def callback_set_count(self, count, sleep: Optional[float] = None): async def callback(update, context): if sleep: await asyncio.sleep(sleep) self.count = count return callback def callback_raise_error(self, error_message: str): async def callback(update, context): raise TelegramError(error_message) return callback async def callback_received(self, update, context): self.received = update.message async def callback_context(self, update, context): if ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(context.update_queue, Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.error, TelegramError) ): self.received = context.error.message async def test_slot_behaviour(self, one_time_bot): async with ApplicationBuilder().bot(one_time_bot).build() as app: for at in app.__slots__: attr = f"_Application{at}" if at.startswith("__") and not at.endswith("__") else at assert getattr(app, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(app)) == len(set(mro_slots(app))), "duplicate slot" def test_manual_init_warning(self, recwarn, updater): Application( bot=None, update_queue=None, job_queue=None, persistence=None, context_types=ContextTypes(), updater=updater, update_processor=False, post_init=None, post_shutdown=None, post_stop=None, ) assert len(recwarn) == 1 assert ( str(recwarn[-1].message) == "`Application` instances should be built via the `ApplicationBuilder`." ) assert recwarn[0].category is PTBUserWarning assert recwarn[0].filename == __file__, "stacklevel is incorrect!" @pytest.mark.filterwarnings("ignore: `Application` instances should") def test_init(self, one_time_bot): update_queue = asyncio.Queue() job_queue = JobQueue() persistence = PicklePersistence("file_path") context_types = ContextTypes() update_processor = SimpleUpdateProcessor(1) updater = Updater(bot=one_time_bot, update_queue=update_queue) async def post_init(application: Application) -> None: pass async def post_shutdown(application: Application) -> None: pass async def post_stop(application: Application) -> None: pass app = Application( bot=one_time_bot, update_queue=update_queue, job_queue=job_queue, persistence=persistence, context_types=context_types, updater=updater, update_processor=update_processor, post_init=post_init, post_shutdown=post_shutdown, post_stop=post_stop, ) assert app.bot is one_time_bot assert app.update_queue is update_queue assert app.job_queue is job_queue assert app.persistence is persistence assert app.context_types is context_types assert app.updater is updater assert app.update_queue is updater.update_queue assert app.bot is updater.bot assert app.update_processor is update_processor assert app.post_init is post_init assert app.post_shutdown is post_shutdown assert app.post_stop is post_stop # These should be done by the builder assert app.persistence.bot is None with pytest.raises(RuntimeError, match="No application was set"): app.job_queue.application assert isinstance(app.bot_data, dict) assert isinstance(app.chat_data[1], dict) assert isinstance(app.user_data[1], dict) async def test_repr(self, app): assert repr(app) == f"PytestApplication[bot={app.bot!r}]" def test_job_queue(self, one_time_bot, app, recwarn): expected_warning = ( "No `JobQueue` set up. To use `JobQueue`, you must install PTB via " '`pip install "python-telegram-bot[job-queue]"`.' ) assert app.job_queue is app._job_queue application = ApplicationBuilder().bot(one_time_bot).job_queue(None).build() assert application.job_queue is None 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_custom_context_init(self, one_time_bot): cc = ContextTypes( context=CustomContext, user_data=int, chat_data=float, bot_data=complex, ) application = ApplicationBuilder().bot(one_time_bot).context_types(cc).build() assert isinstance(application.user_data[1], int) assert isinstance(application.chat_data[1], float) assert isinstance(application.bot_data, complex) @pytest.mark.parametrize("updater", [True, False]) async def test_initialize(self, one_time_bot, monkeypatch, updater): """Initialization of persistence is tested test_basepersistence""" self.test_flag = set() async def after_initialize_bot(*args, **kwargs): self.test_flag.add("bot") async def after_initialize_update_processor(*args, **kwargs): self.test_flag.add("update_processor") async def after_initialize_updater(*args, **kwargs): self.test_flag.add("updater") update_processor = SimpleUpdateProcessor(1) monkeypatch.setattr(Bot, "initialize", call_after(Bot.initialize, after_initialize_bot)) monkeypatch.setattr( SimpleUpdateProcessor, "initialize", call_after(SimpleUpdateProcessor.initialize, after_initialize_update_processor), ) monkeypatch.setattr( Updater, "initialize", call_after(Updater.initialize, after_initialize_updater) ) if updater: app = ( ApplicationBuilder().bot(one_time_bot).concurrent_updates(update_processor).build() ) await app.initialize() assert self.test_flag == {"bot", "update_processor", "updater"} await app.shutdown() else: app = ( ApplicationBuilder() .bot(one_time_bot) .updater(None) .concurrent_updates(update_processor) .build() ) await app.initialize() assert self.test_flag == {"bot", "update_processor"} await app.shutdown() @pytest.mark.parametrize("updater", [True, False]) async def test_shutdown(self, one_time_bot, monkeypatch, updater): """Shutdown of persistence is tested in test_basepersistence""" self.test_flag = set() def after_bot_shutdown(*args, **kwargs): self.test_flag.add("bot") def after_shutdown_update_processor(*args, **kwargs): self.test_flag.add("update_processor") def after_updater_shutdown(*args, **kwargs): self.test_flag.add("updater") update_processor = SimpleUpdateProcessor(1) monkeypatch.setattr(Bot, "shutdown", call_after(Bot.shutdown, after_bot_shutdown)) monkeypatch.setattr( SimpleUpdateProcessor, "shutdown", call_after(SimpleUpdateProcessor.shutdown, after_shutdown_update_processor), ) monkeypatch.setattr( Updater, "shutdown", call_after(Updater.shutdown, after_updater_shutdown) ) if updater: async with ApplicationBuilder().bot(one_time_bot).concurrent_updates( update_processor ).build(): pass assert self.test_flag == {"bot", "update_processor", "updater"} else: async with ApplicationBuilder().bot(one_time_bot).updater(None).concurrent_updates( update_processor ).build(): pass assert self.test_flag == {"bot", "update_processor"} async def test_multiple_inits_and_shutdowns(self, app, monkeypatch): self.received = defaultdict(int) async def after_initialize(*args, **kargs): self.received["init"] += 1 async def after_shutdown(*args, **kwargs): self.received["shutdown"] += 1 monkeypatch.setattr( app.bot, "initialize", call_after(app.bot.initialize, after_initialize) ) monkeypatch.setattr(app.bot, "shutdown", call_after(app.bot.shutdown, after_shutdown)) await app.initialize() await app.initialize() await app.initialize() await app.shutdown() await app.shutdown() await app.shutdown() # 2 instead of 1 since `Updater.initialize` also calls bot.init/shutdown assert self.received["init"] == 2 assert self.received["shutdown"] == 2 async def test_multiple_init_cycles(self, app): # nothing really to assert - this should just not fail async with app: await app.bot.get_me() async with app: await app.bot.get_me() async def test_start_without_initialize(self, app): with pytest.raises(RuntimeError, match="not initialized"): await app.start() async def test_shutdown_while_running(self, app): async with app: await app.start() with pytest.raises(RuntimeError, match="still running"): await app.shutdown() await app.stop() async def test_start_not_running_after_failure(self, one_time_bot, monkeypatch): def start(_): raise Exception("Test Exception") monkeypatch.setattr(JobQueue, "start", start) app = ApplicationBuilder().bot(one_time_bot).job_queue(JobQueue()).build() async with app: with pytest.raises(Exception, match="Test Exception"): await app.start() assert app.running is False async def test_context_manager(self, monkeypatch, app): self.test_flag = set() async def after_initialize(*args, **kwargs): self.test_flag.add("initialize") async def after_shutdown(*args, **kwargs): self.test_flag.add("stop") monkeypatch.setattr( Application, "initialize", call_after(Application.initialize, after_initialize) ) monkeypatch.setattr( Application, "shutdown", call_after(Application.shutdown, after_shutdown) ) async with app: pass assert self.test_flag == {"initialize", "stop"} async def test_context_manager_exception_on_init(self, monkeypatch, app): async def after_initialize(*args, **kwargs): raise RuntimeError("initialize") async def after_shutdown(*args): self.test_flag = "stop" monkeypatch.setattr( Application, "initialize", call_after(Application.initialize, after_initialize) ) monkeypatch.setattr( Application, "shutdown", call_after(Application.shutdown, after_shutdown) ) with pytest.raises(RuntimeError, match="initialize"): async with app: pass assert self.test_flag == "stop" @pytest.mark.parametrize("data", ["chat_data", "user_data"]) def test_chat_user_data_read_only(self, app, data): read_only_data = getattr(app, data) writable_data = getattr(app, f"_{data}") writable_data[123] = 321 assert read_only_data == writable_data with pytest.raises(TypeError): read_only_data[111] = 123 def test_builder(self, app): builder_1 = app.builder() builder_2 = app.builder() assert isinstance(builder_1, ApplicationBuilder) assert isinstance(builder_2, ApplicationBuilder) assert builder_1 is not builder_2 # Make sure that setting a token doesn't raise an exception # i.e. check that the builders are "empty"/new builder_1.token(app.bot.token) builder_2.token(app.bot.token) @pytest.mark.parametrize("job_queue", [True, False]) @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") async def test_start_stop_processing_updates(self, one_time_bot, job_queue): # TODO: repeat a similar test for create_task, persistence processing and job queue if job_queue: app = ApplicationBuilder().bot(one_time_bot).build() else: app = ApplicationBuilder().bot(one_time_bot).job_queue(None).build() async def callback(u, c): self.received = u assert not app.running assert not app.updater.running if job_queue: assert not app.job_queue.scheduler.running else: assert app.job_queue is None app.add_handler(TypeHandler(object, callback)) await app.update_queue.put(1) await asyncio.sleep(0.05) assert not app.update_queue.empty() assert self.received is None async with app: await app.start() assert app.running tasks = asyncio.all_tasks() assert any(":update_fetcher" in task.get_name() for task in tasks) if job_queue: assert app.job_queue.scheduler.running else: assert app.job_queue is None # app.start() should not start the updater! assert not app.updater.running await asyncio.sleep(0.05) assert app.update_queue.empty() assert self.received == 1 try: # just in case start_polling times out await app.updater.start_polling() except TelegramError: pytest.xfail("start_polling timed out") else: await app.stop() assert not app.running # app.stop() should not stop the updater! assert app.updater.running if job_queue: assert not app.job_queue.scheduler.running else: assert app.job_queue is None await app.update_queue.put(2) await asyncio.sleep(0.05) assert not app.update_queue.empty() assert self.received != 2 assert self.received == 1 await app.updater.stop() async def test_error_start_stop_twice(self, app): async with app: await app.start() assert app.running with pytest.raises(RuntimeError, match="already running"): await app.start() await app.stop() assert not app.running with pytest.raises(RuntimeError, match="not running"): await app.stop() async def test_one_context_per_update(self, app): self.received = None async def one(update, context): self.received = context async def two(update, context): if update.message.text == "test": if context is not self.received: pytest.fail("Expected same context object, got different") elif context is self.received: pytest.fail("First handler was wrongly called") async with app: app.add_handler(MessageHandler(filters.Regex("test"), one), group=1) app.add_handler(MessageHandler(filters.ALL, two), group=2) u = make_message_update(message="test") await app.process_update(u) self.received = None u = make_message_update(message="something") await app.process_update(u) def test_add_handler_errors(self, app): handler = "not a handler" with pytest.raises(TypeError, match="handler is not an instance of"): app.add_handler(handler) handler = MessageHandler(filters.PHOTO, self.callback_set_count(1)) with pytest.raises(TypeError, match="group is not int"): app.add_handler(handler, "one") @pytest.mark.parametrize("group_empty", [True, False]) async def test_add_remove_handler(self, app, group_empty): handler = MessageHandler(filters.ALL, self.callback_increase_count) app.add_handler(handler) if not group_empty: app.add_handler(handler) async with app: await app.start() await app.update_queue.put(self.message_update) await asyncio.sleep(0.05) assert self.count == 1 app.remove_handler(handler) assert (0 in app.handlers) == (not group_empty) await app.update_queue.put(self.message_update) assert self.count == 1 await app.stop() async def test_add_remove_handler_non_default_group(self, app): handler = MessageHandler(filters.ALL, self.callback_increase_count) app.add_handler(handler, group=2) with pytest.raises(KeyError): app.remove_handler(handler) app.remove_handler(handler, group=2) async def test_handler_order_in_group(self, app): app.add_handler(MessageHandler(filters.PHOTO, self.callback_set_count(1))) app.add_handler(MessageHandler(filters.ALL, self.callback_set_count(2))) app.add_handler(MessageHandler(filters.TEXT, self.callback_set_count(3))) async with app: await app.start() await app.update_queue.put(self.message_update) await asyncio.sleep(0.05) assert self.count == 2 await app.stop() async def test_groups(self, app): app.add_handler(MessageHandler(filters.ALL, self.callback_increase_count)) app.add_handler(MessageHandler(filters.ALL, self.callback_increase_count), group=2) app.add_handler(MessageHandler(filters.ALL, self.callback_increase_count), group=-1) async with app: await app.start() await app.update_queue.put(self.message_update) await asyncio.sleep(0.05) assert self.count == 3 await app.stop() async def test_add_handlers(self, app): """Tests both add_handler & add_handlers together & confirms the correct insertion order""" msg_handler_set_count = MessageHandler(filters.TEXT, self.callback_set_count(1)) msg_handler_inc_count = MessageHandler(filters.PHOTO, self.callback_increase_count) app.add_handler(msg_handler_set_count, 1) app.add_handlers((msg_handler_inc_count, msg_handler_inc_count), 1) photo_update = make_message_update(message=Message(2, None, None, photo=(True,))) async with app: await app.start() # Putting updates in the queue calls the callback await app.update_queue.put(self.message_update) await app.update_queue.put(photo_update) await asyncio.sleep(0.05) # sleep is required otherwise there is random behaviour # Test if handler was added to correct group with correct order- assert self.count == 2 assert len(app.handlers[1]) == 3 assert app.handlers[1][0] is msg_handler_set_count # Now lets test add_handlers when `handlers` is a dict- voice_filter_handler_to_check = MessageHandler( filters.VOICE, self.callback_increase_count ) app.add_handlers( handlers={ 1: [ MessageHandler(filters.USER, self.callback_increase_count), voice_filter_handler_to_check, ], -1: [MessageHandler(filters.CAPTION, self.callback_set_count(2))], } ) user_update = make_message_update( message=Message(3, None, None, from_user=User(1, "s", True)) ) voice_update = make_message_update(message=Message(4, None, None, voice=True)) await app.update_queue.put(user_update) await app.update_queue.put(voice_update) await asyncio.sleep(0.05) assert self.count == 4 assert len(app.handlers[1]) == 5 assert app.handlers[1][-1] is voice_filter_handler_to_check await app.update_queue.put( make_message_update(message=Message(5, None, None, caption="cap")) ) await asyncio.sleep(0.05) assert self.count == 2 assert len(app.handlers[-1]) == 1 # Now lets test the errors which can be produced- with pytest.raises(ValueError, match="The `group` argument"): app.add_handlers({2: [msg_handler_set_count]}, group=0) with pytest.raises(ValueError, match="Handlers for group 3"): app.add_handlers({3: msg_handler_set_count}) with pytest.raises(ValueError, match="The `handlers` argument must be a sequence"): app.add_handlers({msg_handler_set_count}) await app.stop() async def test_check_update(self, app): class TestHandler(BaseHandler): def check_update(_, update: object): self.received = object() def handle_update( _, update, application, check_result, context, ): assert application is app assert check_result is not self.received async with app: app.add_handler(TestHandler("callback")) await app.start() await app.update_queue.put(object()) await asyncio.sleep(0.05) await app.stop() async def test_flow_stop(self, app, one_time_bot): passed = [] async def start1(b, u): passed.append("start1") raise ApplicationHandlerStop async def start2(b, u): passed.append("start2") async def start3(b, u): passed.append("start3") update = make_message_update( message=Message( 1, None, None, None, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ), ) await one_time_bot.initialize() update.message.set_bot(one_time_bot) async with app: # If ApplicationHandlerStop raised handlers in other groups should not be called. passed = [] app.add_handler(CommandHandler("start", start1), 1) app.add_handler(CommandHandler("start", start3), 1) app.add_handler(CommandHandler("start", start2), 2) await app.process_update(update) assert passed == ["start1"] async def test_flow_stop_by_error_handler(self, app): passed = [] exception = Exception("General exception") async def start1(u, c): passed.append("start1") raise exception async def start2(u, c): passed.append("start2") async def start3(u, c): passed.append("start3") async def error(u, c): passed.append("error") passed.append(c.error) raise ApplicationHandlerStop async with app: # If ApplicationHandlerStop raised handlers in other groups should not be called. passed = [] app.add_error_handler(error) app.add_handler(TypeHandler(object, start1), 1) app.add_handler(TypeHandler(object, start2), 1) app.add_handler(TypeHandler(object, start3), 2) await app.process_update(1) assert passed == ["start1", "error", exception] async def test_error_in_handler_part_1(self, app): app.add_handler( MessageHandler( filters.ALL, self.callback_raise_error(error_message=self.message_update.message.text), ) ) app.add_handler(MessageHandler(filters.ALL, self.callback_set_count(42)), group=1) app.add_error_handler(self.error_handler_context) async with app: await app.start() await app.update_queue.put(self.message_update) await asyncio.sleep(0.05) await app.stop() assert self.received == self.message_update.message.text # Higher groups should still be called assert self.count == 42 async def test_error_in_handler_part_2(self, app, one_time_bot): passed = [] err = Exception("General exception") async def start1(u, c): passed.append("start1") raise err async def start2(u, c): passed.append("start2") async def start3(u, c): passed.append("start3") async def error(u, c): passed.append("error") passed.append(c.error) update = make_message_update( message=Message( 1, None, None, None, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ), ) await one_time_bot.initialize() update.message.set_bot(one_time_bot) async with app: # If an unhandled exception was caught, no further handlers from the same group should # be called. Also, the error handler should be called and receive the exception passed = [] app.add_handler(CommandHandler("start", start1), 1) app.add_handler(CommandHandler("start", start2), 1) app.add_handler(CommandHandler("start", start3), 2) app.add_error_handler(error) await app.process_update(update) assert passed == ["start1", "error", err, "start3"] @pytest.mark.parametrize("block", [True, False]) async def test_error_handler(self, app, block): app.add_error_handler(self.error_handler_context) app.add_handler(TypeHandler(object, self.callback_raise_error("TestError"), block=block)) async with app: await app.start() await app.update_queue.put(1) await asyncio.sleep(0.05) assert self.received == "TestError" # Remove handler app.remove_error_handler(self.error_handler_context) self.reset() await app.update_queue.put(1) await asyncio.sleep(0.05) assert self.received is None await app.stop() def test_double_add_error_handler(self, app, caplog): app.add_error_handler(self.error_handler_context) with caplog.at_level(logging.DEBUG): app.add_error_handler(self.error_handler_context) assert len(caplog.records) == 1 assert caplog.records[-1].name == "telegram.ext.Application" assert caplog.records[-1].getMessage().startswith("The callback is already registered") async def test_error_handler_that_raises_errors(self, app, caplog): """Make sure that errors raised in error handlers don't break the main loop of the application """ handler_raise_error = TypeHandler( int, self.callback_raise_error(error_message="TestError") ) handler_increase_count = TypeHandler(str, self.callback_increase_count) app.add_error_handler(self.error_handler_raise_error) app.add_handler(handler_raise_error) app.add_handler(handler_increase_count) with caplog.at_level(logging.ERROR): async with app: await app.start() await app.update_queue.put(1) await asyncio.sleep(0.05) assert self.count == 0 assert self.received is None assert len(caplog.records) > 0 assert any( "uncaught error was raised while handling the error with an error_handler" in record.getMessage() and record.name == "telegram.ext.Application" for record in caplog.records ) await app.update_queue.put("1") self.received = None caplog.clear() await asyncio.sleep(0.05) assert self.count == 1 assert self.received is None assert not caplog.records await app.stop() async def test_custom_context_error_handler(self, one_time_bot): async def error_handler(_, context): self.received = ( type(context), type(context.user_data), type(context.chat_data), type(context.bot_data), ) application = ( ApplicationBuilder() .bot(one_time_bot) .context_types( ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ) ) .build() ) application.add_error_handler(error_handler) application.add_handler( MessageHandler(filters.ALL, self.callback_raise_error("TestError")) ) async with application: await application.process_update(self.message_update) await asyncio.sleep(0.05) assert self.received == (CustomContext, float, complex, int) async def test_custom_context_handler_callback(self, one_time_bot): async def callback(_, context): self.received = ( type(context), type(context.user_data), type(context.chat_data), type(context.bot_data), ) application = ( ApplicationBuilder() .bot(one_time_bot) .context_types( ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ) ) .build() ) application.add_handler(MessageHandler(filters.ALL, callback)) async with application: await application.process_update(self.message_update) await asyncio.sleep(0.05) assert self.received == (CustomContext, float, complex, int) @pytest.mark.parametrize( ("check", "expected"), [(True, True), (None, False), (False, False), ({}, True), ("", True), ("check", True)], ) async def test_check_update_handling(self, app, check, expected): class MyHandler(BaseHandler): def check_update(self, update: object): return check async def handle_update( _, update, application, check_result, context, ): await super().handle_update( update=update, application=application, check_result=check_result, context=context, ) self.received = check_result async with app: app.add_handler(MyHandler(self.callback_increase_count)) await app.process_update(1) assert self.count == (1 if expected else 0) if expected: assert self.received == check else: assert self.received is None async def test_non_blocking_handler(self, app): event = asyncio.Event() async def callback(update, context): await event.wait() self.count = 42 app.add_handler(TypeHandler(object, callback, block=False)) app.add_handler(TypeHandler(object, self.callback_increase_count), group=1) async with app: await app.start() await app.update_queue.put(1) task = asyncio.create_task(app.stop()) await asyncio.sleep(0.05) tasks = asyncio.all_tasks() assert any(":process_update_non_blocking" in t.get_name() for t in tasks) assert self.count == 1 # Make sure that app stops only once all non blocking callbacks are done assert not task.done() event.set() await asyncio.sleep(0.05) assert self.count == 42 assert task.done() async def test_non_blocking_handler_applicationhandlerstop(self, app, recwarn): async def callback(update, context): raise ApplicationHandlerStop app.add_handler(TypeHandler(object, callback, block=False)) async with app: await app.start() await app.update_queue.put(1) await asyncio.sleep(0.05) await app.stop() assert len(recwarn) == 1 assert recwarn[0].category is PTBUserWarning assert ( str(recwarn[0].message) == "ApplicationHandlerStop is not supported with handlers running non-blocking." ) assert ( Path(recwarn[0].filename) == PROJECT_ROOT_PATH / "telegram" / "ext" / "_application.py" ), "incorrect stacklevel!" async def test_non_blocking_no_error_handler(self, app, caplog): app.add_handler(TypeHandler(object, self.callback_raise_error("Test error"), block=False)) with caplog.at_level(logging.ERROR): async with app: await app.start() await app.update_queue.put(1) await asyncio.sleep(0.05) assert len(caplog.records) == 1 assert ( caplog.records[-1].getMessage().startswith("No error handlers are registered") ) assert caplog.records[-1].name == "telegram.ext.Application" await app.stop() @pytest.mark.parametrize("handler_block", [True, False]) async def test_non_blocking_error_handler(self, app, handler_block): event = asyncio.Event() async def async_error_handler(update, context): await event.wait() self.received = "done" async def normal_error_handler(update, context): self.count = 42 app.add_error_handler(async_error_handler, block=False) app.add_error_handler(normal_error_handler) app.add_handler(TypeHandler(object, self.callback_raise_error("err"), block=handler_block)) async with app: await app.start() await app.update_queue.put(self.message_update) task = asyncio.create_task(app.stop()) await asyncio.sleep(0.05) tasks = asyncio.all_tasks() assert any(":process_error:non_blocking" in t.get_name() for t in tasks) assert self.count == 42 assert self.received is None event.set() await asyncio.sleep(0.05) assert self.received == "done" assert task.done() @pytest.mark.parametrize("handler_block", [True, False]) async def test_non_blocking_error_handler_applicationhandlerstop( self, app, recwarn, handler_block ): async def callback(update, context): raise RuntimeError async def error_handler(update, context): raise ApplicationHandlerStop app.add_handler(TypeHandler(object, callback, block=handler_block)) app.add_error_handler(error_handler, block=False) async with app: await app.start() await app.update_queue.put(1) await asyncio.sleep(0.05) await app.stop() assert len(recwarn) == 1 assert recwarn[0].category is PTBUserWarning assert ( str(recwarn[0].message) == "ApplicationHandlerStop is not supported with handlers running non-blocking." ) assert ( Path(recwarn[0].filename) == PROJECT_ROOT_PATH / "telegram" / "ext" / "_application.py" ), "incorrect stacklevel!" @pytest.mark.parametrize(("block", "expected_output"), [(False, 0), (True, 5)]) async def test_default_block_error_handler(self, bot_info, block, expected_output): async def error_handler(*args, **kwargs): await asyncio.sleep(0.1) self.count = 5 bot = make_bot(bot_info, defaults=Defaults(block=block)) app = Application.builder().bot(bot).build() async with app: app.add_handler(TypeHandler(object, self.callback_raise_error("error"))) app.add_error_handler(error_handler) await app.process_update(1) await asyncio.sleep(0.05) assert self.count == expected_output await asyncio.sleep(0.1) assert self.count == 5 @pytest.mark.parametrize(("block", "expected_output"), [(False, 0), (True, 5)]) async def test_default_block_handler(self, bot_info, block, expected_output): bot = make_bot(bot_info, defaults=Defaults(block=block)) app = Application.builder().bot(bot).build() async with app: app.add_handler(TypeHandler(object, self.callback_set_count(5, sleep=0.1))) await app.process_update(1) await asyncio.sleep(0.05) assert self.count == expected_output await asyncio.sleep(0.15) assert self.count == 5 @pytest.mark.parametrize("handler_block", [True, False]) @pytest.mark.parametrize("error_handler_block", [True, False]) async def test_nonblocking_handler_raises_and_non_blocking_error_handler_raises( self, app, caplog, handler_block, error_handler_block ): handler = TypeHandler(object, self.callback_raise_error("error"), block=handler_block) app.add_handler(handler) app.add_error_handler(self.error_handler_raise_error, block=error_handler_block) async with app: await app.start() with caplog.at_level(logging.ERROR): await app.update_queue.put(1) await asyncio.sleep(0.05) assert len(caplog.records) == 1 assert caplog.records[-1].name == "telegram.ext.Application" assert ( caplog.records[-1] .getMessage() .startswith("An error was raised and an uncaught") ) # Make sure that the main loop still runs app.remove_handler(handler) app.add_handler(MessageHandler(filters.ALL, self.callback_increase_count, block=True)) await app.update_queue.put(self.message_update) await asyncio.sleep(0.05) assert self.count == 1 await app.stop() @pytest.mark.parametrize( "message", [ Message(message_id=1, chat=Chat(id=2, type=None), migrate_from_chat_id=1, date=None), Message(message_id=1, chat=Chat(id=1, type=None), migrate_to_chat_id=2, date=None), Message(message_id=1, chat=Chat(id=1, type=None), date=None), None, ], ) @pytest.mark.parametrize("old_chat_id", [None, 1, "1"]) @pytest.mark.parametrize("new_chat_id", [None, 2, "1"]) def test_migrate_chat_data(self, app, message: "Message", old_chat_id: int, new_chat_id: int): def call(match: str): with pytest.raises(ValueError, match=match): app.migrate_chat_data( message=message, old_chat_id=old_chat_id, new_chat_id=new_chat_id ) if message and (old_chat_id or new_chat_id): call(r"^Message and chat_id pair are mutually exclusive$") return if not any((message, old_chat_id, new_chat_id)): call(r"^chat_id pair or message must be passed$") return if message: if message.migrate_from_chat_id is None and message.migrate_to_chat_id is None: call(r"^Invalid message instance") return effective_old_chat_id = message.migrate_from_chat_id or message.chat.id effective_new_chat_id = message.migrate_to_chat_id or message.chat.id elif not (isinstance(old_chat_id, int) and isinstance(new_chat_id, int)): call(r"^old_chat_id and new_chat_id must be integers$") return else: effective_old_chat_id = old_chat_id effective_new_chat_id = new_chat_id app.chat_data[effective_old_chat_id]["key"] = "test" app.migrate_chat_data(message=message, old_chat_id=old_chat_id, new_chat_id=new_chat_id) assert effective_old_chat_id not in app.chat_data assert app.chat_data[effective_new_chat_id]["key"] == "test" @pytest.mark.parametrize( ("c_id", "expected"), [(321, {222: "remove_me"}), (111, {321: {"not_empty": "no"}, 222: "remove_me"})], ids=["test chat_id removal", "test no key in data (no error)"], ) def test_drop_chat_data(self, app, c_id, expected): app._chat_data.update({321: {"not_empty": "no"}, 222: "remove_me"}) app.drop_chat_data(c_id) assert app.chat_data == expected @pytest.mark.parametrize( ("u_id", "expected"), [(321, {222: "remove_me"}), (111, {321: {"not_empty": "no"}, 222: "remove_me"})], ids=["test user_id removal", "test no key in data (no error)"], ) def test_drop_user_data(self, app, u_id, expected): app._user_data.update({321: {"not_empty": "no"}, 222: "remove_me"}) app.drop_user_data(u_id) assert app.user_data == expected async def test_create_task_basic(self, app): async def callback(): await asyncio.sleep(0.05) self.count = 42 return 43 task = app.create_task(callback(), name="test_task") assert task.get_name() == "test_task" await asyncio.sleep(0.01) assert not task.done() out = await task assert task.done() assert self.count == 42 assert out == 43 @pytest.mark.parametrize("running", [True, False]) async def test_create_task_awaiting_warning(self, app, running, recwarn): async def callback(): await asyncio.sleep(0.1) return 43 async with app: if running: await app.start() task = app.create_task(callback()) if running: assert len(recwarn) == 0 assert not task.done() await app.stop() assert task.done() assert task.result() == 43 else: assert len(recwarn) == 1 assert recwarn[0].category is PTBUserWarning assert "won't be automatically awaited" in str(recwarn[0].message) assert recwarn[0].filename == __file__, "wrong stacklevel!" assert not task.done() await task @pytest.mark.parametrize("update", [None, object()]) async def test_create_task_error_handling(self, app, update): exception = RuntimeError("TestError") async def callback(): raise exception async def error(update_arg, context): self.received = update_arg, context.error app.add_error_handler(error) if update: task = app.create_task(callback(), update=update) else: task = app.create_task(callback()) with pytest.raises(RuntimeError, match="TestError"): await task assert task.exception() is exception assert isinstance(self.received, tuple) assert self.received[0] is update assert self.received[1] is exception async def test_create_task_cancel_task(self, app): async def callback(): await asyncio.sleep(5) async def error(update_arg, context): self.received = update_arg, context.error app.add_error_handler(error) async with app: await app.start() task = app.create_task(callback()) await asyncio.sleep(0.05) task.cancel() with pytest.raises(asyncio.CancelledError): await task with pytest.raises(asyncio.CancelledError): assert task.exception() # Error handlers should not be called if task was cancelled assert self.received is None # make sure that the cancelled task doesn't block the stopping of the app await app.stop() async def test_await_create_task_tasks_on_stop(self, app): event_1 = asyncio.Event() event_2 = asyncio.Event() async def callback_1(): await event_1.wait() async def callback_2(): await event_2.wait() async with app: await app.start() task_1 = app.create_task(callback_1()) task_2 = app.create_task(callback_2()) event_2.set() await task_2 assert not task_1.done() stop_task = asyncio.create_task(app.stop()) assert not stop_task.done() await asyncio.sleep(0.1) assert not stop_task.done() event_1.set() await asyncio.sleep(0.05) assert stop_task.done() async def test_create_task_awaiting_future(self, app): async def callback(): await asyncio.sleep(0.01) return 42 # `asyncio.gather` returns an `asyncio.Future` and not an # `asyncio.Task` out = await app.create_task(asyncio.gather(callback())) assert out == [42] @pytest.mark.skipif(sys.version_info >= (3, 12), reason="generator coroutines are deprecated") async def test_create_task_awaiting_generator(self, app, recwarn): event = asyncio.Event() def gen(): yield event.set() await app.create_task(gen()) assert event.is_set() assert len(recwarn) == 2 # 1st warning is: tasks not being awaited when app isn't running assert recwarn[1].category is PTBDeprecationWarning assert "Generator-based coroutines are deprecated" in str(recwarn[1].message) async def test_no_update_processor(self, app): queue = asyncio.Queue() event_1 = asyncio.Event() event_2 = asyncio.Event() await queue.put(event_1) await queue.put(event_2) async def callback(u, c): await asyncio.sleep(0.1) event = await queue.get() event.set() app.add_handler(TypeHandler(object, callback)) async with app: await app.start() await app.update_queue.put(1) await app.update_queue.put(2) assert not event_1.is_set() assert not event_2.is_set() await asyncio.sleep(0.15) assert event_1.is_set() assert not event_2.is_set() await asyncio.sleep(0.1) assert event_1.is_set() assert event_2.is_set() await app.stop() @pytest.mark.parametrize("update_processor", [15, 50, 100]) async def test_update_processor(self, one_time_bot, update_processor): # We don't test with `True` since the large number of parallel coroutines quickly leads # to test instabilities app = Application.builder().bot(one_time_bot).concurrent_updates(update_processor).build() events = { i: asyncio.Event() for i in range(app.update_processor.max_concurrent_updates + 10) } queue = asyncio.Queue() for event in events.values(): await queue.put(event) async def callback(u, c): await asyncio.sleep(0.5) (await queue.get()).set() app.add_handler(TypeHandler(object, callback)) async with app: await app.start() for i in range(app.update_processor.max_concurrent_updates + 10): await app.update_queue.put(i) for i in range(app.update_processor.max_concurrent_updates + 10): assert not events[i].is_set() await asyncio.sleep(0.9) tasks = asyncio.all_tasks() assert any(":process_concurrent_update" in task.get_name() for task in tasks) for i in range(app.update_processor.max_concurrent_updates): assert events[i].is_set() for i in range( app.update_processor.max_concurrent_updates, app.update_processor.max_concurrent_updates + 10, ): assert not events[i].is_set() await asyncio.sleep(0.5) for i in range(app.update_processor.max_concurrent_updates + 10): assert events[i].is_set() await app.stop() async def test_update_processor_done_on_shutdown(self, one_time_bot): app = Application.builder().bot(one_time_bot).concurrent_updates(True).build() event = asyncio.Event() async def callback(update, context): await event.wait() app.add_handler(TypeHandler(object, callback)) async with app: await app.start() await app.update_queue.put(1) stop_task = asyncio.create_task(app.stop()) await asyncio.sleep(0.1) assert not stop_task.done() event.set() await asyncio.sleep(0.05) assert stop_task.done() @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) def test_run_polling_basic(self, app, monkeypatch, caplog): exception_event = threading.Event() exception_testing_done = threading.Event() update_event = threading.Event() exception = TelegramError("This is a test error") assertions = {} async def get_updates(*args, **kwargs): if exception_event.is_set(): raise exception # This makes sure that other coroutines have a chance of running as well if exception_testing_done.is_set() and app.updater.running: # the longer sleep makes sure that we can exit also while get_updates is running await asyncio.sleep(20) else: await asyncio.sleep(0.01) update_event.set() return [self.message_update] def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") # Check that everything's running assertions["app_running"] = app.running assertions["updater_running"] = app.updater.running assertions["job_queue_running"] = app.job_queue.scheduler.running # Check that we're getting updates update_event.wait() time.sleep(0.05) assertions["getting_updates"] = self.count == 42 # Check that errors are properly handled during polling exception_event.set() time.sleep(0.05) assertions["exception_handling"] = self.received == exception.message exception_testing_done.set() # So that the get_updates call on shutdown doesn't fail exception_event.clear() time.sleep(1) os.kill(os.getpid(), signal.SIGINT) time.sleep(0.1) # # Assert that everything has stopped running assertions["app_not_running"] = not app.running assertions["updater_not_running"] = not app.updater.running assertions["job_queue_not_running"] = not app.job_queue.scheduler.running monkeypatch.setattr(app.bot, "get_updates", get_updates) app.add_error_handler(self.error_handler_context) app.add_handler(TypeHandler(object, self.callback_set_count(42))) thread = Thread(target=thread_target) thread.start() with caplog.at_level(logging.DEBUG): app.run_polling(drop_pending_updates=True, close_loop=False) thread.join() assert len(assertions) == 8 for key, value in assertions.items(): assert value, f"assertion '{key}' failed!" found_log = False for record in caplog.records: if "received stop signal" in record.getMessage() and record.levelno == logging.DEBUG: found_log = True assert found_log @pytest.mark.parametrize( "timeout_name", ["read_timeout", "connect_timeout", "write_timeout", "pool_timeout", "poll_interval"], ) @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) def test_run_polling_timeout_deprecation_warnings( self, timeout_name, monkeypatch, recwarn, app ): async def get_updates(*args, **kwargs): # This makes sure that other coroutines have a chance of running as well await asyncio.sleep(0) return [] def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") time.sleep(0.05) os.kill(os.getpid(), signal.SIGINT) monkeypatch.setattr(app.bot, "get_updates", get_updates) thread = Thread(target=thread_target) thread.start() kwargs = {timeout_name: 42} app.run_polling(drop_pending_updates=True, close_loop=False, **kwargs) thread.join() if timeout_name == "poll_interval": assert len(recwarn) == 0 return assert len(recwarn) == 1 assert "Setting timeouts via `Application.run_polling` is deprecated." in str( recwarn[0].message ) assert recwarn[0].category is PTBDeprecationWarning assert recwarn[0].filename == __file__, "wrong stacklevel" @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) def test_run_polling_post_init(self, one_time_bot, monkeypatch): events = [] async def get_updates(*args, **kwargs): # This makes sure that other coroutines have a chance of running as well await asyncio.sleep(0) return [] def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") os.kill(os.getpid(), signal.SIGINT) async def post_init(app: Application) -> None: events.append("post_init") app = Application.builder().bot(one_time_bot).post_init(post_init).build() app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr( app, "initialize", call_after(app.initialize, lambda _: events.append("init")) ) monkeypatch.setattr( app.updater, "start_polling", call_after(app.updater.start_polling, lambda _: events.append("start_polling")), ) thread = Thread(target=thread_target) thread.start() app.run_polling(drop_pending_updates=True, close_loop=False) thread.join() assert events == ["init", "post_init", "start_polling"], "Wrong order of events detected!" @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) def test_run_polling_post_shutdown(self, one_time_bot, monkeypatch): events = [] async def get_updates(*args, **kwargs): # This makes sure that other coroutines have a chance of running as well await asyncio.sleep(0) return [] def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") os.kill(os.getpid(), signal.SIGINT) async def post_shutdown(app: Application) -> None: events.append("post_shutdown") app = Application.builder().bot(one_time_bot).post_shutdown(post_shutdown).build() app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr( app, "shutdown", call_after(app.shutdown, lambda _: events.append("shutdown")) ) monkeypatch.setattr( app.updater, "shutdown", call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")), ) thread = Thread(target=thread_target) thread.start() app.run_polling(drop_pending_updates=True, close_loop=False) thread.join() assert events == [ "updater.shutdown", "shutdown", "post_shutdown", ], "Wrong order of events detected!" @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) def test_run_polling_post_stop(self, bot, monkeypatch): events = [] async def get_updates(*args, **kwargs): # This makes sure that other coroutines have a chance of running as well await asyncio.sleep(0) return [] def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") os.kill(os.getpid(), signal.SIGINT) async def post_stop(app: Application) -> None: events.append("post_stop") app = Application.builder().token(bot.token).post_stop(post_stop).build() app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr(app, "stop", call_after(app.stop, lambda _: events.append("stop"))) monkeypatch.setattr( app.updater, "stop", call_after(app.updater.stop, lambda _: events.append("updater.stop")), ) monkeypatch.setattr( app.updater, "shutdown", call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")), ) thread = Thread(target=thread_target) thread.start() app.run_polling(drop_pending_updates=True, close_loop=False) thread.join() assert events == [ "updater.stop", "stop", "post_stop", "updater.shutdown", ], "Wrong order of events detected!" @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) def test_run_polling_parameters_passing(self, app, monkeypatch): # First check that the default values match and that we have all arguments there updater_signature = inspect.signature(app.updater.start_polling) app_signature = inspect.signature(app.run_polling) for name, param in updater_signature.parameters.items(): if name == "error_callback": assert name not in app_signature.parameters continue assert name in app_signature.parameters assert param.kind == app_signature.parameters[name].kind assert param.default == app_signature.parameters[name].default # Check that we pass them correctly async def start_polling(_, **kwargs): self.received = kwargs return True async def stop(_, **kwargs): return True def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") time.sleep(0.1) os.kill(os.getpid(), signal.SIGINT) monkeypatch.setattr(Updater, "start_polling", start_polling) monkeypatch.setattr(Updater, "stop", stop) thread = Thread(target=thread_target) thread.start() app.run_polling(close_loop=False) thread.join() assert set(self.received.keys()) == set(updater_signature.parameters.keys()) for name, param in updater_signature.parameters.items(): if name == "error_callback": assert self.received[name] is not None else: assert self.received[name] == param.default expected = { name: name for name in updater_signature.parameters if name != "error_callback" } thread = Thread(target=thread_target) thread.start() app.run_polling(close_loop=False, **expected) thread.join() assert set(self.received.keys()) == set(updater_signature.parameters.keys()) assert self.received.pop("error_callback", None) assert self.received == expected @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) def test_run_webhook_basic(self, app, monkeypatch, caplog): assertions = {} async def delete_webhook(*args, **kwargs): return True async def set_webhook(*args, **kwargs): return True def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") # Check that everything's running assertions["app_running"] = app.running assertions["updater_running"] = app.updater.running assertions["job_queue_running"] = app.job_queue.scheduler.running # Check that we're getting updates loop = asyncio.new_event_loop() loop.run_until_complete( send_webhook_message(ip, port, self.message_update.to_json(), "TOKEN") ) loop.close() time.sleep(0.05) assertions["getting_updates"] = self.count == 42 os.kill(os.getpid(), signal.SIGINT) time.sleep(0.1) # # Assert that everything has stopped running assertions["app_not_running"] = not app.running assertions["updater_not_running"] = not app.updater.running assertions["job_queue_not_running"] = not app.job_queue.scheduler.running monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) app.add_handler(TypeHandler(object, self.callback_set_count(42))) thread = Thread(target=thread_target) thread.start() ip = "127.0.0.1" port = randrange(1024, 49152) with caplog.at_level(logging.DEBUG): app.run_webhook( ip_address=ip, port=port, url_path="TOKEN", drop_pending_updates=True, close_loop=False, ) thread.join() assert len(assertions) == 7 for key, value in assertions.items(): assert value, f"assertion '{key}' failed!" found_log = False for record in caplog.records: if "received stop signal" in record.getMessage() and record.levelno == logging.DEBUG: found_log = True assert found_log @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) def test_run_webhook_post_init(self, one_time_bot, monkeypatch): events = [] async def delete_webhook(*args, **kwargs): return True async def set_webhook(*args, **kwargs): return True async def get_updates(*args, **kwargs): # This makes sure that other coroutines have a chance of running as well await asyncio.sleep(0) return [] def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") os.kill(os.getpid(), signal.SIGINT) async def post_init(app: Application) -> None: events.append("post_init") app = Application.builder().bot(one_time_bot).post_init(post_init).build() app.bot._unfreeze() monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) monkeypatch.setattr( app, "initialize", call_after(app.initialize, lambda _: events.append("init")) ) monkeypatch.setattr( app.updater, "start_webhook", call_after(app.updater.start_webhook, lambda _: events.append("start_webhook")), ) thread = Thread(target=thread_target) thread.start() ip = "127.0.0.1" port = randrange(1024, 49152) app.run_webhook( ip_address=ip, port=port, url_path="TOKEN", drop_pending_updates=True, close_loop=False, ) thread.join() assert events == ["init", "post_init", "start_webhook"], "Wrong order of events detected!" @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) def test_run_webhook_post_shutdown(self, one_time_bot, monkeypatch): events = [] async def delete_webhook(*args, **kwargs): return True async def set_webhook(*args, **kwargs): return True async def get_updates(*args, **kwargs): # This makes sure that other coroutines have a chance of running as well await asyncio.sleep(0) return [] def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") os.kill(os.getpid(), signal.SIGINT) async def post_shutdown(app: Application) -> None: events.append("post_shutdown") app = Application.builder().bot(one_time_bot).post_shutdown(post_shutdown).build() app.bot._unfreeze() monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) monkeypatch.setattr( app, "shutdown", call_after(app.shutdown, lambda _: events.append("shutdown")) ) monkeypatch.setattr( app.updater, "shutdown", call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")), ) thread = Thread(target=thread_target) thread.start() ip = "127.0.0.1" port = randrange(1024, 49152) app.run_webhook( ip_address=ip, port=port, url_path="TOKEN", drop_pending_updates=True, close_loop=False, ) thread.join() assert events == [ "updater.shutdown", "shutdown", "post_shutdown", ], "Wrong order of events detected!" @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) def test_run_webhook_post_stop(self, bot, monkeypatch): events = [] async def delete_webhook(*args, **kwargs): return True async def set_webhook(*args, **kwargs): return True async def get_updates(*args, **kwargs): # This makes sure that other coroutines have a chance of running as well await asyncio.sleep(0) return [] def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") os.kill(os.getpid(), signal.SIGINT) async def post_stop(app: Application) -> None: events.append("post_stop") app = Application.builder().token(bot.token).post_stop(post_stop).build() app.bot._unfreeze() monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) monkeypatch.setattr(app, "stop", call_after(app.stop, lambda _: events.append("stop"))) monkeypatch.setattr( app.updater, "stop", call_after(app.updater.stop, lambda _: events.append("updater.stop")), ) monkeypatch.setattr( app.updater, "shutdown", call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")), ) thread = Thread(target=thread_target) thread.start() ip = "127.0.0.1" port = randrange(1024, 49152) app.run_webhook( ip_address=ip, port=port, url_path="TOKEN", drop_pending_updates=True, close_loop=False, ) thread.join() assert events == [ "updater.stop", "stop", "post_stop", "updater.shutdown", ], "Wrong order of events detected!" @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) def test_run_webhook_parameters_passing(self, one_time_bot, monkeypatch): # Check that we pass them correctly async def start_webhook(_, **kwargs): self.received = kwargs return True async def stop(_, **kwargs): return True # First check that the default values match and that we have all arguments there updater_signature = inspect.signature(Updater.start_webhook) monkeypatch.setattr(Updater, "start_webhook", start_webhook) monkeypatch.setattr(Updater, "stop", stop) app = ApplicationBuilder().bot(one_time_bot).build() app_signature = inspect.signature(app.run_webhook) for name, param in updater_signature.parameters.items(): if name == "self": continue assert name in app_signature.parameters assert param.kind == app_signature.parameters[name].kind assert param.default == app_signature.parameters[name].default def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") time.sleep(0.1) os.kill(os.getpid(), signal.SIGINT) thread = Thread(target=thread_target) thread.start() app.run_webhook(close_loop=False) thread.join() assert set(self.received.keys()) == set(updater_signature.parameters.keys()) - {"self"} for name, param in updater_signature.parameters.items(): if name == "self": continue assert self.received[name] == param.default expected = {name: name for name in updater_signature.parameters if name != "self"} thread = Thread(target=thread_target) thread.start() app.run_webhook(close_loop=False, **expected) thread.join() assert set(self.received.keys()) == set(expected.keys()) assert self.received == expected @pytest.mark.skipif( platform.system() == "Windows", reason="Can't send signals without stopping whole process on windows", ) async def test_cancellation_error_does_not_stop_polling( self, one_time_bot, monkeypatch, caplog ): """ Ensures that hitting CTRL+C while polling *without* run_polling doesn't kill the update_fetcher loop such that a shutdown is still possible. This test is far from perfect, but it's the closest we can come with sane effort. """ async def get_updates(*args, **kwargs): await asyncio.sleep(0) return [None] monkeypatch.setattr(one_time_bot, "get_updates", get_updates) app = ApplicationBuilder().bot(one_time_bot).build() original_get = app.update_queue.get raise_cancelled_error = threading.Event() async def get(*arg, **kwargs): await asyncio.sleep(0.05) if raise_cancelled_error.is_set(): raise_cancelled_error.clear() raise asyncio.CancelledError("Mocked CancelledError") return await original_get(*arg, **kwargs) monkeypatch.setattr(app.update_queue, "get", get) def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") time.sleep(0.1) raise_cancelled_error.set() async with app: with caplog.at_level(logging.WARNING): thread = Thread(target=thread_target) await app.start() thread.start() assert thread.is_alive() raise_cancelled_error.wait() # The exit should have been caught and the app should still be running assert not thread.is_alive() assert app.running # Explicit shutdown is required await app.stop() thread.join() assert not thread.is_alive() assert not app.running # Make sure that we were warned about the necessity of a manual shutdown assert len(caplog.records) == 1 record = caplog.records[0] assert record.name == "telegram.ext.Application" assert record.getMessage().startswith( "Fetching updates got a asyncio.CancelledError. Ignoring" ) def test_run_without_updater(self, one_time_bot): app = ApplicationBuilder().bot(one_time_bot).updater(None).build() with pytest.raises(RuntimeError, match="only available if the application has an Updater"): app.run_webhook() with pytest.raises(RuntimeError, match="only available if the application has an Updater"): app.run_polling() @pytest.mark.parametrize("method", ["start", "initialize"]) @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") def test_run_error_in_application(self, one_time_bot, monkeypatch, method): shutdowns = [] async def raise_method(*args, **kwargs): raise RuntimeError("Test Exception") def after_shutdown(name): def _after_shutdown(*args, **kwargs): shutdowns.append(name) return _after_shutdown monkeypatch.setattr(Application, method, raise_method) monkeypatch.setattr( Application, "shutdown", call_after(Application.shutdown, after_shutdown("application")), ) monkeypatch.setattr( Updater, "shutdown", call_after(Updater.shutdown, after_shutdown("updater")) ) app = ApplicationBuilder().bot(one_time_bot).build() with pytest.raises(RuntimeError, match="Test Exception"): app.run_polling(close_loop=False) assert not app.running assert not app.updater.running if method == "initialize": # If App.initialize fails, then App.shutdown pretty much does nothing, especially # doesn't call Updater.shutdown. assert set(shutdowns) == {"application"} else: assert set(shutdowns) == {"application", "updater"} @pytest.mark.parametrize("method", ["start_polling", "start_webhook"]) @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") def test_run_error_in_updater(self, one_time_bot, monkeypatch, method): shutdowns = [] async def raise_method(*args, **kwargs): raise RuntimeError("Test Exception") def after_shutdown(name): def _after_shutdown(*args, **kwargs): shutdowns.append(name) return _after_shutdown monkeypatch.setattr(Updater, method, raise_method) monkeypatch.setattr( Application, "shutdown", call_after(Application.shutdown, after_shutdown("application")), ) monkeypatch.setattr( Updater, "shutdown", call_after(Updater.shutdown, after_shutdown("updater")) ) app = ApplicationBuilder().bot(one_time_bot).build() with pytest.raises(RuntimeError, match="Test Exception"): # noqa: PT012 if "polling" in method: app.run_polling(close_loop=False) else: app.run_webhook(close_loop=False) assert not app.running assert not app.updater.running assert set(shutdowns) == {"application", "updater"} @pytest.mark.skipif( platform.system() != "Windows", reason="Only really relevant on windows", ) @pytest.mark.parametrize("method", ["start_polling", "start_webhook"]) def test_run_stop_signal_warning_windows(self, one_time_bot, method, recwarn, monkeypatch): async def raise_method(*args, **kwargs): raise RuntimeError("Prevent Actually Running") monkeypatch.setattr(Application, "initialize", raise_method) app = ApplicationBuilder().bot(one_time_bot).build() with pytest.raises(RuntimeError, match="Prevent Actually Running"): # noqa: PT012 if "polling" in method: app.run_polling(close_loop=False, stop_signals=(signal.SIGINT,)) else: app.run_webhook(close_loop=False, stop_signals=(signal.SIGTERM,)) assert len(recwarn) >= 1 found = False for record in recwarn: print(record) if str(record.message).startswith("Could not add signal handlers for the stop"): assert record.category is PTBUserWarning assert record.filename == __file__, "stacklevel is incorrect!" found = True assert found recwarn.clear() with pytest.raises(RuntimeError, match="Prevent Actually Running"): # noqa: PT012 if "polling" in method: app.run_polling(close_loop=False, stop_signals=None) else: app.run_webhook(close_loop=False, stop_signals=None) for record in recwarn: assert not str(record.message).startswith("Could not add signal handlers for the stop") @pytest.mark.flaky(3, 1) # loop.call_later will error the test when a flood error is received def test_signal_handlers(self, app, monkeypatch): # this test should make sure that signal handlers are set by default on Linux + Mac, # and not on Windows. received_signals = [] def signal_handler_test(*args, **kwargs): # args[0] is the signal, [1] the callback received_signals.append(args[0]) loop = asyncio.get_event_loop() monkeypatch.setattr(loop, "add_signal_handler", signal_handler_test) def abort_app(): raise SystemExit loop.call_later(0.6, abort_app) app.run_polling(close_loop=False) if platform.system() == "Windows": assert received_signals == [] else: assert received_signals == [signal.SIGINT, signal.SIGTERM, signal.SIGABRT] received_signals.clear() loop.call_later(0.6, abort_app) app.run_webhook(port=49152, webhook_url="example.com", close_loop=False) if platform.system() == "Windows": assert received_signals == [] else: assert received_signals == [signal.SIGINT, signal.SIGTERM, signal.SIGABRT] def test_stop_running_not_running(self, app, caplog): with caplog.at_level(logging.DEBUG): app.stop_running() assert len(caplog.records) == 1 assert caplog.records[-1].name == "telegram.ext.Application" assert caplog.records[-1].getMessage().endswith("stop_running() does nothing.") @pytest.mark.parametrize("method", ["polling", "webhook"]) def test_stop_running(self, one_time_bot, monkeypatch, method): # asyncio.Event() seems to be hard to use across different threads (awaiting in main # thread, setting in another thread), so we use threading.Event() instead. # This requires the use of run_in_executor, but that's fine. put_update_event = threading.Event() callback_done_event = threading.Event() called_stop_running = threading.Event() assertions = {} async def get_updates(*args, **kwargs): await asyncio.sleep(0) return [] async def delete_webhook(*args, **kwargs): return True async def set_webhook(*args, **kwargs): return True async def post_init(app): # Simply calling app.update_queue.put_nowait(method) in the thread_target doesn't work # for some reason (probably threading magic), so we use an event from the thread_target # to put the update into the queue in the main thread. async def task(app): await asyncio.get_running_loop().run_in_executor(None, put_update_event.wait) await app.update_queue.put(method) app.create_task(task(app)) app = ApplicationBuilder().bot(one_time_bot).post_init(post_init).build() monkeypatch.setattr(app.bot, "get_updates", get_updates) monkeypatch.setattr(app.bot, "set_webhook", set_webhook) monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook) events = [] monkeypatch.setattr( app.updater, "stop", call_after(app.updater.stop, lambda _: events.append("updater.stop")), ) monkeypatch.setattr( app, "stop", call_after(app.stop, lambda _: events.append("app.stop")), ) monkeypatch.setattr( app, "shutdown", call_after(app.shutdown, lambda _: events.append("app.shutdown")), ) def thread_target(): waited = 0 while not app.running: time.sleep(0.05) waited += 0.05 if waited > 5: pytest.fail("App apparently won't start") time.sleep(0.1) assertions["called_stop_running_not_set"] = not called_stop_running.is_set() put_update_event.set() time.sleep(0.1) assertions["called_stop_running_set"] = called_stop_running.is_set() # App should have entered `stop` now but not finished it yet because the callback # is still running assertions["updater.stop_event"] = events == ["updater.stop"] assertions["app.running_False"] = not app.running callback_done_event.set() time.sleep(0.1) # Now that the update is fully handled, we expect the full shutdown assertions["events"] = events == ["updater.stop", "app.stop", "app.shutdown"] async def callback(update, context): context.application.stop_running() called_stop_running.set() await asyncio.get_running_loop().run_in_executor(None, callback_done_event.wait) app.add_handler(TypeHandler(object, callback)) thread = Thread(target=thread_target) thread.start() if method == "polling": app.run_polling(close_loop=False, drop_pending_updates=True) else: ip = "127.0.0.1" port = randrange(1024, 49152) app.run_webhook( ip_address=ip, port=port, url_path="TOKEN", drop_pending_updates=False, close_loop=False, ) thread.join() assert len(assertions) == 5 for key, value in assertions.items(): assert value, f"assertion '{key}' failed!" python-telegram-bot-21.1.1/tests/ext/test_applicationbuilder.py000066400000000000000000000573751460724040100247410ustar00rootroot00000000000000#!/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 from dataclasses import dataclass from http import HTTPStatus import httpx import pytest from telegram import Bot from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.ext import ( AIORateLimiter, Application, ApplicationBuilder, CallbackDataCache, ContextTypes, Defaults, ExtBot, JobQueue, PicklePersistence, Updater, ) from telegram.ext._applicationbuilder import _BOT_CHECKS from telegram.ext._baseupdateprocessor import SimpleUpdateProcessor from telegram.request import HTTPXRequest from telegram.warnings import PTBDeprecationWarning from tests.auxil.constants import PRIVATE_KEY from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture() def builder(): return ApplicationBuilder() @pytest.mark.skipif(TEST_WITH_OPT_DEPS, reason="Optional dependencies are installed") class TestApplicationBuilderNoOptDeps: @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") def test_init(self, builder): builder.token("token") app = builder.build() assert app.job_queue is None @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="Optional dependencies not installed") class TestApplicationBuilder: def test_slot_behaviour(self, builder): for attr in builder.__slots__: assert getattr(builder, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(builder)) == len(set(mro_slots(builder))), "duplicate slot" @pytest.mark.parametrize("get_updates", [True, False]) def test_all_methods_request(self, builder, get_updates): arguments = inspect.signature(HTTPXRequest.__init__).parameters.keys() prefix = "get_updates_" if get_updates else "" for argument in arguments: if argument == "self": continue if argument == "media_write_timeout" and get_updates: # get_updates never makes media requests continue assert hasattr(builder, prefix + argument), f"missing method {prefix}{argument}" @pytest.mark.parametrize("bot_class", [Bot, ExtBot]) def test_all_methods_bot(self, builder, bot_class): arguments = inspect.signature(bot_class.__init__).parameters.keys() for argument in arguments: if argument == "self": continue if argument == "private_key_password": argument = "private_key" # noqa: PLW2901 assert hasattr(builder, argument), f"missing method {argument}" def test_all_methods_application(self, builder): arguments = inspect.signature(Application.__init__).parameters.keys() for argument in arguments: if argument == "self": continue if argument == "update_processor": argument = "concurrent_updates" # noqa: PLW2901 assert hasattr(builder, argument), f"missing method {argument}" def test_job_queue_init_exception(self, monkeypatch): def init_raises_runtime_error(*args, **kwargs): raise RuntimeError("RuntimeError") monkeypatch.setattr(JobQueue, "__init__", init_raises_runtime_error) with pytest.raises(RuntimeError, match="RuntimeError"): ApplicationBuilder() def test_build_without_token(self, builder): with pytest.raises(RuntimeError, match="No bot token was set."): builder.build() def test_build_custom_bot(self, builder, bot): builder.bot(bot) app = builder.build() assert app.bot is bot assert app.updater.bot is bot def test_default_values(self, bot, monkeypatch, builder): @dataclass class Client: timeout: object proxy: object limits: object http1: object http2: object transport: object = None monkeypatch.setattr(httpx, "AsyncClient", Client) app = builder.token(bot.token).build() assert isinstance(app, Application) assert isinstance(app.update_processor, SimpleUpdateProcessor) assert app.update_processor.max_concurrent_updates == 1 assert isinstance(app.bot, ExtBot) assert isinstance(app.bot.request, HTTPXRequest) assert "api.telegram.org" in app.bot.base_url assert bot.token in app.bot.base_url assert "api.telegram.org" in app.bot.base_file_url assert bot.token in app.bot.base_file_url assert app.bot.private_key is None assert app.bot.callback_data_cache is None assert app.bot.defaults is None assert app.bot.rate_limiter is None assert app.bot.local_mode is False get_updates_client = app.bot._request[0]._client assert get_updates_client.limits == httpx.Limits( max_connections=1, max_keepalive_connections=1 ) assert get_updates_client.proxy is None assert get_updates_client.timeout == httpx.Timeout( connect=5.0, read=5.0, write=5.0, pool=1.0 ) assert get_updates_client.http1 is True assert not get_updates_client.http2 client = app.bot.request._client assert client.limits == httpx.Limits(max_connections=256, max_keepalive_connections=256) assert client.proxy is None assert client.timeout == httpx.Timeout(connect=5.0, read=5.0, write=5.0, pool=1.0) assert client.http1 is True assert not client.http2 assert isinstance(app.update_queue, asyncio.Queue) assert isinstance(app.updater, Updater) assert app.updater.bot is app.bot assert app.updater.update_queue is app.update_queue assert isinstance(app.job_queue, JobQueue) assert app.job_queue.application is app assert app.persistence is None assert app.post_init is None assert app.post_shutdown is None assert app.post_stop is None @pytest.mark.parametrize( ("method", "description"), _BOT_CHECKS, ids=[entry[0] for entry in _BOT_CHECKS] ) def test_mutually_exclusive_for_bot(self, builder, method, description): # First test that e.g. `bot` can't be set if `request` was already set # We pass the private key since `private_key` is the only method that doesn't just save # the passed value getattr(builder, method)(data_file("private.key")) with pytest.raises(RuntimeError, match=f"`bot` may only be set, if no {description}"): builder.bot(None) # Now test that `request` can't be set if `bot` was already set builder = builder.__class__() builder.bot(None) with pytest.raises(RuntimeError, match=f"`{method}` may only be set, if no bot instance"): getattr(builder, method)(data_file("private.key")) @pytest.mark.parametrize( "method", [ "connection_pool_size", "connect_timeout", "pool_timeout", "read_timeout", "write_timeout", "media_write_timeout", "proxy", "proxy_url", "socket_options", "bot", "updater", "http_version", ], ) def test_mutually_exclusive_for_request(self, builder, method): builder.request(1) method_name = method.replace("proxy_url", "proxy") with pytest.raises( RuntimeError, match=f"`{method_name}` may only be set, if no request instance" ): getattr(builder, method)(data_file("private.key")) builder = ApplicationBuilder() getattr(builder, method)(1) with pytest.raises(RuntimeError, match="`request` may only be set, if no"): builder.request(1) @pytest.mark.parametrize( "method", [ "get_updates_connection_pool_size", "get_updates_connect_timeout", "get_updates_pool_timeout", "get_updates_read_timeout", "get_updates_write_timeout", "get_updates_proxy", "get_updates_proxy_url", "get_updates_socket_options", "get_updates_http_version", "bot", "updater", ], ) def test_mutually_exclusive_for_get_updates_request(self, builder, method): builder.get_updates_request(1) method_name = method.replace("proxy_url", "proxy") with pytest.raises( RuntimeError, match=f"`{method_name}` may only be set, if no get_updates_request instance", ): getattr(builder, method)(data_file("private.key")) builder = ApplicationBuilder() getattr(builder, method)(1) with pytest.raises(RuntimeError, match="`get_updates_request` may only be set, if no"): builder.get_updates_request(1) @pytest.mark.parametrize( "method", [ "get_updates_connection_pool_size", "get_updates_connect_timeout", "get_updates_pool_timeout", "get_updates_read_timeout", "get_updates_write_timeout", "get_updates_proxy_url", "get_updates_proxy", "get_updates_socket_options", "get_updates_http_version", "connection_pool_size", "connect_timeout", "pool_timeout", "read_timeout", "write_timeout", "media_write_timeout", "proxy", "proxy_url", "socket_options", "http_version", "bot", "update_queue", "rate_limiter", ] + [entry[0] for entry in _BOT_CHECKS], ) def test_mutually_exclusive_for_updater(self, builder, method): builder.updater(1) method_name = method.replace("proxy_url", "proxy") with pytest.raises( RuntimeError, match=f"`{method_name}` may only be set, if no updater", ): getattr(builder, method)(data_file("private.key")) builder = ApplicationBuilder() getattr(builder, method)(data_file("private.key")) method = method.replace("proxy_url", "proxy") with pytest.raises(RuntimeError, match=f"`updater` may only be set, if no {method}"): builder.updater(1) @pytest.mark.parametrize( "method", [ "get_updates_connection_pool_size", "get_updates_connect_timeout", "get_updates_pool_timeout", "get_updates_read_timeout", "get_updates_write_timeout", "get_updates_proxy", "get_updates_proxy_url", "get_updates_socket_options", "get_updates_http_version", "connection_pool_size", "connect_timeout", "pool_timeout", "read_timeout", "write_timeout", "media_write_timeout", "proxy", "proxy_url", "socket_options", "bot", "http_version", ] + [entry[0] for entry in _BOT_CHECKS], ) def test_mutually_non_exclusive_for_updater(self, builder, method): # If no updater is to be used, all these parameters should be settable # Since the parameters themself are tested in the other tests, we here just make sure # that no exception is raised builder.updater(None) getattr(builder, method)(data_file("private.key")) builder = ApplicationBuilder() getattr(builder, method)(data_file("private.key")) builder.updater(None) # We test with bot the new & legacy version to ensure that the legacy version still works @pytest.mark.parametrize( ("proxy_method", "get_updates_proxy_method"), [("proxy", "get_updates_proxy"), ("proxy_url", "get_updates_proxy_url")], ids=["new", "legacy"], ) def test_all_bot_args_custom( self, builder, bot, monkeypatch, proxy_method, get_updates_proxy_method ): # Only socket_options is tested in a standalone test, since that's easier defaults = Defaults() request = HTTPXRequest() get_updates_request = HTTPXRequest() rate_limiter = AIORateLimiter() builder.token(bot.token).base_url("base_url").base_file_url("base_file_url").private_key( PRIVATE_KEY ).defaults(defaults).arbitrary_callback_data(42).request(request).get_updates_request( get_updates_request ).rate_limiter( rate_limiter ).local_mode( True ) built_bot = builder.build().bot # In the following we access some private attributes of bot and request. this is not # really nice as we want to test the public interface, but here it's hard to ensure by # other means that the parameters are passed correctly assert built_bot.token == bot.token assert built_bot.base_url == "base_url" + bot.token assert built_bot.base_file_url == "base_file_url" + bot.token assert built_bot.defaults is defaults assert built_bot.request is request assert built_bot._request[0] is get_updates_request assert built_bot.callback_data_cache.maxsize == 42 assert built_bot.private_key assert built_bot.rate_limiter is rate_limiter assert built_bot.local_mode is True @dataclass class Client: timeout: object proxy: object limits: object http1: object http2: object transport: object = None original_init = HTTPXRequest.__init__ media_write_timeout = [] def init_httpx_request(self_, *args, **kwargs): media_write_timeout.append(kwargs.get("media_write_timeout")) original_init(self_, *args, **kwargs) monkeypatch.setattr(httpx, "AsyncClient", Client) monkeypatch.setattr(HTTPXRequest, "__init__", init_httpx_request) builder = ApplicationBuilder().token(bot.token) builder.connection_pool_size(1).connect_timeout(2).pool_timeout(3).read_timeout( 4 ).write_timeout(5).media_write_timeout(6).http_version("1.1") getattr(builder, proxy_method)("proxy") app = builder.build() client = app.bot.request._client assert client.timeout == httpx.Timeout(pool=3, connect=2, read=4, write=5) assert client.limits == httpx.Limits(max_connections=1, max_keepalive_connections=1) assert client.proxy == "proxy" assert client.http1 is True assert client.http2 is False assert media_write_timeout == [6, None] media_write_timeout.clear() builder = ApplicationBuilder().token(bot.token) builder.get_updates_connection_pool_size(1).get_updates_connect_timeout( 2 ).get_updates_pool_timeout(3).get_updates_read_timeout(4).get_updates_write_timeout( 5 ).get_updates_http_version( "1.1" ) getattr(builder, get_updates_proxy_method)("get_updates_proxy") app = builder.build() client = app.bot._request[0]._client assert client.timeout == httpx.Timeout(pool=3, connect=2, read=4, write=5) assert client.limits == httpx.Limits(max_connections=1, max_keepalive_connections=1) assert client.proxy == "get_updates_proxy" assert client.http1 is True assert client.http2 is False assert media_write_timeout == [None, None] def test_custom_socket_options(self, builder, monkeypatch, bot): httpx_request_kwargs = [] httpx_request_init = HTTPXRequest.__init__ def init_transport(*args, **kwargs): nonlocal httpx_request_kwargs # This is called once for request and once for get_updates_request, so we make # it a list httpx_request_kwargs.append(kwargs.copy()) httpx_request_init(*args, **kwargs) monkeypatch.setattr(HTTPXRequest, "__init__", init_transport) builder.token(bot.token).build() assert httpx_request_kwargs[0].get("socket_options") is None assert httpx_request_kwargs[1].get("socket_options") is None httpx_request_kwargs = [] ApplicationBuilder().token(bot.token).socket_options(((1, 2, 3),)).connection_pool_size( "request" ).get_updates_socket_options(((4, 5, 6),)).get_updates_connection_pool_size( "get_updates" ).build() for kwargs in httpx_request_kwargs: if kwargs.get("connection_pool_size") == "request": assert kwargs.get("socket_options") == ((1, 2, 3),) else: assert kwargs.get("socket_options") == ((4, 5, 6),) def test_custom_application_class(self, bot, builder): class CustomApplication(Application): def __init__(self, arg, **kwargs): super().__init__(**kwargs) self.arg = arg builder.application_class(CustomApplication, kwargs={"arg": 2}).token(bot.token) app = builder.build() assert isinstance(app, CustomApplication) assert app.arg == 2 @pytest.mark.parametrize( ("concurrent_updates", "expected"), [ (4, SimpleUpdateProcessor(4)), (False, SimpleUpdateProcessor(1)), (True, SimpleUpdateProcessor(256)), ], ) def test_all_application_args_custom( self, builder, bot, monkeypatch, concurrent_updates, expected ): job_queue = JobQueue() persistence = PicklePersistence("file_path") update_queue = asyncio.Queue() context_types = ContextTypes() async def post_init(app: Application) -> None: pass async def post_shutdown(app: Application) -> None: pass async def post_stop(app: Application) -> None: pass app = ( builder.token(bot.token) .job_queue(job_queue) .persistence(persistence) .update_queue(update_queue) .context_types(context_types) .concurrent_updates(concurrent_updates) .post_init(post_init) .post_shutdown(post_shutdown) .post_stop(post_stop) .arbitrary_callback_data(True) ).build() assert app.job_queue is job_queue assert app.job_queue.application is app assert app.persistence is persistence assert app.persistence.bot is app.bot assert app.update_queue is update_queue assert app.updater.update_queue is update_queue assert app.updater.bot is app.bot assert app.context_types is context_types assert isinstance(app.update_processor, SimpleUpdateProcessor) assert app.update_processor.max_concurrent_updates == expected.max_concurrent_updates assert app.concurrent_updates == app.update_processor.max_concurrent_updates assert app.post_init is post_init assert app.post_shutdown is post_shutdown assert app.post_stop is post_stop assert isinstance(app.bot.callback_data_cache, CallbackDataCache) updater = Updater(bot=bot, update_queue=update_queue) app = ApplicationBuilder().updater(updater).build() assert app.updater is updater assert app.bot is updater.bot assert app.update_queue is updater.update_queue app = ( builder.token(bot.token) .job_queue(job_queue) .persistence(persistence) .update_queue(update_queue) .context_types(context_types) .concurrent_updates(expected) .post_init(post_init) .post_shutdown(post_shutdown) .post_stop(post_stop) .arbitrary_callback_data(True) ).build() assert app.update_processor is expected @pytest.mark.parametrize("input_type", ["bytes", "str", "Path"]) def test_all_private_key_input_types(self, builder, bot, input_type): private_key = data_file("private.key") password = data_file("private_key.password") if input_type == "bytes": private_key = private_key.read_bytes() password = password.read_bytes() if input_type == "str": private_key = str(private_key) password = str(password) builder.token(bot.token).private_key( private_key=private_key, password=password, ) bot = builder.build().bot assert bot.private_key def test_no_updater(self, bot, builder): app = builder.token(bot.token).updater(None).build() assert app.bot.token == bot.token assert app.updater is None assert isinstance(app.update_queue, asyncio.Queue) assert isinstance(app.job_queue, JobQueue) assert app.job_queue.application is app @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") def test_no_job_queue(self, bot, builder): app = builder.token(bot.token).job_queue(None).build() assert app.bot.token == bot.token assert app.job_queue is None assert isinstance(app.update_queue, asyncio.Queue) assert isinstance(app.updater, Updater) def test_proxy_url_deprecation_warning(self, bot, builder, recwarn): builder.token(bot.token).proxy_url("proxy_url") assert len(recwarn) == 1 assert "`ApplicationBuilder.proxy_url` is deprecated" in str(recwarn[0].message) assert recwarn[0].category is PTBDeprecationWarning assert recwarn[0].filename == __file__, "wrong stacklevel" def test_get_updates_proxy_url_deprecation_warning(self, bot, builder, recwarn): builder.token(bot.token).get_updates_proxy_url("get_updates_proxy_url") assert len(recwarn) == 1 assert "`ApplicationBuilder.get_updates_proxy_url` is deprecated" in str( recwarn[0].message ) assert recwarn[0].category is PTBDeprecationWarning assert recwarn[0].filename == __file__, "wrong stacklevel" @pytest.mark.parametrize( ("read_timeout", "timeout", "expected"), [ (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, builder ): # This test is a double check that ApplicationBuilder respects the changes of #3963 just # like `Bot` does - see also the corresponding test in test_bot.py (same name) 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 = builder.get_updates_read_timeout(10).token(bot.token).build().bot await bot.get_updates(read_timeout=read_timeout, timeout=timeout) assert caught_read_timeout == expected python-telegram-bot-21.1.1/tests/ext/test_basehandler.py000066400000000000000000000046051460724040100233230ustar00rootroot00000000000000#!/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.ext import BaseHandler from tests.auxil.slots import mro_slots class TestHandler: def test_slot_behaviour(self): class SubclassHandler(BaseHandler): __slots__ = () def __init__(self): super().__init__(lambda x: None) def check_update(self, update: object): pass inst = SubclassHandler() 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_repr(self): async def some_func(): return None class SubclassHandler(BaseHandler): __slots__ = () def __init__(self): super().__init__(callback=some_func) def check_update(self, update: object): pass sh = SubclassHandler() assert repr(sh) == "SubclassHandler[callback=TestHandler.test_repr..some_func]" def test_repr_no_qualname(self): class ClassBasedCallback: async def __call__(self, *args, **kwargs): pass def __repr__(self): return "Repr of ClassBasedCallback" class SubclassHandler(BaseHandler): __slots__ = () def __init__(self): super().__init__(callback=ClassBasedCallback()) def check_update(self, update: object): pass sh = SubclassHandler() assert repr(sh) == "SubclassHandler[callback=Repr of ClassBasedCallback]" python-telegram-bot-21.1.1/tests/ext/test_basepersistence.py000066400000000000000000002034411460724040100242310ustar00rootroot00000000000000#!/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 collections import copy import enum import functools import logging import sys import time from pathlib import Path from typing import NamedTuple, Optional import pytest from telegram import Bot, Chat, InlineKeyboardButton, InlineKeyboardMarkup, Update, User from telegram.ext import ( Application, ApplicationBuilder, ApplicationHandlerStop, BaseHandler, BasePersistence, CallbackContext, ConversationHandler, ExtBot, MessageHandler, PersistenceInput, filters, ) from telegram.warnings import PTBUserWarning from tests.auxil.build_messages import make_message_update from tests.auxil.pytest_classes import PytestApplication, make_bot from tests.auxil.slots import mro_slots class HandlerStates(int, enum.Enum): END = ConversationHandler.END STATE_1 = 1 STATE_2 = 2 STATE_3 = 3 STATE_4 = 4 def next(self): cls = self.__class__ members = list(cls) index = members.index(self) + 1 if index >= len(members): index = 0 return members[index] class TrackingPersistence(BasePersistence): """A dummy implementation of BasePersistence that will help us a great deal in keeping the individual tests as short as reasonably possible.""" def __init__( self, store_data: Optional[PersistenceInput] = None, update_interval: float = 60, fill_data: bool = False, ): super().__init__(store_data=store_data, update_interval=update_interval) self.updated_chat_ids = collections.Counter() self.updated_user_ids = collections.Counter() self.refreshed_chat_ids = collections.Counter() self.refreshed_user_ids = collections.Counter() self.dropped_chat_ids = collections.Counter() self.dropped_user_ids = collections.Counter() self.updated_conversations = collections.defaultdict(collections.Counter) self.updated_bot_data: bool = False self.refreshed_bot_data: bool = False self.updated_callback_data: bool = False self.flushed = False self.chat_data = collections.defaultdict(dict) self.user_data = collections.defaultdict(dict) self.conversations = collections.defaultdict(dict) self.bot_data = {} self.callback_data = ([], {}) if fill_data: self.fill() CALLBACK_DATA = ( [("uuid", time.time(), {"uuid4": "callback_data"})], {"query_id": "keyboard_id"}, ) def fill(self): self.chat_data[1]["key"] = "value" self.chat_data[2]["foo"] = "bar" self.user_data[1]["key"] = "value" self.user_data[2]["foo"] = "bar" self.bot_data["key"] = "value" self.conversations["conv_1"][(1, 1)] = HandlerStates.STATE_1 self.conversations["conv_1"][(2, 2)] = HandlerStates.STATE_2 self.conversations["conv_2"][(3, 3)] = HandlerStates.STATE_3 self.conversations["conv_2"][(4, 4)] = HandlerStates.STATE_4 self.callback_data = self.CALLBACK_DATA def reset_tracking(self): self.updated_user_ids.clear() self.updated_chat_ids.clear() self.dropped_user_ids.clear() self.dropped_chat_ids.clear() self.refreshed_chat_ids = collections.Counter() self.refreshed_user_ids = collections.Counter() self.updated_conversations.clear() self.updated_bot_data = False self.refreshed_bot_data = False self.updated_callback_data = False self.flushed = False self.chat_data = {} self.user_data = {} self.conversations = collections.defaultdict(dict) self.bot_data = {} self.callback_data = ([], {}) async def update_bot_data(self, data): self.updated_bot_data = True self.bot_data = data async def update_chat_data(self, chat_id: int, data): self.updated_chat_ids[chat_id] += 1 self.chat_data[chat_id] = data async def update_user_data(self, user_id: int, data): self.updated_user_ids[user_id] += 1 self.user_data[user_id] = data async def update_conversation(self, name: str, key, new_state): self.updated_conversations[name][key] += 1 self.conversations[name][key] = new_state async def update_callback_data(self, data): self.updated_callback_data = True self.callback_data = data async def get_conversations(self, name): return self.conversations.get(name, {}) async def get_bot_data(self): return copy.deepcopy(self.bot_data) async def get_chat_data(self): return copy.deepcopy(self.chat_data) async def get_user_data(self): return copy.deepcopy(self.user_data) async def get_callback_data(self): return copy.deepcopy(self.callback_data) async def drop_chat_data(self, chat_id): self.dropped_chat_ids[chat_id] += 1 self.chat_data.pop(chat_id, None) async def drop_user_data(self, user_id): self.dropped_user_ids[user_id] += 1 self.user_data.pop(user_id, None) async def refresh_user_data(self, user_id: int, user_data: dict): self.refreshed_user_ids[user_id] += 1 user_data["refreshed"] = True async def refresh_chat_data(self, chat_id: int, chat_data: dict): self.refreshed_chat_ids[chat_id] += 1 chat_data["refreshed"] = True async def refresh_bot_data(self, bot_data: dict): self.refreshed_bot_data = True bot_data["refreshed"] = True async def flush(self) -> None: self.flushed = True class TrackingConversationHandler(ConversationHandler): def __init__(self, *args, **kwargs): fallbacks = [] states = {state.value: [self.build_handler(state)] for state in HandlerStates} entry_points = [self.build_handler(HandlerStates.END)] super().__init__( *args, **kwargs, fallbacks=fallbacks, states=states, entry_points=entry_points ) @staticmethod async def callback(update, context, state): return state.next() @staticmethod def build_update(state: HandlerStates, chat_id: int): user = User(id=chat_id, first_name="", is_bot=False) chat = Chat(id=chat_id, type="") return make_message_update(message=str(state.value), user=user, chat=chat) @classmethod def build_handler(cls, state: HandlerStates, callback=None): return MessageHandler( filters.Regex(f"^{state.value}$"), callback or functools.partial(cls.callback, state=state), ) class PappInput(NamedTuple): bot_data: Optional[bool] = None chat_data: Optional[bool] = None user_data: Optional[bool] = None callback_data: Optional[bool] = None conversations: bool = True update_interval: float = None fill_data: bool = False def build_papp( bot_info: Optional[dict] = None, token: Optional[str] = None, store_data: Optional[dict] = None, update_interval: Optional[float] = None, fill_data: bool = False, ) -> Application: store_data = PersistenceInput(**(store_data or {})) if update_interval is not None: persistence = TrackingPersistence( store_data=store_data, update_interval=update_interval, fill_data=fill_data ) else: persistence = TrackingPersistence(store_data=store_data, fill_data=fill_data) if bot_info is not None: bot = make_bot(bot_info, arbitrary_callback_data=True) else: bot = make_bot(token=token, arbitrary_callback_data=True) return ( ApplicationBuilder() .bot(bot) .persistence(persistence) .application_class(PytestApplication) .build() ) def build_conversation_handler(name: str, persistent: bool = True) -> BaseHandler: return TrackingConversationHandler(name=name, persistent=persistent) @pytest.fixture() def papp(request, bot_info) -> Application: papp_input = request.param store_data = {} if papp_input.bot_data is not None: store_data["bot_data"] = papp_input.bot_data if papp_input.chat_data is not None: store_data["chat_data"] = papp_input.chat_data if papp_input.user_data is not None: store_data["user_data"] = papp_input.user_data if papp_input.callback_data is not None: store_data["callback_data"] = papp_input.callback_data app = build_papp( bot_info=bot_info, store_data=store_data, update_interval=papp_input.update_interval, fill_data=papp_input.fill_data, ) app.add_handlers( [ build_conversation_handler(name="conv_1", persistent=papp_input.conversations), build_conversation_handler(name="conv_2", persistent=papp_input.conversations), ] ) return app # Decorator shortcuts default_papp = pytest.mark.parametrize("papp", [PappInput()], indirect=True) filled_papp = pytest.mark.parametrize("papp", [PappInput(fill_data=True)], indirect=True) papp_store_all_or_none = pytest.mark.parametrize( "papp", [ PappInput(), PappInput(False, False, False, False), ], ids=( "all_data", "no_data", ), indirect=True, ) class TestBasePersistence: """Tests basic behavior of BasePersistence and (most importantly) the integration of persistence into the Application.""" def job_callback(self, chat_id: Optional[int] = None): async def callback(context): if context.user_data: context.user_data["key"] = "value" if context.chat_data: context.chat_data["key"] = "value" context.bot_data["key"] = "value" if chat_id: await context.bot.send_message( chat_id=chat_id, text="text", reply_markup=InlineKeyboardMarkup.from_button( InlineKeyboardButton(text="text", callback_data="callback_data") ), ) return callback def handler_callback(self, chat_id: Optional[int] = None, sleep: Optional[float] = None): async def callback(update, context): if sleep: await asyncio.sleep(sleep) context.user_data["key"] = "value" context.chat_data["key"] = "value" context.bot_data["key"] = "value" if chat_id: await context.bot.send_message( chat_id=chat_id, text="text", reply_markup=InlineKeyboardMarkup.from_button( InlineKeyboardButton(text="text", callback_data="callback_data") ), ) raise ApplicationHandlerStop return callback def test_slot_behaviour(self): inst = TrackingPersistence() for attr in inst.__slots__: assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" # We're interested in BasePersistence, not in the implementation slots = mro_slots(inst, only_parents=True) assert len(slots) == len(set(slots)), "duplicate slot" @pytest.mark.parametrize("bot_data", [True, False]) @pytest.mark.parametrize("chat_data", [True, False]) @pytest.mark.parametrize("user_data", [True, False]) @pytest.mark.parametrize("callback_data", [True, False]) def test_init_store_data_update_interval(self, bot_data, chat_data, user_data, callback_data): store_data = PersistenceInput( bot_data=bot_data, chat_data=chat_data, user_data=user_data, callback_data=callback_data, ) persistence = TrackingPersistence(store_data=store_data, update_interval=3.14) assert persistence.store_data.bot_data == bot_data assert persistence.store_data.chat_data == chat_data assert persistence.store_data.user_data == user_data assert persistence.store_data.callback_data == callback_data def test_abstract_methods(self): methods = list(BasePersistence.__abstractmethods__) methods.sort() with pytest.raises( TypeError, match=( ", ".join(methods) if sys.version_info < (3, 12) else ", ".join(f"'{i}'" for i in methods) ), ): BasePersistence() @default_papp def test_update_interval_immutable(self, papp): with pytest.raises(AttributeError, match="can not assign a new value to update_interval"): papp.persistence.update_interval = 7 @default_papp def test_set_bot_error(self, papp): with pytest.raises(TypeError, match="when using telegram.ext.ExtBot"): papp.persistence.set_bot(Bot(papp.bot.token)) # just making sure that setting an ExtBoxt without callback_data_cache doesn't raise an # error even though store_callback_data is True bot = ExtBot(papp.bot.token) assert bot.callback_data_cache is None assert papp.persistence.set_bot(bot) is None def test_construction_with_bad_persistence(self, bot): class MyPersistence: def __init__(self): self.store_data = PersistenceInput(False, False, False, False) with pytest.raises( TypeError, match="persistence must be based on telegram.ext.BasePersistence" ): ApplicationBuilder().bot(bot).persistence(MyPersistence()).build() @pytest.mark.parametrize( "papp", [PappInput(fill_data=True), PappInput(False, False, False, False, False, fill_data=True)], indirect=True, ) async def test_initialization_basic(self, papp: Application): # Check that no data is there before init assert not papp.chat_data assert not papp.user_data assert not papp.bot_data assert papp.bot.callback_data_cache.persistence_data == ([], {}) assert not papp.handlers[0][0].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_1, chat_id=1) ) assert not papp.handlers[0][0].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_2, chat_id=2) ) assert not papp.handlers[0][1].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_3, chat_id=3) ) assert not papp.handlers[0][1].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_4, chat_id=4) ) async with papp: # Check that data is loaded on init # We check just bot_data because we set all to the same value if papp.persistence.store_data.bot_data: assert papp.chat_data[1]["key"] == "value" assert papp.chat_data[2]["foo"] == "bar" assert papp.user_data[1]["key"] == "value" assert papp.user_data[2]["foo"] == "bar" assert papp.bot_data == {"key": "value"} assert ( papp.bot.callback_data_cache.persistence_data == TrackingPersistence.CALLBACK_DATA ) assert papp.handlers[0][0].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_1, chat_id=1) ) assert papp.handlers[0][0].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_2, chat_id=2) ) assert papp.handlers[0][1].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_3, chat_id=3) ) assert papp.handlers[0][1].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_4, chat_id=4) ) else: assert not papp.chat_data assert not papp.user_data assert not papp.bot_data assert papp.bot.callback_data_cache.persistence_data == ([], {}) assert not papp.handlers[0][0].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_1, chat_id=1) ) assert not papp.handlers[0][0].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_2, chat_id=2) ) assert not papp.handlers[0][1].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_3, chat_id=3) ) assert not papp.handlers[0][1].check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_4, chat_id=4) ) @pytest.mark.parametrize( "papp", [PappInput(fill_data=True)], indirect=True, ) async def test_initialization_invalid_bot_data(self, papp: Application, monkeypatch): async def get_bot_data(*args, **kwargs): return "invalid" monkeypatch.setattr(papp.persistence, "get_bot_data", get_bot_data) with pytest.raises(ValueError, match="bot_data must be"): await papp.initialize() @pytest.mark.parametrize( "papp", [PappInput(fill_data=True)], indirect=True, ) @pytest.mark.parametrize("callback_data", ["invalid", (1, 2, 3)]) async def test_initialization_invalid_callback_data( self, papp: Application, callback_data, monkeypatch ): async def get_callback_data(*args, **kwargs): return callback_data monkeypatch.setattr(papp.persistence, "get_callback_data", get_callback_data) with pytest.raises(ValueError, match="callback_data must be"): await papp.initialize() @filled_papp async def test_add_conversation_handler_after_init(self, papp: Application, recwarn): context = CallbackContext(application=papp) # Set it up such that the handler has a conversation in progress that's not persisted papp.persistence.conversations["conv_1"].pop((2, 2)) conversation = build_conversation_handler("conv_1", persistent=True) update = TrackingConversationHandler.build_update(state=HandlerStates.END, chat_id=2) check = conversation.check_update(update=update) await conversation.handle_update( update=update, check_result=check, application=papp, context=context ) assert conversation.check_update( TrackingConversationHandler.build_update(state=HandlerStates.STATE_1, chat_id=2) ) # and another one that will be overridden update = TrackingConversationHandler.build_update(state=HandlerStates.END, chat_id=1) check = conversation.check_update(update=update) await conversation.handle_update( update=update, check_result=check, application=papp, context=context ) update = TrackingConversationHandler.build_update(state=HandlerStates.STATE_1, chat_id=1) check = conversation.check_update(update=update) await conversation.handle_update( update=update, check_result=check, application=papp, context=context ) assert conversation.check_update( TrackingConversationHandler.build_update(state=HandlerStates.STATE_2, chat_id=1) ) async with papp: papp.add_handler(conversation) assert len(recwarn) >= 1 tasks = asyncio.all_tasks() assert any("conversation_handler_after_init" in t.get_name() for t in tasks) found = False for warning in recwarn: if "after `Application.initialize` was called" in str(warning.message): found = True assert warning.category is PTBUserWarning assert Path(warning.filename) == Path(__file__), "incorrect stacklevel!" assert found await asyncio.sleep(0.05) # conversation with chat_id 2 must not have been overridden assert conversation.check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_1, chat_id=2) ) # conversation with chat_id 1 must have been overridden assert not conversation.check_update( TrackingConversationHandler.build_update(state=HandlerStates.STATE_2, chat_id=1) ) assert conversation.check_update( TrackingConversationHandler.build_update(state=HandlerStates.STATE_1, chat_id=1) ) def test_add_conversation_without_persistence(self, app): with pytest.raises(ValueError, match="if application has no persistence"): app.add_handler(build_conversation_handler("name", persistent=True)) @default_papp async def test_add_conversation_handler_without_name(self, papp: Application): with pytest.raises(ValueError, match="when handler is unnamed"): papp.add_handler(build_conversation_handler(name=None, persistent=True)) @pytest.mark.parametrize( "papp", [ PappInput(update_interval=0.0), ], indirect=True, ) async def test_update_persistence_called(self, papp: Application, monkeypatch): """Tests if Application.update_persistence is called from app.start()""" called = asyncio.Event() async def update_persistence(*args, **kwargs): called.set() monkeypatch.setattr(papp, "update_persistence", update_persistence) async with papp: await papp.start() tasks = asyncio.all_tasks() assert any(":persistence_updater" in task.get_name() for task in tasks) assert await called.wait() await papp.stop() @pytest.mark.flaky(3, 1) @pytest.mark.parametrize( "papp", [ PappInput(update_interval=1.5), ], indirect=True, ) async def test_update_interval(self, papp: Application, monkeypatch): """If we don't want this test to take much longer to run, the accuracy will be a bit low. A few tenths of seconds are easy to go astray ... That's why it's flaky.""" call_times = [] async def update_persistence(*args, **kwargs): call_times.append(time.time()) monkeypatch.setattr(papp, "update_persistence", update_persistence) async with papp: await papp.start() await asyncio.sleep(5) await papp.stop() # Make assertions before calling shutdown, as that calls update_persistence again! diffs = [j - i for i, j in zip(call_times[:-1], call_times[1:])] assert sum(diffs) / len(diffs) == pytest.approx( papp.persistence.update_interval, rel=1e-1 ) @papp_store_all_or_none async def test_update_persistence_loop_call_count_update_handling( self, papp: Application, caplog ): async with papp: for _ in range(5): # second pass processes update in conv_2 await papp.process_update( TrackingConversationHandler.build_update(HandlerStates.END, chat_id=1) ) assert not papp.persistence.updated_bot_data assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_user_ids assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids assert not papp.persistence.updated_callback_data assert not papp.persistence.updated_conversations await papp.update_persistence() assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids assert papp.persistence.updated_bot_data == papp.persistence.store_data.bot_data assert ( papp.persistence.updated_callback_data == papp.persistence.store_data.callback_data ) if papp.persistence.store_data.user_data: assert papp.persistence.updated_user_ids == {1: 1} else: assert not papp.persistence.updated_user_ids if papp.persistence.store_data.chat_data: assert papp.persistence.updated_chat_ids == {1: 1} else: assert not papp.persistence.updated_chat_ids assert papp.persistence.updated_conversations == { "conv_1": {(1, 1): 1}, "conv_2": {(1, 1): 1}, } # Nothing should have been updated after handling nothing papp.persistence.reset_tracking() with caplog.at_level(logging.ERROR): await papp.update_persistence() # Make sure that "nothing updated" is not just due to an error assert not caplog.text assert papp.persistence.updated_bot_data == papp.persistence.store_data.bot_data assert ( papp.persistence.updated_callback_data == papp.persistence.store_data.callback_data ) assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_user_ids assert not papp.persistence.updated_conversations assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids # Nothing should have been updated after handling an update without associated # user/chat_data papp.persistence.reset_tracking() await papp.process_update("string_update") with caplog.at_level(logging.ERROR): await papp.update_persistence() # Make sure that "nothing updated" is not just due to an error assert not caplog.text assert papp.persistence.updated_bot_data == papp.persistence.store_data.bot_data assert ( papp.persistence.updated_callback_data == papp.persistence.store_data.callback_data ) assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_user_ids assert not papp.persistence.updated_conversations assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids @papp_store_all_or_none async def test_update_persistence_loop_call_count_job(self, papp: Application, caplog): async with papp: await papp.job_queue.start() papp.job_queue.run_once(self.job_callback(), when=1.5, chat_id=1, user_id=1) await asyncio.sleep(2.5) assert not papp.persistence.updated_bot_data assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_user_ids assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids assert not papp.persistence.updated_callback_data assert not papp.persistence.updated_conversations await papp.update_persistence() assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids assert papp.persistence.updated_bot_data == papp.persistence.store_data.bot_data assert ( papp.persistence.updated_callback_data == papp.persistence.store_data.callback_data ) if papp.persistence.store_data.user_data: assert papp.persistence.updated_user_ids == {1: 1} else: assert not papp.persistence.updated_user_ids if papp.persistence.store_data.chat_data: assert papp.persistence.updated_chat_ids == {1: 1} else: assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_conversations # Nothing should have been updated after no job ran papp.persistence.reset_tracking() with caplog.at_level(logging.ERROR): await papp.update_persistence() # Make sure that "nothing updated" is not just due to an error assert not caplog.text assert papp.persistence.updated_bot_data == papp.persistence.store_data.bot_data assert ( papp.persistence.updated_callback_data == papp.persistence.store_data.callback_data ) assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_user_ids assert not papp.persistence.updated_conversations assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids # Nothing should have been updated after running job without associated user/chat_data papp.persistence.reset_tracking() papp.job_queue.run_once(self.job_callback(), when=0.1) await asyncio.sleep(0.2) with caplog.at_level(logging.ERROR): await papp.update_persistence() # Make sure that "nothing updated" is not just due to an error assert not caplog.text assert papp.persistence.updated_bot_data == papp.persistence.store_data.bot_data assert ( papp.persistence.updated_callback_data == papp.persistence.store_data.callback_data ) assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_user_ids assert not papp.persistence.updated_conversations assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids @default_papp async def test_calls_on_shutdown(self, papp, chat_id): papp.add_handler( MessageHandler(filters.ALL, callback=self.handler_callback(chat_id=chat_id)), group=-1 ) async with papp: await papp.process_update( TrackingConversationHandler.build_update(HandlerStates.STATE_1, chat_id=1) ) assert not papp.persistence.updated_bot_data assert not papp.persistence.updated_callback_data assert not papp.persistence.updated_user_ids assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_conversations assert not papp.persistence.flushed # Make sure this this outside the context manager, which is where shutdown is called! assert papp.persistence.updated_bot_data assert papp.persistence.bot_data == {"key": "value", "refreshed": True} assert papp.persistence.updated_callback_data assert papp.persistence.callback_data[1] == {} assert len(papp.persistence.callback_data[0]) == 1 assert papp.persistence.updated_user_ids == {1: 1} assert papp.persistence.user_data == {1: {"key": "value", "refreshed": True}} assert papp.persistence.updated_chat_ids == {1: 1} assert papp.persistence.chat_data == {1: {"key": "value", "refreshed": True}} assert not papp.persistence.updated_conversations assert not papp.persistence.conversations assert papp.persistence.flushed @papp_store_all_or_none async def test_update_persistence_loop_saved_data_update_handling( self, papp: Application, chat_id ): papp.add_handler( MessageHandler(filters.ALL, callback=self.handler_callback(chat_id=chat_id)), group=-1 ) async with papp: await papp.process_update( TrackingConversationHandler.build_update(HandlerStates.STATE_1, chat_id=1) ) assert not papp.persistence.bot_data assert papp.persistence.bot_data is not papp.bot_data assert not papp.persistence.chat_data assert papp.persistence.chat_data is not papp.chat_data assert not papp.persistence.user_data assert papp.persistence.user_data is not papp.user_data assert papp.persistence.callback_data == ([], {}) assert ( papp.persistence.callback_data is not papp.bot.callback_data_cache.persistence_data ) assert not papp.persistence.conversations await papp.update_persistence() assert papp.persistence.bot_data is not papp.bot_data if papp.persistence.store_data.bot_data: assert papp.persistence.bot_data == {"key": "value", "refreshed": True} else: assert not papp.persistence.bot_data assert papp.persistence.chat_data is not papp.chat_data if papp.persistence.store_data.chat_data: assert papp.persistence.chat_data == {1: {"key": "value", "refreshed": True}} assert papp.persistence.chat_data[1] is not papp.chat_data[1] else: assert not papp.persistence.chat_data assert papp.persistence.user_data is not papp.user_data if papp.persistence.store_data.user_data: assert papp.persistence.user_data == {1: {"key": "value", "refreshed": True}} assert papp.persistence.user_data[1] is not papp.chat_data[1] else: assert not papp.persistence.user_data assert ( papp.persistence.callback_data is not papp.bot.callback_data_cache.persistence_data ) if papp.persistence.store_data.callback_data: assert papp.persistence.callback_data[1] == {} assert len(papp.persistence.callback_data[0]) == 1 else: assert papp.persistence.callback_data == ([], {}) assert not papp.persistence.conversations @papp_store_all_or_none async def test_update_persistence_loop_saved_data_job(self, papp: Application, chat_id): papp.add_handler( MessageHandler(filters.ALL, callback=self.handler_callback(chat_id=chat_id)), group=-1 ) async with papp: await papp.job_queue.start() papp.job_queue.run_once( self.job_callback(chat_id=chat_id), when=1.5, chat_id=1, user_id=1 ) await asyncio.sleep(2.5) assert not papp.persistence.bot_data assert papp.persistence.bot_data is not papp.bot_data assert not papp.persistence.chat_data assert papp.persistence.chat_data is not papp.chat_data assert not papp.persistence.user_data assert papp.persistence.user_data is not papp.user_data assert papp.persistence.callback_data == ([], {}) assert ( papp.persistence.callback_data is not papp.bot.callback_data_cache.persistence_data ) assert not papp.persistence.conversations await papp.update_persistence() assert papp.persistence.bot_data is not papp.bot_data if papp.persistence.store_data.bot_data: assert papp.persistence.bot_data == {"key": "value", "refreshed": True} else: assert not papp.persistence.bot_data assert papp.persistence.chat_data is not papp.chat_data if papp.persistence.store_data.chat_data: assert papp.persistence.chat_data == {1: {"key": "value", "refreshed": True}} assert papp.persistence.chat_data[1] is not papp.chat_data[1] else: assert not papp.persistence.chat_data assert papp.persistence.user_data is not papp.user_data if papp.persistence.store_data.user_data: assert papp.persistence.user_data == {1: {"key": "value", "refreshed": True}} assert papp.persistence.user_data[1] is not papp.chat_data[1] else: assert not papp.persistence.user_data assert ( papp.persistence.callback_data is not papp.bot.callback_data_cache.persistence_data ) if papp.persistence.store_data.callback_data: assert papp.persistence.callback_data[1] == {} assert len(papp.persistence.callback_data[0]) == 1 else: assert papp.persistence.callback_data == ([], {}) assert not papp.persistence.conversations @default_papp @pytest.mark.parametrize("delay_type", ["job", "handler", "task"]) async def test_update_persistence_loop_async_logic( self, papp: Application, delay_type: str, chat_id ): """All three kinds of 'asyncio background processes' should mark things for update once they're done.""" sleep = 1.5 update = TrackingConversationHandler.build_update(HandlerStates.STATE_1, chat_id=1) async with papp: if delay_type == "job": await papp.job_queue.start() papp.job_queue.run_once(self.job_callback(), when=sleep, chat_id=1, user_id=1) elif delay_type == "handler": papp.add_handler( MessageHandler( filters.ALL, self.handler_callback(sleep=sleep), block=False, ) ) await papp.process_update(update) else: papp.create_task(asyncio.sleep(sleep), update=update) await papp.update_persistence() assert papp.persistence.updated_bot_data assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_user_ids assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids assert papp.persistence.updated_callback_data assert not papp.persistence.updated_conversations # Wait for the asyncio process to be done await asyncio.sleep(sleep + 1) await papp.update_persistence() assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids assert papp.persistence.updated_bot_data == papp.persistence.store_data.bot_data assert ( papp.persistence.updated_callback_data == papp.persistence.store_data.callback_data ) if papp.persistence.store_data.user_data: assert papp.persistence.updated_user_ids == {1: 1} else: assert not papp.persistence.updated_user_ids if papp.persistence.store_data.chat_data: assert papp.persistence.updated_chat_ids == {1: 1} else: assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_conversations @papp_store_all_or_none async def test_update_persistence_loop_manual_mark_for_update_persistence( self, papp: Application, chat_id ): async with papp: papp.chat_data[1].update({"key": "value", "refreshed": True}) papp.user_data[1].update({"key": "value", "refreshed": True}) await papp.update_persistence() # Since no update has been processed, nothing should be marked for update # So we expect the persisted data to differ from the current data assert not papp.persistence.chat_data assert papp.persistence.chat_data is not papp.chat_data assert not papp.persistence.user_data assert papp.persistence.user_data is not papp.user_data # Double checking that not marking data doesn't change anything papp.mark_data_for_update_persistence() await papp.update_persistence() assert not papp.persistence.chat_data assert papp.persistence.chat_data is not papp.chat_data assert not papp.persistence.user_data assert papp.persistence.user_data is not papp.user_data # marking data should lead to the data being updated papp.mark_data_for_update_persistence(chat_ids=1, user_ids=1) await papp.update_persistence() assert papp.persistence.chat_data is not papp.chat_data if papp.persistence.store_data.chat_data: assert papp.persistence.chat_data == {1: {"key": "value", "refreshed": True}} assert papp.persistence.chat_data[1] is not papp.chat_data[1] else: assert not papp.persistence.chat_data assert papp.persistence.user_data is not papp.user_data if papp.persistence.store_data.user_data: assert papp.persistence.user_data == {1: {"key": "value", "refreshed": True}} assert papp.persistence.user_data[1] is not papp.chat_data[1] else: assert not papp.persistence.user_data # Also testing passing collections papp.chat_data[1].update({"key": "value", "refreshed": False}) papp.user_data[1].update({"key": "value", "refreshed": False}) papp.mark_data_for_update_persistence(chat_ids={1}, user_ids={1}) await papp.update_persistence() # marking data should lead to the data being updated assert papp.persistence.chat_data is not papp.chat_data if papp.persistence.store_data.chat_data: assert papp.persistence.chat_data == {1: {"key": "value", "refreshed": False}} assert papp.persistence.chat_data[1] is not papp.chat_data[1] else: assert not papp.persistence.chat_data assert papp.persistence.user_data is not papp.user_data if papp.persistence.store_data.user_data: assert papp.persistence.user_data == {1: {"key": "value", "refreshed": False}} assert papp.persistence.user_data[1] is not papp.chat_data[1] else: assert not papp.persistence.user_data @filled_papp async def test_drop_chat_data(self, papp: Application): async with papp: assert papp.persistence.chat_data == {1: {"key": "value"}, 2: {"foo": "bar"}} assert not papp.persistence.dropped_chat_ids assert not papp.persistence.updated_chat_ids papp.drop_chat_data(1) assert papp.persistence.chat_data == {1: {"key": "value"}, 2: {"foo": "bar"}} assert not papp.persistence.dropped_chat_ids assert not papp.persistence.updated_chat_ids await papp.update_persistence() assert papp.persistence.chat_data == {2: {"foo": "bar"}} assert papp.persistence.dropped_chat_ids == {1: 1} assert not papp.persistence.updated_chat_ids @filled_papp async def test_drop_user_data(self, papp: Application): async with papp: assert papp.persistence.user_data == {1: {"key": "value"}, 2: {"foo": "bar"}} assert not papp.persistence.dropped_user_ids assert not papp.persistence.updated_user_ids papp.drop_user_data(1) assert papp.persistence.user_data == {1: {"key": "value"}, 2: {"foo": "bar"}} assert not papp.persistence.dropped_user_ids assert not papp.persistence.updated_user_ids await papp.update_persistence() assert papp.persistence.user_data == {2: {"foo": "bar"}} assert papp.persistence.dropped_user_ids == {1: 1} assert not papp.persistence.updated_user_ids @filled_papp async def test_migrate_chat_data(self, papp: Application): async with papp: assert papp.persistence.chat_data == {1: {"key": "value"}, 2: {"foo": "bar"}} assert not papp.persistence.dropped_chat_ids assert not papp.persistence.updated_chat_ids papp.migrate_chat_data(old_chat_id=1, new_chat_id=2) assert papp.persistence.chat_data == {1: {"key": "value"}, 2: {"foo": "bar"}} assert not papp.persistence.dropped_chat_ids assert not papp.persistence.updated_chat_ids await papp.update_persistence() assert papp.persistence.chat_data == {2: {"key": "value"}} assert papp.persistence.dropped_chat_ids == {1: 1} assert papp.persistence.updated_chat_ids == {2: 1} async def test_errors_while_persisting(self, bot_info, caplog): class ErrorPersistence(TrackingPersistence): def raise_error(self): raise Exception("PersistenceError") async def update_callback_data(self, data): self.raise_error() async def update_bot_data(self, data): self.raise_error() async def update_chat_data(self, chat_id, data): self.raise_error() async def update_user_data(self, user_id, data): self.raise_error() async def drop_user_data(self, user_id): self.raise_error() async def drop_chat_data(self, chat_id): self.raise_error() async def update_conversation(self, name, key, new_state): self.raise_error() test_flag = [] async def error(update, context): test_flag.append(str(context.error) == "PersistenceError") raise Exception("ErrorHandlingError") app = ( ApplicationBuilder() .bot(make_bot(bot_info, arbitrary_callback_data=True)) .persistence(ErrorPersistence()) .build() ) async with app: app.add_error_handler(error) for _ in range(5): # second pass processes update in conv_2 await app.process_update( TrackingConversationHandler.build_update(HandlerStates.END, chat_id=1) ) app.drop_chat_data(7) app.drop_user_data(42) assert not caplog.records with caplog.at_level(logging.ERROR): await app.update_persistence() assert len(caplog.records) == 6 assert test_flag == [True, True, True, True, True, True] for record in caplog.records: assert record.name == "telegram.ext.Application" message = record.getMessage() assert message.startswith("An error was raised and an uncaught") @default_papp @pytest.mark.parametrize( "delay_type", ["job", "blocking_handler", "nonblocking_handler", "task"] ) async def test_update_persistence_after_exception( self, papp: Application, delay_type: str, chat_id ): """Makes sure that persistence is updated even if an exception happened in a callback.""" sleep = 1.5 update = TrackingConversationHandler.build_update(HandlerStates.STATE_1, chat_id=1) errors = 0 async def error(_, __): nonlocal errors errors += 1 async def raise_error(*args, **kwargs): raise Exception async with papp: papp.add_error_handler(error) await papp.update_persistence() assert papp.persistence.updated_bot_data assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_user_ids assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids assert papp.persistence.updated_callback_data assert not papp.persistence.updated_conversations assert errors == 0 if delay_type == "job": await papp.job_queue.start() papp.job_queue.run_once(raise_error, when=sleep, chat_id=1, user_id=1) elif delay_type.endswith("_handler"): papp.add_handler( MessageHandler( filters.ALL, raise_error, block=delay_type.startswith("blocking"), ) ) await papp.process_update(update) else: papp.create_task(raise_error(), update=update) # Wait for the asyncio process to be done await asyncio.sleep(sleep + 1) assert errors == 1 await papp.update_persistence() assert not papp.persistence.dropped_chat_ids assert not papp.persistence.dropped_user_ids assert papp.persistence.updated_bot_data == papp.persistence.store_data.bot_data assert ( papp.persistence.updated_callback_data == papp.persistence.store_data.callback_data ) if papp.persistence.store_data.user_data: assert papp.persistence.updated_user_ids == {1: 1} else: assert not papp.persistence.updated_user_ids if papp.persistence.store_data.chat_data: assert papp.persistence.updated_chat_ids == {1: 1} else: assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_conversations async def test_non_blocking_conversations(self, bot, caplog): papp = build_papp(token=bot.token, update_interval=100) event = asyncio.Event() async def callback(_, __): await event.wait() return HandlerStates.STATE_1 conversation = ConversationHandler( entry_points=[ TrackingConversationHandler.build_handler(HandlerStates.END, callback=callback) ], states={}, fallbacks=[], persistent=True, name="conv", block=False, ) papp.add_handler(conversation) async with papp: await papp.start() assert papp.persistence.updated_conversations == {} await papp.process_update( TrackingConversationHandler.build_update(HandlerStates.END, 1) ) assert papp.persistence.updated_conversations == {} with caplog.at_level(logging.DEBUG): await papp.update_persistence() await asyncio.sleep(0.01) # Conversation should have been updated with the current state, i.e. None assert papp.persistence.updated_conversations == {"conv": {(1, 1): 1}} assert papp.persistence.conversations == {"conv": {(1, 1): None}} # Ensure that we warn the user about this! found_record = None for record in caplog.records: message = record.getMessage() if message.startswith("A ConversationHandlers state was not yet resolved"): assert "Will check again on next run" in record.getMessage() assert record.name == "telegram.ext.Application" found_record = record break assert found_record is not None caplog.clear() papp.persistence.reset_tracking() event.set() await asyncio.sleep(0.01) with caplog.at_level(logging.DEBUG): await papp.update_persistence() # Conversation should have been updated with the resolved state now and hence # there should be no warning assert not any( record.getMessage().startswith("A ConversationHandlers state was not yet") for record in caplog.records ) assert papp.persistence.updated_conversations == {"conv": {(1, 1): 1}} assert papp.persistence.conversations == {"conv": {(1, 1): HandlerStates.STATE_1}} await papp.stop() async def test_non_blocking_conversations_raises_Exception(self, bot): papp = build_papp(token=bot.token) async def callback_1(_, __): return HandlerStates.STATE_1 async def callback_2(_, __): raise Exception("Test Exception") conversation = ConversationHandler( entry_points=[ TrackingConversationHandler.build_handler(HandlerStates.END, callback=callback_1) ], states={ HandlerStates.STATE_1: [ TrackingConversationHandler.build_handler( HandlerStates.STATE_1, callback=callback_2 ) ] }, fallbacks=[], persistent=True, name="conv", block=False, ) papp.add_handler(conversation) async with papp: assert papp.persistence.updated_conversations == {} await papp.process_update( TrackingConversationHandler.build_update(HandlerStates.END, 1) ) assert papp.persistence.updated_conversations == {} await papp.update_persistence() await asyncio.sleep(0.05) assert papp.persistence.updated_conversations == {"conv": {(1, 1): 1}} # The result of the pending state wasn't retrieved by the CH yet, so we must be in # state `None` assert papp.persistence.conversations == {"conv": {(1, 1): None}} await papp.process_update( TrackingConversationHandler.build_update(HandlerStates.STATE_1, 1) ) papp.persistence.reset_tracking() await asyncio.sleep(0.01) await papp.update_persistence() assert papp.persistence.updated_conversations == {"conv": {(1, 1): 1}} # since the second callback raised an exception, the state must be the previous one! assert papp.persistence.conversations == {"conv": {(1, 1): HandlerStates.STATE_1}} async def test_non_blocking_conversations_on_stop(self, bot): papp = build_papp(token=bot.token, update_interval=100) event = asyncio.Event() async def callback(_, __): await event.wait() return HandlerStates.STATE_1 conversation = ConversationHandler( entry_points=[ TrackingConversationHandler.build_handler(HandlerStates.END, callback=callback) ], states={}, fallbacks=[], persistent=True, name="conv", block=False, ) papp.add_handler(conversation) await papp.initialize() assert papp.persistence.updated_conversations == {} await papp.start() await papp.process_update(TrackingConversationHandler.build_update(HandlerStates.END, 1)) assert papp.persistence.updated_conversations == {} stop_task = asyncio.create_task(papp.stop()) assert not stop_task.done() event.set() await asyncio.sleep(0.5) assert stop_task.done() assert papp.persistence.updated_conversations == {} await papp.shutdown() await asyncio.sleep(0.01) # The pending state must have been resolved on shutdown! assert papp.persistence.updated_conversations == {"conv": {(1, 1): 1}} assert papp.persistence.conversations == {"conv": {(1, 1): HandlerStates.STATE_1}} async def test_non_blocking_conversations_on_improper_stop(self, bot, caplog): papp = build_papp(token=bot.token, update_interval=100) event = asyncio.Event() async def callback(_, __): await event.wait() return HandlerStates.STATE_1 conversation = ConversationHandler( entry_points=[ TrackingConversationHandler.build_handler(HandlerStates.END, callback=callback) ], states={}, fallbacks=[], persistent=True, name="conv", block=False, ) papp.add_handler(conversation) await papp.initialize() assert papp.persistence.updated_conversations == {} await papp.process_update(TrackingConversationHandler.build_update(HandlerStates.END, 1)) assert papp.persistence.updated_conversations == {} with caplog.at_level(logging.WARNING): await papp.shutdown() await asyncio.sleep(0.01) # Because the app wasn't running, the pending state isn't ensured to be done on # shutdown - hence we expect the persistence to be updated with state `None` assert papp.persistence.updated_conversations == {"conv": {(1, 1): 1}} assert papp.persistence.conversations == {"conv": {(1, 1): None}} # Ensure that we warn the user about this! found_record = None for record in caplog.records: if record.getMessage().startswith("A ConversationHandlers state was not yet resolved"): assert "will check again" not in record.getMessage() assert record.name == "telegram.ext.Application" found_record = record break assert found_record is not None @default_papp async def test_conversation_ends(self, papp): async with papp: assert papp.persistence.updated_conversations == {} for state in HandlerStates: await papp.process_update(TrackingConversationHandler.build_update(state, 1)) assert papp.persistence.updated_conversations == {} await papp.update_persistence() assert papp.persistence.updated_conversations == {"conv_1": {(1, 1): 1}} # This is the important part: the persistence is updated with `None` when the conv ends assert papp.persistence.conversations == {"conv_1": {(1, 1): None}} async def test_non_blocking_conversation_ends(self, bot): papp = build_papp(token=bot.token, update_interval=100) event = asyncio.Event() async def callback(_, __): await event.wait() return HandlerStates.END conversation = ConversationHandler( entry_points=[ TrackingConversationHandler.build_handler(HandlerStates.END, callback=callback) ], states={}, fallbacks=[], persistent=True, name="conv", block=False, ) papp.add_handler(conversation) async with papp: await papp.start() assert papp.persistence.updated_conversations == {} await papp.process_update( TrackingConversationHandler.build_update(HandlerStates.END, 1) ) assert papp.persistence.updated_conversations == {} papp.persistence.reset_tracking() event.set() await asyncio.sleep(0.01) await papp.update_persistence() # On shutdown, persisted data should include the END state b/c that's what the # pending state is being resolved to assert papp.persistence.updated_conversations == {"conv": {(1, 1): 1}} assert papp.persistence.conversations == {"conv": {(1, 1): HandlerStates.END}} await papp.stop() async with papp: # On the next restart/persistence loading the ConversationHandler should resolve # the stored END state to None … assert papp.persistence.conversations == {"conv": {(1, 1): HandlerStates.END}} # … and the update should be accepted by the entry point again assert conversation.check_update( TrackingConversationHandler.build_update(HandlerStates.END, 1) ) await papp.update_persistence() assert papp.persistence.conversations == {"conv": {(1, 1): None}} async def test_conversation_timeout(self, bot): # high update_interval so that we can instead manually call it papp = build_papp(token=bot.token, update_interval=150) async def callback(_, __): return HandlerStates.STATE_1 conversation = ConversationHandler( entry_points=[ TrackingConversationHandler.build_handler(HandlerStates.END, callback=callback) ], states={HandlerStates.STATE_1: []}, fallbacks=[], persistent=True, name="conv", conversation_timeout=3, ) papp.add_handler(conversation) async with papp: await papp.start() assert papp.persistence.updated_conversations == {} await papp.process_update( TrackingConversationHandler.build_update(HandlerStates.END, 1) ) assert papp.persistence.updated_conversations == {} await papp.update_persistence() assert papp.persistence.updated_conversations == {"conv": {(1, 1): 1}} assert papp.persistence.conversations == {"conv": {(1, 1): HandlerStates.STATE_1}} papp.persistence.reset_tracking() await asyncio.sleep(4) # After the timeout the conversation should run the entry point again … assert conversation.check_update( TrackingConversationHandler.build_update(HandlerStates.END, 1) ) await papp.update_persistence() # … and persistence should be updated with `None` assert papp.persistence.updated_conversations == {"conv": {(1, 1): 1}} assert papp.persistence.conversations == {"conv": {(1, 1): None}} await papp.stop() async def test_persistent_nested_conversations(self, bot): papp = build_papp(token=bot.token, update_interval=150) def build_callback( state: HandlerStates, ): async def callback(_: Update, __: CallbackContext) -> HandlerStates: return state return callback grand_child = ConversationHandler( entry_points=[TrackingConversationHandler.build_handler(HandlerStates.END)], states={ HandlerStates.STATE_1: [ TrackingConversationHandler.build_handler( HandlerStates.STATE_1, callback=build_callback(HandlerStates.END) ) ] }, fallbacks=[], persistent=True, name="grand_child", map_to_parent={HandlerStates.END: HandlerStates.STATE_2}, ) child = ConversationHandler( entry_points=[TrackingConversationHandler.build_handler(HandlerStates.END)], states={ HandlerStates.STATE_1: [grand_child], HandlerStates.STATE_2: [ TrackingConversationHandler.build_handler(HandlerStates.STATE_2) ], }, fallbacks=[], persistent=True, name="child", map_to_parent={HandlerStates.STATE_3: HandlerStates.STATE_2}, ) parent = ConversationHandler( entry_points=[TrackingConversationHandler.build_handler(HandlerStates.END)], states={ HandlerStates.STATE_1: [child], HandlerStates.STATE_2: [ TrackingConversationHandler.build_handler( HandlerStates.STATE_2, callback=build_callback(HandlerStates.END) ) ], }, fallbacks=[], persistent=True, name="parent", ) papp.add_handler(parent) papp.persistence.conversations["grand_child"][(1, 1)] = HandlerStates.STATE_1 papp.persistence.conversations["child"][(1, 1)] = HandlerStates.STATE_1 papp.persistence.conversations["parent"][(1, 1)] = HandlerStates.STATE_1 # Should load the stored data into the persistence so that the updates below are handled # accordingly await papp.initialize() assert papp.persistence.updated_conversations == {} assert not parent.check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_2, 1) ) assert not parent.check_update( TrackingConversationHandler.build_update(HandlerStates.END, 1) ) assert parent.check_update( TrackingConversationHandler.build_update(HandlerStates.STATE_1, 1) ) await papp.process_update( TrackingConversationHandler.build_update(HandlerStates.STATE_1, 1) ) assert papp.persistence.updated_conversations == {} await papp.update_persistence() assert papp.persistence.updated_conversations == { "grand_child": {(1, 1): 1}, "child": {(1, 1): 1}, } assert papp.persistence.conversations == { "grand_child": {(1, 1): None}, "child": {(1, 1): HandlerStates.STATE_2}, "parent": {(1, 1): HandlerStates.STATE_1}, } papp.persistence.reset_tracking() await papp.process_update( TrackingConversationHandler.build_update(HandlerStates.STATE_2, 1) ) await papp.update_persistence() assert papp.persistence.updated_conversations == { "parent": {(1, 1): 1}, "child": {(1, 1): 1}, } assert papp.persistence.conversations == { "child": {(1, 1): None}, "parent": {(1, 1): HandlerStates.STATE_2}, } papp.persistence.reset_tracking() await papp.process_update( TrackingConversationHandler.build_update(HandlerStates.STATE_2, 1) ) await papp.update_persistence() assert papp.persistence.updated_conversations == { "parent": {(1, 1): 1}, } assert papp.persistence.conversations == { "parent": {(1, 1): None}, } await papp.shutdown() python-telegram-bot-21.1.1/tests/ext/test_baseupdateprocessor.py000066400000000000000000000136401460724040100251270ustar00rootroot00000000000000#!/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/]. """Here we run tests directly with SimpleUpdateProcessor because that's easier than providing dummy implementations for SimpleUpdateProcessor and we want to test SimpleUpdateProcessor anyway.""" import asyncio import pytest from telegram import Update from telegram.ext import SimpleUpdateProcessor from tests.auxil.asyncio_helpers import call_after from tests.auxil.slots import mro_slots @pytest.fixture() def mock_processor(): class MockProcessor(SimpleUpdateProcessor): test_flag = False async def do_process_update(self, update, coroutine): await coroutine self.test_flag = True return MockProcessor(5) class TestSimpleUpdateProcessor: def test_slot_behaviour(self): inst = SimpleUpdateProcessor(1) 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" @pytest.mark.parametrize("concurrent_updates", [0, -1]) def test_init(self, concurrent_updates): processor = SimpleUpdateProcessor(3) assert processor.max_concurrent_updates == 3 with pytest.raises(ValueError, match="must be a positive integer"): SimpleUpdateProcessor(concurrent_updates) async def test_process_update(self, mock_processor): """Test that process_update calls do_process_update.""" update = Update(1) async def coroutine(): pass await mock_processor.process_update(update, coroutine()) # This flag is set in the mock processor in do_process_update, telling us that # do_process_update was called. assert mock_processor.test_flag async def test_do_process_update(self): """Test that do_process_update calls the coroutine.""" processor = SimpleUpdateProcessor(1) update = Update(1) test_flag = False async def coroutine(): nonlocal test_flag test_flag = True await processor.do_process_update(update, coroutine()) assert test_flag async def test_max_concurrent_updates_enforcement(self, mock_processor): """Test that max_concurrent_updates is enforced, i.e. that the processor will run at most max_concurrent_updates coroutines at the same time.""" count = 2 * mock_processor.max_concurrent_updates events = {i: asyncio.Event() for i in range(count)} queue = asyncio.Queue() for event in events.values(): await queue.put(event) async def callback(): await asyncio.sleep(0.5) (await queue.get()).set() # We start several calls to `process_update` at the same time, each of them taking # 0.5 seconds to complete. We know that they are completed when the corresponding # event is set. tasks = [ asyncio.create_task(mock_processor.process_update(update=_, coroutine=callback())) for _ in range(count) ] # Right now we expect no event to be set for i in range(count): assert not events[i].is_set() # After 0.5 seconds (+ some buffer), we expect that exactly max_concurrent_updates # events are set. await asyncio.sleep(0.75) for i in range(mock_processor.max_concurrent_updates): assert events[i].is_set() for i in range( mock_processor.max_concurrent_updates, count, ): assert not events[i].is_set() # After wating another 0.5 seconds, we expect that the next max_concurrent_updates # events are set. await asyncio.sleep(0.5) for i in range(count): assert events[i].is_set() # Sanity check: we expect that all tasks are completed. await asyncio.gather(*tasks) async def test_context_manager(self, monkeypatch, mock_processor): self.test_flag = set() async def after_initialize(*args, **kwargs): self.test_flag.add("initialize") async def after_shutdown(*args, **kwargs): self.test_flag.add("stop") monkeypatch.setattr( SimpleUpdateProcessor, "initialize", call_after(SimpleUpdateProcessor.initialize, after_initialize), ) monkeypatch.setattr( SimpleUpdateProcessor, "shutdown", call_after(SimpleUpdateProcessor.shutdown, after_shutdown), ) async with mock_processor: pass assert self.test_flag == {"initialize", "stop"} async def test_context_manager_exception_on_init(self, monkeypatch, mock_processor): async def initialize(*args, **kwargs): raise RuntimeError("initialize") async def shutdown(*args, **kwargs): self.test_flag = "shutdown" monkeypatch.setattr(SimpleUpdateProcessor, "initialize", initialize) monkeypatch.setattr(SimpleUpdateProcessor, "shutdown", shutdown) with pytest.raises(RuntimeError, match="initialize"): async with mock_processor: pass assert self.test_flag == "shutdown" python-telegram-bot-21.1.1/tests/ext/test_businessconnectionhandler.py000066400000000000000000000147461460724040100263330ustar00rootroot00000000000000#!/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 ( Bot, BusinessConnection, CallbackQuery, Chat, ChosenInlineResult, Message, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram._utils.datetime import UTC from telegram.ext import BusinessConnectionHandler, CallbackContext, JobQueue from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, {"channel_post": message}, {"edited_channel_post": message}, {"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 = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "chosen_inline_result", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=2, **request.param) @pytest.fixture(scope="class") def time(): return datetime.datetime.now(tz=UTC) @pytest.fixture(scope="class") def business_connection(bot): bc = BusinessConnection( id="1", user_chat_id=1, user=User(1, "name", username="user_a", is_bot=False), date=datetime.datetime.now(tz=UTC), can_reply=True, is_enabled=True, ) bc.set_bot(bot) return bc @pytest.fixture() def business_connection_update(bot, business_connection): return Update(0, business_connection=business_connection) class TestBusinessConnectionHandler: test_flag = False def test_slot_behaviour(self): action = BusinessConnectionHandler(self.callback) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.user_data, dict) and isinstance(context.bot_data, dict) and isinstance( update.business_connection, BusinessConnection, ) ) def test_with_user_id(self, business_connection_update): handler = BusinessConnectionHandler(self.callback, user_id=1) assert handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, user_id=[1]) assert handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, user_id=2, username="@user_a") assert handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, user_id=2) assert not handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, user_id=[2]) assert not handler.check_update(business_connection_update) def test_with_username(self, business_connection_update): handler = BusinessConnectionHandler(self.callback, username="user_a") assert handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, username="@user_a") assert handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, username=["user_a"]) assert handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, username=["@user_a"]) assert handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, user_id=1, username="@user_b") assert handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, username="user_b") assert not handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, username="@user_b") assert not handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, username=["user_b"]) assert not handler.check_update(business_connection_update) handler = BusinessConnectionHandler(self.callback, username=["@user_b"]) assert not handler.check_update(business_connection_update) business_connection_update.business_connection.user._unfreeze() business_connection_update.business_connection.user.username = None assert not handler.check_update(business_connection_update) def test_other_update_types(self, false_update): handler = BusinessConnectionHandler(self.callback) assert not handler.check_update(false_update) assert not handler.check_update(True) async def test_context(self, app, business_connection_update): handler = BusinessConnectionHandler(callback=self.callback) app.add_handler(handler) async with app: await app.process_update(business_connection_update) assert self.test_flag python-telegram-bot-21.1.1/tests/ext/test_businessmessagesdeletedhandler.py000066400000000000000000000152731460724040100273260ustar00rootroot00000000000000#!/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 ( Bot, BusinessMessagesDeleted, CallbackQuery, Chat, ChosenInlineResult, Message, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram._utils.datetime import UTC from telegram.ext import BusinessMessagesDeletedHandler, CallbackContext, JobQueue from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, {"channel_post": message}, {"edited_channel_post": message}, {"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 = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "chosen_inline_result", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=2, **request.param) @pytest.fixture(scope="class") def time(): return datetime.datetime.now(tz=UTC) @pytest.fixture(scope="class") def business_messages_deleted(bot): bmd = BusinessMessagesDeleted( business_connection_id="1", chat=Chat(1, Chat.PRIVATE, username="user_a"), message_ids=[1, 2, 3], ) bmd.set_bot(bot) return bmd @pytest.fixture() def business_messages_deleted_update(bot, business_messages_deleted): return Update(0, deleted_business_messages=business_messages_deleted) class TestBusinessMessagesDeletedHandler: test_flag = False def test_slot_behaviour(self): action = BusinessMessagesDeletedHandler(self.callback) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.chat_data, dict) and isinstance(context.bot_data, dict) and isinstance( update.deleted_business_messages, BusinessMessagesDeleted, ) ) def test_with_chat_id(self, business_messages_deleted_update): handler = BusinessMessagesDeletedHandler(self.callback, chat_id=1) assert handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, chat_id=[1]) assert handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, chat_id=2, username="@user_a") assert handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, chat_id=2) assert not handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, chat_id=[2]) assert not handler.check_update(business_messages_deleted_update) def test_with_username(self, business_messages_deleted_update): handler = BusinessMessagesDeletedHandler(self.callback, username="user_a") assert handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, username="@user_a") assert handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, username=["user_a"]) assert handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, username=["@user_a"]) assert handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, chat_id=1, username="@user_b") assert handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, username="user_b") assert not handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, username="@user_b") assert not handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, username=["user_b"]) assert not handler.check_update(business_messages_deleted_update) handler = BusinessMessagesDeletedHandler(self.callback, username=["@user_b"]) assert not handler.check_update(business_messages_deleted_update) business_messages_deleted_update.deleted_business_messages.chat._unfreeze() business_messages_deleted_update.deleted_business_messages.chat.username = None assert not handler.check_update(business_messages_deleted_update) def test_other_update_types(self, false_update): handler = BusinessMessagesDeletedHandler(self.callback) assert not handler.check_update(false_update) assert not handler.check_update(True) async def test_context(self, app, business_messages_deleted_update): handler = BusinessMessagesDeletedHandler(callback=self.callback) app.add_handler(handler) async with app: await app.process_update(business_messages_deleted_update) assert self.test_flag python-telegram-bot-21.1.1/tests/ext/test_callbackcontext.py000066400000000000000000000234051460724040100242130ustar00rootroot00000000000000#!/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, CallbackQuery, Chat, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User, ) from telegram.error import TelegramError from telegram.ext import ApplicationBuilder, CallbackContext, Job from telegram.warnings import PTBUserWarning from tests.auxil.pytest_classes import make_bot from tests.auxil.slots import mro_slots """ CallbackContext.refresh_data is tested in TestBasePersistence """ class TestCallbackContext: def test_slot_behaviour(self, app): c = CallbackContext(app) for attr in c.__slots__: assert getattr(c, attr, "err") != "err", f"got extra slot '{attr}'" assert not c.__dict__, f"got missing slot(s): {c.__dict__}" assert len(mro_slots(c)) == len(set(mro_slots(c))), "duplicate slot" def test_from_job(self, app): job = app.job_queue.run_once(lambda x: x, 10) callback_context = CallbackContext.from_job(job, app) assert callback_context.job is job assert callback_context.chat_data is None assert callback_context.user_data is None assert callback_context.bot_data is app.bot_data assert callback_context.bot is app.bot assert callback_context.job_queue is app.job_queue assert callback_context.update_queue is app.update_queue def test_job_queue(self, bot, app, recwarn): expected_warning = ( "No `JobQueue` set up. To use `JobQueue`, you must install PTB via " '`pip install "python-telegram-bot[job-queue]"`.' ) callback_context = CallbackContext(app) assert callback_context.job_queue is app.job_queue app = ApplicationBuilder().job_queue(None).token(bot.token).build() callback_context = CallbackContext(app) assert callback_context.job_queue is None 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_from_update(self, app): update = Update( 0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False)) ) callback_context = CallbackContext.from_update(update, app) assert callback_context.chat_data == {} assert callback_context.user_data == {} assert callback_context.bot_data is app.bot_data assert callback_context.bot is app.bot assert callback_context.job_queue is app.job_queue assert callback_context.update_queue is app.update_queue callback_context_same_user_chat = CallbackContext.from_update(update, app) callback_context.bot_data["test"] = "bot" callback_context.chat_data["test"] = "chat" callback_context.user_data["test"] = "user" assert callback_context_same_user_chat.bot_data is callback_context.bot_data assert callback_context_same_user_chat.chat_data is callback_context.chat_data assert callback_context_same_user_chat.user_data is callback_context.user_data update_other_user_chat = Update( 0, message=Message(0, None, Chat(2, "chat"), from_user=User(2, "user", False)) ) callback_context_other_user_chat = CallbackContext.from_update(update_other_user_chat, app) assert callback_context_other_user_chat.bot_data is callback_context.bot_data assert callback_context_other_user_chat.chat_data is not callback_context.chat_data assert callback_context_other_user_chat.user_data is not callback_context.user_data def test_from_update_not_update(self, app): callback_context = CallbackContext.from_update(None, app) assert callback_context.chat_data is None assert callback_context.user_data is None assert callback_context.bot_data is app.bot_data assert callback_context.bot is app.bot assert callback_context.job_queue is app.job_queue assert callback_context.update_queue is app.update_queue callback_context = CallbackContext.from_update("", app) assert callback_context.chat_data is None assert callback_context.user_data is None assert callback_context.bot_data is app.bot_data assert callback_context.bot is app.bot assert callback_context.job_queue is app.job_queue assert callback_context.update_queue is app.update_queue def test_from_error(self, app): error = TelegramError("test") update = Update( 0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False)) ) coroutine = object() callback_context = CallbackContext.from_error( update=update, error=error, application=app, coroutine=coroutine ) assert callback_context.error is error assert callback_context.chat_data == {} assert callback_context.user_data == {} assert callback_context.bot_data is app.bot_data assert callback_context.bot is app.bot assert callback_context.job_queue is app.job_queue assert callback_context.update_queue is app.update_queue assert callback_context.coroutine is coroutine def test_from_error_job_user_chat_data(self, app): error = TelegramError("test") job = Job(callback=lambda x: x, data=None, chat_id=42, user_id=43) callback_context = CallbackContext.from_error( update=None, error=error, application=app, job=job ) assert callback_context.error is error assert callback_context.chat_data == {} assert callback_context.user_data == {} assert callback_context.bot_data is app.bot_data assert callback_context.bot is app.bot assert callback_context.job_queue is app.job_queue assert callback_context.update_queue is app.update_queue assert callback_context.job is job def test_match(self, app): callback_context = CallbackContext(app) assert callback_context.match is None callback_context.matches = ["test", "blah"] assert callback_context.match == "test" def test_data_assignment(self, app): update = Update( 0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False)) ) callback_context = CallbackContext.from_update(update, app) with pytest.raises(AttributeError): callback_context.bot_data = {"test": 123} with pytest.raises(AttributeError): callback_context.user_data = {} with pytest.raises(AttributeError): callback_context.chat_data = "test" def test_application_attribute(self, app): callback_context = CallbackContext(app) assert callback_context.application is app def test_drop_callback_data_exception(self, bot, app): non_ext_bot = Bot(bot.token) update = Update( 0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False)) ) callback_context = CallbackContext.from_update(update, app) with pytest.raises(RuntimeError, match="This telegram.ext.ExtBot instance does not"): callback_context.drop_callback_data(None) try: app.bot = non_ext_bot with pytest.raises(RuntimeError, match="telegram.Bot does not allow for"): callback_context.drop_callback_data(None) finally: app.bot = bot async def test_drop_callback_data(self, bot, chat_id): new_bot = make_bot(token=bot.token, arbitrary_callback_data=True) app = ApplicationBuilder().bot(new_bot).build() update = Update( 0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False)) ) callback_context = CallbackContext.from_update(update, app) async with app: await app.bot.send_message( chat_id=chat_id, text="test", reply_markup=InlineKeyboardMarkup.from_button( InlineKeyboardButton("test", callback_data="callback_data") ), ) keyboard_uuid = app.bot.callback_data_cache.persistence_data[0][0][0] button_uuid = next(iter(app.bot.callback_data_cache.persistence_data[0][0][2])) callback_data = keyboard_uuid + button_uuid callback_query = CallbackQuery( id="1", from_user=None, chat_instance=None, data=callback_data, ) app.bot.callback_data_cache.process_callback_query(callback_query) try: assert len(app.bot.callback_data_cache.persistence_data[0]) == 1 assert list(app.bot.callback_data_cache.persistence_data[1]) == ["1"] callback_context.drop_callback_data(callback_query) assert app.bot.callback_data_cache.persistence_data == ([], {}) finally: app.bot.callback_data_cache.clear_callback_data() app.bot.callback_data_cache.clear_callback_queries() python-telegram-bot-21.1.1/tests/ext/test_callbackdatacache.py000066400000000000000000000430061460724040100244230ustar00rootroot00000000000000#!/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 from uuid import uuid4 import pytest from telegram import CallbackQuery, Chat, InlineKeyboardButton, InlineKeyboardMarkup, Message, User from telegram._utils.datetime import UTC from telegram.ext import ExtBot from telegram.ext._callbackdatacache import CallbackDataCache, InvalidCallbackData, _KeyboardData from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.slots import mro_slots @pytest.fixture() def callback_data_cache(bot): return CallbackDataCache(bot) @pytest.mark.skipif( TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is not installed", ) class TestNoCallbackDataCache: def test_init(self, bot): with pytest.raises(RuntimeError, match=r"python-telegram-bot\[callback-data\]"): CallbackDataCache(bot=bot) def test_bot_init(self): bot = ExtBot(token="TOKEN") assert bot.callback_data_cache is None with pytest.raises(RuntimeError, match=r"python-telegram-bot\[callback-data\]"): ExtBot(token="TOKEN", arbitrary_callback_data=True) class TestInvalidCallbackData: def test_slot_behaviour(self): invalid_callback_data = InvalidCallbackData() for attr in invalid_callback_data.__slots__: assert getattr(invalid_callback_data, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(invalid_callback_data)) == len( set(mro_slots(invalid_callback_data)) ), "duplicate slot" class TestKeyboardData: def test_slot_behaviour(self): keyboard_data = _KeyboardData("uuid") for attr in keyboard_data.__slots__: assert getattr(keyboard_data, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(keyboard_data)) == len( set(mro_slots(keyboard_data)) ), "duplicate slot" @pytest.mark.skipif( not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed", ) class TestCallbackDataCache: def test_slot_behaviour(self, callback_data_cache): for attr in callback_data_cache.__slots__: at = ( f"_CallbackDataCache{attr}" if attr.startswith("__") and not attr.endswith("__") else attr ) assert getattr(callback_data_cache, at, "err") != "err", f"got extra slot '{at}'" assert len(mro_slots(callback_data_cache)) == len( set(mro_slots(callback_data_cache)) ), "duplicate slot" @pytest.mark.parametrize("maxsize", [1, 5, 2048]) def test_init_maxsize(self, maxsize, bot): assert CallbackDataCache(bot).maxsize == 1024 cdc = CallbackDataCache(bot, maxsize=maxsize) assert cdc.maxsize == maxsize assert cdc.bot is bot def test_init_and_access__persistent_data(self, bot): """This also tests CDC.load_persistent_data.""" keyboard_data = _KeyboardData("123", 456, {"button": 678}) persistent_data = ([keyboard_data.to_tuple()], {"id": "123"}) cdc = CallbackDataCache(bot, persistent_data=persistent_data) assert cdc.maxsize == 1024 assert dict(cdc._callback_queries) == {"id": "123"} assert list(cdc._keyboard_data.keys()) == ["123"] assert cdc._keyboard_data["123"].keyboard_uuid == "123" assert cdc._keyboard_data["123"].access_time == 456 assert cdc._keyboard_data["123"].button_data == {"button": 678} assert cdc.persistence_data == persistent_data def test_process_keyboard(self, callback_data_cache): changing_button_1 = InlineKeyboardButton("changing", callback_data="some data 1") changing_button_2 = InlineKeyboardButton("changing", callback_data="some data 2") non_changing_button = InlineKeyboardButton("non-changing", url="https://ptb.org") reply_markup = InlineKeyboardMarkup.from_row( [non_changing_button, changing_button_1, changing_button_2] ) out = callback_data_cache.process_keyboard(reply_markup) assert out.inline_keyboard[0][0] is non_changing_button assert out.inline_keyboard[0][1] != changing_button_1 assert out.inline_keyboard[0][2] != changing_button_2 keyboard_1, button_1 = callback_data_cache.extract_uuids( out.inline_keyboard[0][1].callback_data ) keyboard_2, button_2 = callback_data_cache.extract_uuids( out.inline_keyboard[0][2].callback_data ) assert keyboard_1 == keyboard_2 assert ( callback_data_cache._keyboard_data[keyboard_1].button_data[button_1] == "some data 1" ) assert ( callback_data_cache._keyboard_data[keyboard_2].button_data[button_2] == "some data 2" ) def test_process_keyboard_no_changing_button(self, callback_data_cache): reply_markup = InlineKeyboardMarkup.from_button( InlineKeyboardButton("non-changing", url="https://ptb.org") ) assert callback_data_cache.process_keyboard(reply_markup) is reply_markup def test_process_keyboard_full(self, bot): cdc = CallbackDataCache(bot, maxsize=1) changing_button_1 = InlineKeyboardButton("changing", callback_data="some data 1") changing_button_2 = InlineKeyboardButton("changing", callback_data="some data 2") non_changing_button = InlineKeyboardButton("non-changing", url="https://ptb.org") reply_markup = InlineKeyboardMarkup.from_row( [non_changing_button, changing_button_1, changing_button_2] ) out1 = cdc.process_keyboard(reply_markup) assert len(cdc.persistence_data[0]) == 1 out2 = cdc.process_keyboard(reply_markup) assert len(cdc.persistence_data[0]) == 1 keyboard_1, _ = cdc.extract_uuids(out1.inline_keyboard[0][1].callback_data) keyboard_2, _ = cdc.extract_uuids(out2.inline_keyboard[0][2].callback_data) assert cdc.persistence_data[0][0][0] != keyboard_1 assert cdc.persistence_data[0][0][0] == keyboard_2 @pytest.mark.parametrize("data", [True, False]) @pytest.mark.parametrize("message", [True, False]) @pytest.mark.parametrize("invalid", [True, False]) def test_process_callback_query(self, callback_data_cache, data, message, invalid): """This also tests large parts of process_message""" changing_button_1 = InlineKeyboardButton("changing", callback_data="some data 1") changing_button_2 = InlineKeyboardButton("changing", callback_data="some data 2") non_changing_button = InlineKeyboardButton("non-changing", url="https://ptb.org") reply_markup = InlineKeyboardMarkup.from_row( [non_changing_button, changing_button_1, changing_button_2] ) out = callback_data_cache.process_keyboard(reply_markup) if invalid: callback_data_cache.clear_callback_data() chat = Chat(1, "private") effective_message = Message(message_id=1, date=datetime.now(), chat=chat, reply_markup=out) effective_message._unfreeze() effective_message.reply_to_message = deepcopy(effective_message) effective_message.pinned_message = deepcopy(effective_message) cq_id = uuid4().hex callback_query = CallbackQuery( cq_id, from_user=None, chat_instance=None, # not all CallbackQueries have callback_data data=out.inline_keyboard[0][1].callback_data if data else None, # CallbackQueries from inline messages don't have the message attached, so we test that message=effective_message if message else None, ) callback_data_cache.process_callback_query(callback_query) if not invalid: if data: assert callback_query.data == "some data 1" # make sure that we stored the mapping CallbackQuery.id -> keyboard_uuid correctly assert len(callback_data_cache._keyboard_data) == 1 assert callback_data_cache._callback_queries[cq_id] == next( iter(callback_data_cache._keyboard_data.keys()) ) else: assert callback_query.data is None if message: for msg in ( callback_query.message, callback_query.message.reply_to_message, callback_query.message.pinned_message, ): assert msg.reply_markup == reply_markup else: if data: assert isinstance(callback_query.data, InvalidCallbackData) else: assert callback_query.data is None if message: for msg in ( callback_query.message, callback_query.message.reply_to_message, callback_query.message.pinned_message, ): assert isinstance( msg.reply_markup.inline_keyboard[0][1].callback_data, InvalidCallbackData, ) assert isinstance( msg.reply_markup.inline_keyboard[0][2].callback_data, InvalidCallbackData, ) @pytest.mark.parametrize("pass_from_user", [True, False]) @pytest.mark.parametrize("pass_via_bot", [True, False]) def test_process_message_wrong_sender(self, pass_from_user, pass_via_bot, callback_data_cache): reply_markup = InlineKeyboardMarkup.from_button( InlineKeyboardButton("test", callback_data="callback_data") ) user = User(1, "first", False) message = Message( 1, None, None, from_user=user if pass_from_user else None, via_bot=user if pass_via_bot else None, reply_markup=reply_markup, ) callback_data_cache.process_message(message) if pass_from_user or pass_via_bot: # Here we can determine that the message is not from our bot, so no replacing assert message.reply_markup.inline_keyboard[0][0].callback_data == "callback_data" else: # Here we have no chance to know, so InvalidCallbackData assert isinstance( message.reply_markup.inline_keyboard[0][0].callback_data, InvalidCallbackData ) @pytest.mark.parametrize("pass_from_user", [True, False]) def test_process_message_inline_mode(self, pass_from_user, callback_data_cache): """Check that via_bot tells us correctly that our bot sent the message, even if from_user is not our bot.""" reply_markup = InlineKeyboardMarkup.from_button( InlineKeyboardButton("test", callback_data="callback_data") ) user = User(1, "first", False) message = Message( 1, None, None, from_user=user if pass_from_user else None, via_bot=callback_data_cache.bot.bot, reply_markup=callback_data_cache.process_keyboard(reply_markup), ) callback_data_cache.process_message(message) # Here we can determine that the message is not from our bot, so no replacing assert message.reply_markup.inline_keyboard[0][0].callback_data == "callback_data" def test_process_message_no_reply_markup(self, callback_data_cache): message = Message(1, None, None) callback_data_cache.process_message(message) assert message.reply_markup is None def test_drop_data(self, callback_data_cache): changing_button_1 = InlineKeyboardButton("changing", callback_data="some data 1") changing_button_2 = InlineKeyboardButton("changing", callback_data="some data 2") reply_markup = InlineKeyboardMarkup.from_row([changing_button_1, changing_button_2]) out = callback_data_cache.process_keyboard(reply_markup) callback_query = CallbackQuery( "1", from_user=None, chat_instance=None, data=out.inline_keyboard[0][1].callback_data, ) callback_data_cache.process_callback_query(callback_query) assert len(callback_data_cache.persistence_data[1]) == 1 assert len(callback_data_cache.persistence_data[0]) == 1 callback_data_cache.drop_data(callback_query) assert len(callback_data_cache.persistence_data[1]) == 0 assert len(callback_data_cache.persistence_data[0]) == 0 def test_drop_data_missing_data(self, callback_data_cache): changing_button_1 = InlineKeyboardButton("changing", callback_data="some data 1") changing_button_2 = InlineKeyboardButton("changing", callback_data="some data 2") reply_markup = InlineKeyboardMarkup.from_row([changing_button_1, changing_button_2]) out = callback_data_cache.process_keyboard(reply_markup) callback_query = CallbackQuery( "1", from_user=None, chat_instance=None, data=out.inline_keyboard[0][1].callback_data, ) with pytest.raises(KeyError, match="CallbackQuery was not found in cache."): callback_data_cache.drop_data(callback_query) callback_data_cache.process_callback_query(callback_query) callback_data_cache.clear_callback_data() callback_data_cache.drop_data(callback_query) assert callback_data_cache.persistence_data == ([], {}) @pytest.mark.parametrize("method", ["callback_data", "callback_queries"]) def test_clear_all(self, callback_data_cache, method): changing_button_1 = InlineKeyboardButton("changing", callback_data="some data 1") changing_button_2 = InlineKeyboardButton("changing", callback_data="some data 2") reply_markup = InlineKeyboardMarkup.from_row([changing_button_1, changing_button_2]) for i in range(100): out = callback_data_cache.process_keyboard(reply_markup) callback_query = CallbackQuery( str(i), from_user=None, chat_instance=None, data=out.inline_keyboard[0][1].callback_data, ) callback_data_cache.process_callback_query(callback_query) if method == "callback_data": callback_data_cache.clear_callback_data() # callback_data was cleared, callback_queries weren't assert len(callback_data_cache.persistence_data[0]) == 0 assert len(callback_data_cache.persistence_data[1]) == 100 else: callback_data_cache.clear_callback_queries() # callback_queries were cleared, callback_data wasn't assert len(callback_data_cache.persistence_data[0]) == 100 assert len(callback_data_cache.persistence_data[1]) == 0 @pytest.mark.parametrize("time_method", ["time", "datetime", "defaults"]) def test_clear_cutoff(self, callback_data_cache, time_method, tz_bot): # Fill the cache with some fake data for i in range(50): reply_markup = InlineKeyboardMarkup.from_button( InlineKeyboardButton("changing", callback_data=str(i)) ) out = callback_data_cache.process_keyboard(reply_markup) callback_query = CallbackQuery( str(i), from_user=None, chat_instance=None, data=out.inline_keyboard[0][0].callback_data, ) callback_data_cache.process_callback_query(callback_query) # sleep a bit before saving the time cutoff, to make test more reliable time.sleep(0.1) if time_method == "time": cutoff = time.time() elif time_method == "datetime": cutoff = datetime.now(UTC) else: cutoff = datetime.now(tz_bot.defaults.tzinfo).replace(tzinfo=None) callback_data_cache.bot = tz_bot time.sleep(0.1) # more fake data after the time cutoff for i in range(50, 100): reply_markup = InlineKeyboardMarkup.from_button( InlineKeyboardButton("changing", callback_data=str(i)) ) out = callback_data_cache.process_keyboard(reply_markup) callback_query = CallbackQuery( str(i), from_user=None, chat_instance=None, data=out.inline_keyboard[0][0].callback_data, ) callback_data_cache.process_callback_query(callback_query) callback_data_cache.clear_callback_data(time_cutoff=cutoff) assert len(callback_data_cache.persistence_data[0]) == 50 assert len(callback_data_cache.persistence_data[1]) == 100 callback_data = [ next(iter(data[2].values())) for data in callback_data_cache.persistence_data[0] ] assert callback_data == [str(i) for i in range(50, 100)] python-telegram-bot-21.1.1/tests/ext/test_callbackqueryhandler.py000066400000000000000000000201331460724040100252250ustar00rootroot00000000000000#!/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 pytest from telegram import ( Bot, CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram.ext import CallbackContext, CallbackQueryHandler, InvalidCallbackData, JobQueue from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_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, "")}, ] ids = ( "message", "edited_message", "channel_post", "edited_channel_post", "inline_query", "chosen_inline_result", "shipping_query", "pre_checkout_query", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=2, **request.param) @pytest.fixture() def callback_query(bot): update = Update(0, callback_query=CallbackQuery(2, User(1, "", False), None, data="test data")) update._unfreeze() update.callback_query._unfreeze() return update class TestCallbackQueryHandler: test_flag = False def test_slot_behaviour(self): handler = CallbackQueryHandler(self.callback_data_1) for attr in handler.__slots__: assert getattr(handler, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False def callback_basic(self, update, context): test_bot = isinstance(context.bot, Bot) test_update = isinstance(update, Update) self.test_flag = test_bot and test_update def callback_data_1(self, bot, update, user_data=None, chat_data=None): self.test_flag = (user_data is not None) or (chat_data is not None) def callback_data_2(self, bot, update, user_data=None, chat_data=None): self.test_flag = (user_data is not None) and (chat_data is not None) def callback_queue_1(self, bot, update, job_queue=None, update_queue=None): self.test_flag = (job_queue is not None) or (update_queue is not None) def callback_queue_2(self, bot, update, job_queue=None, update_queue=None): self.test_flag = (job_queue is not None) and (update_queue is not None) def callback_group(self, bot, update, groups=None, groupdict=None): if groups is not None: self.test_flag = groups == ("t", " data") if groupdict is not None: self.test_flag = groupdict == {"begin": "t", "end": " data"} async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.user_data, dict) and context.chat_data is None and isinstance(context.bot_data, dict) and isinstance(update.callback_query, CallbackQuery) ) def callback_pattern(self, update, context): if context.matches[0].groups(): self.test_flag = context.matches[0].groups() == ("t", " data") if context.matches[0].groupdict(): self.test_flag = context.matches[0].groupdict() == {"begin": "t", "end": " data"} def test_with_pattern(self, callback_query): handler = CallbackQueryHandler(self.callback_basic, pattern=".*est.*") assert handler.check_update(callback_query) callback_query.callback_query.data = "nothing here" assert not handler.check_update(callback_query) callback_query.callback_query.data = None callback_query.callback_query.game_short_name = "this is a short game name" assert not handler.check_update(callback_query) callback_query.callback_query.data = object() assert not handler.check_update(callback_query) callback_query.callback_query.data = InvalidCallbackData() assert not handler.check_update(callback_query) def test_with_callable_pattern(self, callback_query): class CallbackData: pass def pattern(callback_data): return isinstance(callback_data, CallbackData) handler = CallbackQueryHandler(self.callback_basic, pattern=pattern) callback_query.callback_query.data = CallbackData() assert handler.check_update(callback_query) callback_query.callback_query.data = "callback_data" assert not handler.check_update(callback_query) def test_with_type_pattern(self, callback_query): class CallbackData: pass handler = CallbackQueryHandler(self.callback_basic, pattern=CallbackData) callback_query.callback_query.data = CallbackData() assert handler.check_update(callback_query) callback_query.callback_query.data = "callback_data" assert not handler.check_update(callback_query) handler = CallbackQueryHandler(self.callback_basic, pattern=bool) callback_query.callback_query.data = False assert handler.check_update(callback_query) callback_query.callback_query.data = "callback_data" assert not handler.check_update(callback_query) def test_other_update_types(self, false_update): handler = CallbackQueryHandler(self.callback_basic) assert not handler.check_update(false_update) async def test_context(self, app, callback_query): handler = CallbackQueryHandler(self.callback) app.add_handler(handler) async with app: await app.process_update(callback_query) assert self.test_flag async def test_context_pattern(self, app, callback_query): handler = CallbackQueryHandler( self.callback_pattern, pattern=r"(?P.*)est(?P.*)" ) app.add_handler(handler) async with app: await app.process_update(callback_query) assert self.test_flag app.remove_handler(handler) handler = CallbackQueryHandler(self.callback_pattern, pattern=r"(t)est(.*)") app.add_handler(handler) await app.process_update(callback_query) assert self.test_flag async def test_context_callable_pattern(self, app, callback_query): class CallbackData: pass def pattern(callback_data): return isinstance(callback_data, CallbackData) def callback(update, context): assert context.matches is None handler = CallbackQueryHandler(callback, pattern=pattern) app.add_handler(handler) async with app: await app.process_update(callback_query) def test_async_pattern(self): async def pattern(): pass with pytest.raises(TypeError, match="must not be a coroutine function"): CallbackQueryHandler(self.callback, pattern=pattern) python-telegram-bot-21.1.1/tests/ext/test_chatboosthandler.py000066400000000000000000000172271460724040100244030ustar00rootroot00000000000000#!/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 import pytest from telegram import ( Chat, ChatBoost, ChatBoostRemoved, ChatBoostSourcePremium, ChatBoostUpdated, Update, User, ) from telegram._utils.datetime import from_timestamp from telegram.ext import CallbackContext, ChatBoostHandler from tests.auxil.slots import mro_slots from tests.test_update import all_types as really_all_types from tests.test_update import params as all_params # Remove "chat_boost" from params params = [param for param in all_params for key in param if "chat_boost" not in key] all_types = [param for param in really_all_types if "chat_boost" not in param] ids = (*all_types, "callback_query_without_message") @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=2, **request.param) def chat_boost(): return ChatBoost( "1", from_timestamp(int(time.time())), from_timestamp(int(time.time())), ChatBoostSourcePremium( User(1, "first_name", False), ), ) @pytest.fixture(scope="module") def removed_chat_boost(): return ChatBoostRemoved( Chat(1, "group", username="chat"), "1", from_timestamp(int(time.time())), ChatBoostSourcePremium( User(1, "first_name", False), ), ) def removed_chat_boost_update(): return Update( update_id=2, removed_chat_boost=ChatBoostRemoved( Chat(1, "group", username="chat"), "1", from_timestamp(int(time.time())), ChatBoostSourcePremium( User(1, "first_name", False), ), ), ) @pytest.fixture(scope="module") def chat_boost_updated(): return ChatBoostUpdated(Chat(1, "group", username="chat"), chat_boost()) def chat_boost_updated_update(): return Update( update_id=2, chat_boost=ChatBoostUpdated( Chat(1, "group", username="chat"), chat_boost(), ), ) class TestChatBoostHandler: test_flag = False def test_slot_behaviour(self): action = ChatBoostHandler(self.cb_chat_boost_removed) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def cb_chat_boost_updated(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(update.chat_boost, ChatBoostUpdated) and not isinstance(update.removed_chat_boost, ChatBoostRemoved) ) async def cb_chat_boost_removed(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(update.removed_chat_boost, ChatBoostRemoved) and not isinstance(update.chat_boost, ChatBoostUpdated) ) async def cb_chat_boost_any(self, update, context): self.test_flag = isinstance(context, CallbackContext) and ( isinstance(update.removed_chat_boost, ChatBoostRemoved) or isinstance(update.chat_boost, ChatBoostUpdated) ) @pytest.mark.parametrize( argnames=["allowed_types", "cb", "expected"], argvalues=[ (ChatBoostHandler.CHAT_BOOST, "cb_chat_boost_updated", (True, False)), (ChatBoostHandler.REMOVED_CHAT_BOOST, "cb_chat_boost_removed", (False, True)), (ChatBoostHandler.ANY_CHAT_BOOST, "cb_chat_boost_any", (True, True)), ], ids=["CHAT_BOOST", "REMOVED_CHAT_BOOST", "ANY_CHAT_MEMBER"], ) async def test_chat_boost_types(self, app, cb, expected, allowed_types): result_1, result_2 = expected update_type, other = chat_boost_updated_update(), removed_chat_boost_update() handler = ChatBoostHandler(getattr(self, cb), chat_boost_types=allowed_types) app.add_handler(handler) async with app: assert handler.check_update(update_type) == result_1 await app.process_update(update_type) assert self.test_flag == result_1 self.test_flag = False assert handler.check_update(other) == result_2 await app.process_update(other) assert self.test_flag == result_2 def test_other_update_types(self, false_update): handler = ChatBoostHandler(self.cb_chat_boost_removed) assert not handler.check_update(false_update) assert not handler.check_update(True) async def test_context(self, app): handler = ChatBoostHandler(self.cb_chat_boost_updated) app.add_handler(handler) async with app: await app.process_update(chat_boost_updated_update()) assert self.test_flag def test_with_chat_id(self): update = chat_boost_updated_update() cb = self.cb_chat_boost_updated handler = ChatBoostHandler(cb, chat_id=1) assert handler.check_update(update) handler = ChatBoostHandler(cb, chat_id=[1]) assert handler.check_update(update) handler = ChatBoostHandler(cb, chat_id=2, chat_username="@chat") assert handler.check_update(update) handler = ChatBoostHandler(cb, chat_id=2) assert not handler.check_update(update) handler = ChatBoostHandler(cb, chat_id=[2]) assert not handler.check_update(update) def test_with_username(self): update = removed_chat_boost_update() cb = self.cb_chat_boost_removed handler = ChatBoostHandler(cb, chat_boost_types=0, chat_username="chat") assert handler.check_update(update) handler = ChatBoostHandler(cb, chat_boost_types=0, chat_username="@chat") assert handler.check_update(update) handler = ChatBoostHandler(cb, chat_boost_types=0, chat_username=["chat"]) assert handler.check_update(update) handler = ChatBoostHandler(cb, chat_boost_types=0, chat_username=["@chat"]) assert handler.check_update(update) handler = ChatBoostHandler( cb, chat_boost_types=0, chat_id=1, chat_username="@chat_something" ) assert handler.check_update(update) handler = ChatBoostHandler(cb, chat_boost_types=0, chat_username="chat_b") assert not handler.check_update(update) handler = ChatBoostHandler(cb, chat_boost_types=0, chat_username="@chat_b") assert not handler.check_update(update) handler = ChatBoostHandler(cb, chat_boost_types=0, chat_username=["chat_b"]) assert not handler.check_update(update) handler = ChatBoostHandler(cb, chat_boost_types=0, chat_username=["@chat_b"]) assert not handler.check_update(update) update.removed_chat_boost.chat._unfreeze() update.removed_chat_boost.chat.username = None assert not handler.check_update(update) python-telegram-bot-21.1.1/tests/ext/test_chatjoinrequesthandler.py000066400000000000000000000152461460724040100256240ustar00rootroot00000000000000#!/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 ( Bot, CallbackQuery, Chat, ChatInviteLink, ChatJoinRequest, ChosenInlineResult, Message, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram._utils.datetime import UTC from telegram.ext import CallbackContext, ChatJoinRequestHandler, JobQueue from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, {"channel_post": message}, {"edited_channel_post": message}, {"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 = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "chosen_inline_result", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=2, **request.param) @pytest.fixture(scope="class") def time(): return datetime.datetime.now(tz=UTC) @pytest.fixture(scope="class") def chat_join_request(time, bot): cjr = ChatJoinRequest( chat=Chat(1, Chat.SUPERGROUP), from_user=User(2, "first_name", False, username="user_a"), date=time, bio="bio", invite_link=ChatInviteLink( "https://invite.link", User(42, "creator", False), creates_join_request=False, name="InviteLink", is_revoked=False, is_primary=False, ), user_chat_id=2, ) cjr.set_bot(bot) return cjr @pytest.fixture() def chat_join_request_update(bot, chat_join_request): return Update(0, chat_join_request=chat_join_request) class TestChatJoinRequestHandler: test_flag = False def test_slot_behaviour(self): action = ChatJoinRequestHandler(self.callback) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.user_data, dict) and isinstance(context.chat_data, dict) and isinstance(context.bot_data, dict) and isinstance( update.chat_join_request, ChatJoinRequest, ) ) def test_with_chat_id(self, chat_join_request_update): handler = ChatJoinRequestHandler(self.callback, chat_id=1) assert handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, chat_id=[1]) assert handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, chat_id=2, username="@user_a") assert handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, chat_id=2) assert not handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, chat_id=[2]) assert not handler.check_update(chat_join_request_update) def test_with_username(self, chat_join_request_update): handler = ChatJoinRequestHandler(self.callback, username="user_a") assert handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, username="@user_a") assert handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, username=["user_a"]) assert handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, username=["@user_a"]) assert handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, chat_id=1, username="@user_b") assert handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, username="user_b") assert not handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, username="@user_b") assert not handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, username=["user_b"]) assert not handler.check_update(chat_join_request_update) handler = ChatJoinRequestHandler(self.callback, username=["@user_b"]) assert not handler.check_update(chat_join_request_update) chat_join_request_update.chat_join_request.from_user._unfreeze() chat_join_request_update.chat_join_request.from_user.username = None assert not handler.check_update(chat_join_request_update) def test_other_update_types(self, false_update): handler = ChatJoinRequestHandler(self.callback) assert not handler.check_update(false_update) assert not handler.check_update(True) async def test_context(self, app, chat_join_request_update): handler = ChatJoinRequestHandler(callback=self.callback) app.add_handler(handler) async with app: await app.process_update(chat_join_request_update) assert self.test_flag python-telegram-bot-21.1.1/tests/ext/test_chatmemberhandler.py000066400000000000000000000122401460724040100245120ustar00rootroot00000000000000#!/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 time import pytest from telegram import ( Bot, CallbackQuery, Chat, ChatMember, ChatMemberUpdated, ChosenInlineResult, Message, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram._utils.datetime import from_timestamp from telegram.ext import CallbackContext, ChatMemberHandler, JobQueue from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, {"channel_post": message}, {"edited_channel_post": message}, {"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 = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "chosen_inline_result", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=2, **request.param) @pytest.fixture(scope="class") def chat_member_updated(): return ChatMemberUpdated( Chat(1, "chat"), User(1, "", False), from_timestamp(int(time.time())), ChatMember(User(1, "", False), ChatMember.OWNER), ChatMember(User(1, "", False), ChatMember.OWNER), ) @pytest.fixture() def chat_member(bot, chat_member_updated): update = Update(0, my_chat_member=chat_member_updated) update._unfreeze() return update class TestChatMemberHandler: test_flag = False def test_slot_behaviour(self): action = ChatMemberHandler(self.callback) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.user_data, dict) and isinstance(context.chat_data, dict) and isinstance(context.bot_data, dict) and isinstance(update.chat_member or update.my_chat_member, ChatMemberUpdated) ) @pytest.mark.parametrize( argnames=["allowed_types", "expected"], argvalues=[ (ChatMemberHandler.MY_CHAT_MEMBER, (True, False)), (ChatMemberHandler.CHAT_MEMBER, (False, True)), (ChatMemberHandler.ANY_CHAT_MEMBER, (True, True)), ], ids=["MY_CHAT_MEMBER", "CHAT_MEMBER", "ANY_CHAT_MEMBER"], ) async def test_chat_member_types( self, app, chat_member_updated, chat_member, expected, allowed_types ): result_1, result_2 = expected handler = ChatMemberHandler(self.callback, chat_member_types=allowed_types) app.add_handler(handler) async with app: assert handler.check_update(chat_member) == result_1 await app.process_update(chat_member) assert self.test_flag == result_1 self.test_flag = False chat_member.my_chat_member = None chat_member.chat_member = chat_member_updated assert handler.check_update(chat_member) == result_2 await app.process_update(chat_member) assert self.test_flag == result_2 def test_other_update_types(self, false_update): handler = ChatMemberHandler(self.callback) assert not handler.check_update(false_update) assert not handler.check_update(True) async def test_context(self, app, chat_member): handler = ChatMemberHandler(self.callback) app.add_handler(handler) async with app: await app.process_update(chat_member) assert self.test_flag python-telegram-bot-21.1.1/tests/ext/test_choseninlineresulthandler.py000066400000000000000000000135531460724040100263300ustar00rootroot00000000000000#!/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 pytest from telegram import ( Bot, CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram.ext import CallbackContext, ChosenInlineResultHandler, JobQueue from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, {"channel_post": message}, {"edited_channel_post": message}, {"inline_query": InlineQuery(1, 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 = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "inline_query", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=1, **request.param) @pytest.fixture(scope="class") def chosen_inline_result(): out = Update( 1, chosen_inline_result=ChosenInlineResult("result_id", User(1, "test_user", False), "query"), ) out._unfreeze() out.chosen_inline_result._unfreeze() return out class TestChosenInlineResultHandler: test_flag = False @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False def test_slot_behaviour(self): handler = ChosenInlineResultHandler(self.callback_basic) for attr in handler.__slots__: assert getattr(handler, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" def callback_basic(self, update, context): test_bot = isinstance(context.bot, Bot) test_update = isinstance(update, Update) self.test_flag = test_bot and test_update def callback_data_1(self, bot, update, user_data=None, chat_data=None): self.test_flag = (user_data is not None) or (chat_data is not None) def callback_data_2(self, bot, update, user_data=None, chat_data=None): self.test_flag = (user_data is not None) and (chat_data is not None) def callback_queue_1(self, bot, update, job_queue=None, update_queue=None): self.test_flag = (job_queue is not None) or (update_queue is not None) def callback_queue_2(self, bot, update, job_queue=None, update_queue=None): self.test_flag = (job_queue is not None) and (update_queue is not None) async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.user_data, dict) and context.chat_data is None and isinstance(context.bot_data, dict) and isinstance(update.chosen_inline_result, ChosenInlineResult) ) def callback_pattern(self, update, context): if context.matches[0].groups(): self.test_flag = context.matches[0].groups() == ("res", "_id") if context.matches[0].groupdict(): self.test_flag = context.matches[0].groupdict() == {"begin": "res", "end": "_id"} def test_other_update_types(self, false_update): handler = ChosenInlineResultHandler(self.callback_basic) assert not handler.check_update(false_update) async def test_context(self, app, chosen_inline_result): handler = ChosenInlineResultHandler(self.callback) app.add_handler(handler) async with app: await app.process_update(chosen_inline_result) assert self.test_flag def test_with_pattern(self, chosen_inline_result): handler = ChosenInlineResultHandler(self.callback_basic, pattern=".*ult.*") assert handler.check_update(chosen_inline_result) chosen_inline_result.chosen_inline_result.result_id = "nothing here" assert not handler.check_update(chosen_inline_result) chosen_inline_result.chosen_inline_result.result_id = "result_id" async def test_context_pattern(self, app, chosen_inline_result): handler = ChosenInlineResultHandler( self.callback_pattern, pattern=r"(?P.*)ult(?P.*)" ) app.add_handler(handler) async with app: await app.process_update(chosen_inline_result) assert self.test_flag app.remove_handler(handler) handler = ChosenInlineResultHandler(self.callback_pattern, pattern=r"(res)ult(.*)") app.add_handler(handler) await app.process_update(chosen_inline_result) assert self.test_flag python-telegram-bot-21.1.1/tests/ext/test_commandhandler.py000066400000000000000000000310141460724040100240210ustar00rootroot00000000000000#!/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 re import pytest from telegram import Bot, Chat, Message, Update from telegram.ext import CallbackContext, CommandHandler, JobQueue, filters from tests.auxil.build_messages import ( make_command_message, make_command_update, make_message_update, ) from tests.auxil.slots import mro_slots def is_match(handler, update): """ Utility function that returns whether an update matched against a specific handler. :param handler: ``CommandHandler`` to check against :param update: update to check :return: (bool) whether ``update`` matched with ``handler`` """ check = handler.check_update(update) return check is not None and check is not False class BaseTest: """Base class for command and prefix handler test classes. Contains utility methods an several callbacks used by both classes.""" test_flag = False SRE_TYPE = type(re.match("", "")) @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def response(self, application, update): """ Utility to send an update to a dispatcher and assert whether the callback was called appropriately. Its purpose is for repeated usage in the same test function. """ self.test_flag = False async with application: await application.process_update(update) return self.test_flag def callback_basic(self, update, context): test_bot = isinstance(context.bot, Bot) test_update = isinstance(update, Update) self.test_flag = test_bot and test_update def make_callback_for(self, pass_keyword): def callback(bot, update, **kwargs): self.test_flag = kwargs.get(keyword) is not None keyword = pass_keyword[5:] return callback async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.user_data, dict) and isinstance(context.chat_data, dict) and isinstance(context.bot_data, dict) and isinstance(update.message, Message) ) def callback_args(self, update, context): self.test_flag = context.args == ["one", "two"] def callback_regex1(self, update, context): if context.matches: types = all(type(res) is self.SRE_TYPE for res in context.matches) num = len(context.matches) == 1 self.test_flag = types and num def callback_regex2(self, update, context): if context.matches: types = all(type(res) is self.SRE_TYPE for res in context.matches) num = len(context.matches) == 2 self.test_flag = types and num async def _test_context_args_or_regex(self, app, handler, text): app.add_handler(handler) update = make_command_update(text, bot=app.bot) assert not await self.response(app, update) update = make_command_update(text + " one two", bot=app.bot) assert await self.response(app, update) def _test_edited(self, message, handler_edited, handler_not_edited): """ Assert whether a handler that should accept edited messages and a handler that shouldn't work correctly. :param message: ``telegram.Message`` to check against the handlers :param handler_edited: handler that should accept edited messages :param handler_not_edited: handler that should not accept edited messages """ update = make_command_update(message) edited_update = make_command_update(message, edited=True) assert is_match(handler_edited, update) assert is_match(handler_edited, edited_update) assert is_match(handler_not_edited, update) assert not is_match(handler_not_edited, edited_update) # ----------------------------- CommandHandler ----------------------------- class TestCommandHandler(BaseTest): CMD = "/test" def test_slot_behaviour(self): handler = self.make_default_handler() for attr in handler.__slots__: assert getattr(handler, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" @pytest.fixture(scope="class") def command(self): return self.CMD @pytest.fixture(scope="class") def command_message(self, command, bot): return make_command_message(command, bot=bot) @pytest.fixture(scope="class") def command_update(self, command_message): return make_command_update(command_message) def make_default_handler(self, callback=None, **kwargs): callback = callback or self.callback_basic return CommandHandler(self.CMD[1:], callback, **kwargs) async def test_basic(self, app, command): """Test whether a command handler responds to its command and not to others, or badly formatted commands""" handler = self.make_default_handler() app.add_handler(handler) assert await self.response(app, make_command_update(command, bot=app.bot)) assert not is_match(handler, make_command_update(command[1:], bot=app.bot)) assert not is_match(handler, make_command_update(f"/not{command[1:]}", bot=app.bot)) assert not is_match(handler, make_command_update(f"not {command} at start", bot=app.bot)) assert not is_match( handler, make_message_update(bot=app.bot, message=None, caption="caption") ) handler = CommandHandler(["FOO", "bAR"], callback=self.callback) assert isinstance(handler.commands, frozenset) assert handler.commands == {"foo", "bar"} handler = CommandHandler(["FOO"], callback=self.callback) assert isinstance(handler.commands, frozenset) assert handler.commands == {"foo"} @pytest.mark.parametrize( "cmd", ["way_too_longcommand1234567yes_way_toooooooLong", "ïñválídletters", "invalid #&* chars"], ids=["too long", "invalid letter", "invalid characters"], ) def test_invalid_commands(self, cmd): with pytest.raises( ValueError, match=f"`{re.escape(cmd.lower())}` is not a valid bot command" ): CommandHandler(cmd, self.callback_basic) def test_command_list(self, bot): """A command handler with multiple commands registered should respond to all of them.""" handler = CommandHandler(["test", "star"], self.callback_basic) assert is_match(handler, make_command_update("/test", bot=bot)) assert is_match(handler, make_command_update("/star", bot=bot)) assert not is_match(handler, make_command_update("/stop", bot=bot)) def test_edited(self, command_message): """Test that a CH responds to an edited message if its filters allow it""" handler_edited = self.make_default_handler() handler_no_edited = self.make_default_handler(filters=~filters.UpdateType.EDITED_MESSAGE) self._test_edited(command_message, handler_edited, handler_no_edited) def test_directed_commands(self, bot, command): """Test recognition of commands with a mention to the bot""" handler = self.make_default_handler() assert is_match(handler, make_command_update(command + "@" + bot.username, bot=bot)) assert not is_match(handler, make_command_update(command + "@otherbot", bot=bot)) def test_with_filter(self, command, bot): """Test that a CH with a (generic) filter responds if its filters match""" handler = self.make_default_handler(filters=filters.ChatType.GROUP) assert is_match(handler, make_command_update(command, chat=Chat(-23, Chat.GROUP), bot=bot)) assert not is_match( handler, make_command_update(command, chat=Chat(23, Chat.PRIVATE), bot=bot) ) async def test_newline(self, app, command): """Assert that newlines don't interfere with a command handler matching a message""" handler = self.make_default_handler() app.add_handler(handler) update = make_command_update(command + "\nfoobar", bot=app.bot) async with app: assert is_match(handler, update) assert await self.response(app, update) def test_other_update_types(self, false_update): """Test that a command handler doesn't respond to unrelated updates""" handler = self.make_default_handler() assert not is_match(handler, false_update) def test_filters_for_wrong_command(self, mock_filter, bot): """Filters should not be executed if the command does not match the handler""" handler = self.make_default_handler(filters=mock_filter) assert not is_match(handler, make_command_update("/star", bot=bot)) assert not mock_filter.tested async def test_context(self, app, command_update): """Test correct behaviour of CHs with context-based callbacks""" handler = self.make_default_handler(self.callback) app.add_handler(handler) assert await self.response(app, command_update) async def test_context_args(self, app, command): """Test CHs that pass arguments through ``context``""" handler = self.make_default_handler(self.callback_args) await self._test_context_args_or_regex(app, handler, command) async def test_context_regex(self, app, command): """Test CHs with context-based callbacks and a single filter""" handler = self.make_default_handler(self.callback_regex1, filters=filters.Regex("one two")) await self._test_context_args_or_regex(app, handler, command) async def test_context_multiple_regex(self, app, command): """Test CHs with context-based callbacks and filters combined""" handler = self.make_default_handler( self.callback_regex2, filters=filters.Regex("one") & filters.Regex("two") ) await self._test_context_args_or_regex(app, handler, command) def test_command_has_args(self, bot): """Test CHs with optional has_args specified.""" handler_true = CommandHandler(["test"], self.callback_basic, has_args=True) handler_false = CommandHandler(["test"], self.callback_basic, has_args=False) handler_int_one = CommandHandler(["test"], self.callback_basic, has_args=1) handler_int_two = CommandHandler(["test"], self.callback_basic, has_args=2) assert is_match(handler_true, make_command_update("/test helloworld", bot=bot)) assert not is_match(handler_true, make_command_update("/test", bot=bot)) assert is_match(handler_false, make_command_update("/test", bot=bot)) assert not is_match(handler_false, make_command_update("/test helloworld", bot=bot)) assert is_match(handler_int_one, make_command_update("/test helloworld", bot=bot)) assert not is_match(handler_int_one, make_command_update("/test hello world", bot=bot)) assert not is_match(handler_int_one, make_command_update("/test", bot=bot)) assert is_match(handler_int_two, make_command_update("/test hello world", bot=bot)) assert not is_match(handler_int_two, make_command_update("/test helloworld", bot=bot)) assert not is_match(handler_int_two, make_command_update("/test", bot=bot)) def test_command_has_negative_args(self, bot): """Test CHs with optional has_args specified with negative int""" # Assert that CommandHandler cannot be instantiated. with pytest.raises( ValueError, match="CommandHandler argument has_args cannot be a negative integer" ): is_match(CommandHandler(["test"], self.callback_basic, has_args=-1)) python-telegram-bot-21.1.1/tests/ext/test_contexttypes.py000066400000000000000000000035741460724040100236300ustar00rootroot00000000000000#!/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.ext import CallbackContext, ContextTypes from tests.auxil.slots import mro_slots class SubClass(CallbackContext): pass class TestContextTypes: def test_slot_behaviour(self): instance = ContextTypes() for attr in instance.__slots__: assert getattr(instance, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(instance)) == len(set(mro_slots(instance))), "duplicate slot" def test_data_init(self): ct = ContextTypes(SubClass, int, float, bool) assert ct.context is SubClass assert ct.bot_data is int assert ct.chat_data is float assert ct.user_data is bool with pytest.raises(ValueError, match="subclass of CallbackContext"): ContextTypes(context=bool) def test_data_assignment(self): ct = ContextTypes() with pytest.raises(AttributeError): ct.bot_data = bool with pytest.raises(AttributeError): ct.user_data = bool with pytest.raises(AttributeError): ct.chat_data = bool python-telegram-bot-21.1.1/tests/ext/test_conversationhandler.py000066400000000000000000002611071460724040100251250ustar00rootroot00000000000000#!/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/]. """Persistence of conversations is tested in test_basepersistence.py""" import asyncio import functools import logging from pathlib import Path from warnings import filterwarnings import pytest from telegram import ( CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message, MessageEntity, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram.ext import ( ApplicationBuilder, ApplicationHandlerStop, CallbackContext, CallbackQueryHandler, ChosenInlineResultHandler, CommandHandler, ConversationHandler, Defaults, InlineQueryHandler, JobQueue, MessageHandler, PollAnswerHandler, PollHandler, PreCheckoutQueryHandler, ShippingQueryHandler, StringCommandHandler, StringRegexHandler, TypeHandler, filters, ) from telegram.warnings import PTBUserWarning from tests.auxil.build_messages import make_command_message from tests.auxil.files import PROJECT_ROOT_PATH from tests.auxil.pytest_classes import PytestBot, make_bot from tests.auxil.slots import mro_slots @pytest.fixture(scope="class") def user1(): return User(first_name="Misses Test", id=123, is_bot=False) @pytest.fixture(scope="class") def user2(): return User(first_name="Mister Test", id=124, is_bot=False) def raise_ahs(func): @functools.wraps(func) # for checking __repr__ async def decorator(self, *args, **kwargs): result = await func(self, *args, **kwargs) if self.raise_app_handler_stop: raise ApplicationHandlerStop(result) return result return decorator class TestConversationHandler: """Persistence of conversations is tested in test_basepersistence.py""" # State definitions # At first we're thirsty. Then we brew coffee, we drink it # and then we can start coding! END, THIRSTY, BREWING, DRINKING, CODING = range(-1, 4) # Drinking state definitions (nested) # At first we're holding the cup. Then we sip coffee, and last we swallow it HOLDING, SIPPING, SWALLOWING, REPLENISHING, STOPPING = map(chr, range(ord("a"), ord("f"))) current_state, entry_points, states, fallbacks = None, None, None, None group = Chat(0, Chat.GROUP) second_group = Chat(1, Chat.GROUP) raise_app_handler_stop = False test_flag = False # Test related @pytest.fixture(autouse=True) def _reset(self): self.raise_app_handler_stop = False self.test_flag = False self.current_state = {} self.entry_points = [CommandHandler("start", self.start)] self.states = { self.THIRSTY: [CommandHandler("brew", self.brew), CommandHandler("wait", self.start)], self.BREWING: [CommandHandler("pourCoffee", self.drink)], self.DRINKING: [ CommandHandler("startCoding", self.code), CommandHandler("drinkMore", self.drink), CommandHandler("end", self.end), ], self.CODING: [ CommandHandler("keepCoding", self.code), CommandHandler("gettingThirsty", self.start), CommandHandler("drinkMore", self.drink), ], } self.fallbacks = [CommandHandler("eat", self.start)] self.is_timeout = False # for nesting tests self.nested_states = { self.THIRSTY: [CommandHandler("brew", self.brew), CommandHandler("wait", self.start)], self.BREWING: [CommandHandler("pourCoffee", self.drink)], self.CODING: [ CommandHandler("keepCoding", self.code), CommandHandler("gettingThirsty", self.start), CommandHandler("drinkMore", self.drink), ], } self.drinking_entry_points = [CommandHandler("hold", self.hold)] self.drinking_states = { self.HOLDING: [CommandHandler("sip", self.sip)], self.SIPPING: [CommandHandler("swallow", self.swallow)], self.SWALLOWING: [CommandHandler("hold", self.hold)], } self.drinking_fallbacks = [ CommandHandler("replenish", self.replenish), CommandHandler("stop", self.stop), CommandHandler("end", self.end), CommandHandler("startCoding", self.code), CommandHandler("drinkMore", self.drink), ] self.drinking_entry_points.extend(self.drinking_fallbacks) # Map nested states to parent states: self.drinking_map_to_parent = { # Option 1 - Map a fictional internal state to an external parent state self.REPLENISHING: self.BREWING, # Option 2 - Map a fictional internal state to the END state on the parent self.STOPPING: self.END, # Option 3 - Map the internal END state to an external parent state self.END: self.CODING, # Option 4 - Map an external state to the same external parent state self.CODING: self.CODING, # Option 5 - Map an external state to the internal entry point self.DRINKING: self.DRINKING, } # State handlers def _set_state(self, update, state): self.current_state[update.message.from_user.id] = state return state # Actions @raise_ahs async def start(self, update, context): if isinstance(update, Update): return self._set_state(update, self.THIRSTY) return self._set_state(context.bot, self.THIRSTY) @raise_ahs async def end(self, update, context): return self._set_state(update, self.END) @raise_ahs async def start_end(self, update, context): return self._set_state(update, self.END) @raise_ahs async def start_none(self, update, context): return self._set_state(update, None) @raise_ahs async def brew(self, update, context): if isinstance(update, Update): return self._set_state(update, self.BREWING) return self._set_state(context.bot, self.BREWING) @raise_ahs async def drink(self, update, context): return self._set_state(update, self.DRINKING) @raise_ahs async def code(self, update, context): return self._set_state(update, self.CODING) @raise_ahs async def passout(self, update, context): assert update.message.text == "/brew" assert isinstance(update, Update) self.is_timeout = True @raise_ahs async def passout2(self, update, context): assert isinstance(update, Update) self.is_timeout = True @raise_ahs async def passout_context(self, update, context): assert update.message.text == "/brew" assert isinstance(context, CallbackContext) self.is_timeout = True @raise_ahs async def passout2_context(self, update, context): assert isinstance(context, CallbackContext) self.is_timeout = True # Drinking actions (nested) @raise_ahs async def hold(self, update, context): return self._set_state(update, self.HOLDING) @raise_ahs async def sip(self, update, context): return self._set_state(update, self.SIPPING) @raise_ahs async def swallow(self, update, context): return self._set_state(update, self.SWALLOWING) @raise_ahs async def replenish(self, update, context): return self._set_state(update, self.REPLENISHING) @raise_ahs async def stop(self, update, context): return self._set_state(update, self.STOPPING) def test_slot_behaviour(self): handler = ConversationHandler(entry_points=[], states={}, fallbacks=[]) for attr in handler.__slots__: assert getattr(handler, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" def test_init(self): entry_points = [] states = {} fallbacks = [] map_to_parent = {} ch = ConversationHandler( entry_points=entry_points, states=states, fallbacks=fallbacks, per_chat="per_chat", per_user="per_user", per_message="per_message", persistent="persistent", name="name", allow_reentry="allow_reentry", conversation_timeout=42, map_to_parent=map_to_parent, ) assert ch.entry_points is entry_points assert ch.states is states assert ch.fallbacks is fallbacks assert ch.map_to_parent is map_to_parent assert ch.per_chat == "per_chat" assert ch.per_user == "per_user" assert ch.per_message == "per_message" assert ch.persistent == "persistent" assert ch.name == "name" assert ch.allow_reentry == "allow_reentry" def test_init_persistent_no_name(self): with pytest.raises(ValueError, match="can't be persistent when handler is unnamed"): ConversationHandler( self.entry_points, states=self.states, fallbacks=[], persistent=True ) def test_repr_no_truncation(self): # ConversationHandler's __repr__ is not inherited from BaseHandler. ch = ConversationHandler( name="test_handler", entry_points=[], states=self.drinking_states, fallbacks=[], ) assert repr(ch) == ( "ConversationHandler[name=test_handler, " "states={'a': [CommandHandler[callback=TestConversationHandler.sip]], " "'b': [CommandHandler[callback=TestConversationHandler.swallow]], " "'c': [CommandHandler[callback=TestConversationHandler.hold]]}]" ) def test_repr_with_truncation(self): from copy import copy states = copy(self.drinking_states) # there are exactly 3 drinking states. adding one more to make sure it's truncated states["extra_to_be_truncated"] = [CommandHandler("foo", self.start)] ch = ConversationHandler( name="test_handler", entry_points=[], states=states, fallbacks=[], ) assert repr(ch) == ( "ConversationHandler[name=test_handler, " "states={'a': [CommandHandler[callback=TestConversationHandler.sip]], " "'b': [CommandHandler[callback=TestConversationHandler.swallow]], " "'c': [CommandHandler[callback=TestConversationHandler.hold]], ...}]" ) async def test_check_update_returns_non(self, app, user1): """checks some cases where updates should not be handled""" conv_handler = ConversationHandler([], {}, [], per_message=True, per_chat=True) assert not conv_handler.check_update("not an Update") assert not conv_handler.check_update(Update(0)) assert not conv_handler.check_update( Update(0, callback_query=CallbackQuery("1", from_user=user1, chat_instance="1")) ) async def test_handlers_generate_warning(self, recwarn): """this function tests all handler + per_* setting combinations.""" # the warning message action needs to be set to always, # otherwise only the first occurrence will be issued filterwarnings(action="always", category=PTBUserWarning) # this class doesn't do anything, its just not the Update class class NotUpdate: pass recwarn.clear() # this conversation handler has the string, string_regex, Pollhandler and TypeHandler # which should all generate a warning no matter the per_* setting. TypeHandler should # not when the class is Update ConversationHandler( entry_points=[StringCommandHandler("code", self.code)], states={ self.BREWING: [ StringRegexHandler("code", self.code), PollHandler(self.code), TypeHandler(NotUpdate, self.code), ], }, fallbacks=[TypeHandler(Update, self.code)], ) # these handlers should all raise a warning when per_chat is True ConversationHandler( entry_points=[ShippingQueryHandler(self.code)], states={ self.BREWING: [ InlineQueryHandler(self.code), PreCheckoutQueryHandler(self.code), PollAnswerHandler(self.code), ], }, fallbacks=[ChosenInlineResultHandler(self.code)], per_chat=True, ) # the CallbackQueryHandler should *not* raise when per_message is True, # but any other one should ConversationHandler( entry_points=[CallbackQueryHandler(self.code)], states={ self.BREWING: [CommandHandler("code", self.code)], }, fallbacks=[CallbackQueryHandler(self.code)], per_message=True, ) # the CallbackQueryHandler should raise when per_message is False ConversationHandler( entry_points=[CommandHandler("code", self.code)], states={ self.BREWING: [CommandHandler("code", self.code)], }, fallbacks=[CallbackQueryHandler(self.code)], per_message=False, ) # adding a nested conv to a conversation with timeout should warn child = ConversationHandler( entry_points=[CommandHandler("code", self.code)], states={ self.BREWING: [CommandHandler("code", self.code)], }, fallbacks=[CommandHandler("code", self.code)], ) ConversationHandler( entry_points=[CommandHandler("code", self.code)], states={ self.BREWING: [child], }, fallbacks=[CommandHandler("code", self.code)], conversation_timeout=42, ) # If per_message is True, per_chat should also be True, since msg ids are not unique ConversationHandler( entry_points=[CallbackQueryHandler(self.code, "code")], states={ self.BREWING: [CallbackQueryHandler(self.code, "code")], }, fallbacks=[CallbackQueryHandler(self.code, "code")], per_message=True, per_chat=False, ) # the overall number of handlers throwing a warning is 13 assert len(recwarn) == 13 # now we test the messages, they are raised in the order they are inserted # into the conversation handler assert ( str(recwarn[0].message) == "The `ConversationHandler` only handles updates of type `telegram.Update`. " "StringCommandHandler handles updates of type `str`." ) assert ( str(recwarn[1].message) == "The `ConversationHandler` only handles updates of type `telegram.Update`. " "StringRegexHandler handles updates of type `str`." ) assert ( str(recwarn[2].message) == "PollHandler will never trigger in a conversation since it has no information " "about the chat or the user who voted in it. Do you mean the " "`PollAnswerHandler`?" ) assert ( str(recwarn[3].message) == "The `ConversationHandler` only handles updates of type `telegram.Update`. " "The TypeHandler is set to handle NotUpdate." ) per_faq_link = ( " Read this FAQ entry to learn more about the per_* settings: " "https://github.com/python-telegram-bot/python-telegram-bot/wiki" "/Frequently-Asked-Questions#what-do-the-per_-settings-in-conversationhandler-do." ) assert str(recwarn[4].message) == ( "Updates handled by ShippingQueryHandler only have information about the user," " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link ) assert str(recwarn[5].message) == ( "Updates handled by ChosenInlineResultHandler only have information about the user," " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link ) assert str(recwarn[6].message) == ( "Updates handled by InlineQueryHandler only have information about the user," " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link ) assert str(recwarn[7].message) == ( "Updates handled by PreCheckoutQueryHandler only have information about the user," " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link ) assert str(recwarn[8].message) == ( "Updates handled by PollAnswerHandler only have information about the user," " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link ) assert str(recwarn[9].message) == ( "If 'per_message=True', all entry points, state handlers, and fallbacks must be " "'CallbackQueryHandler', since no other handlers have a message context." + per_faq_link ) assert str(recwarn[10].message) == ( "If 'per_message=False', 'CallbackQueryHandler' will not be tracked for every message." + per_faq_link ) assert ( str(recwarn[11].message) == "Using `conversation_timeout` with nested conversations is currently not " "supported. You can still try to use it, but it will likely behave differently" " from what you expect." ) assert ( str(recwarn[12].message) == "If 'per_message=True' is used, 'per_chat=True' should also be used, " "since message IDs are not globally unique." ) # this for loop checks if the correct stacklevel is used when generating the warning for warning in recwarn: assert warning.category is PTBUserWarning assert warning.filename == __file__, "incorrect stacklevel!" @pytest.mark.parametrize( "attr", [ "entry_points", "states", "fallbacks", "per_chat", "per_user", "per_message", "name", "persistent", "allow_reentry", "conversation_timeout", "map_to_parent", ], indirect=False, ) def test_immutable(self, attr): ch = ConversationHandler(entry_points=[], states={}, fallbacks=[]) with pytest.raises(AttributeError, match=f"You can not assign a new value to {attr}"): setattr(ch, attr, True) def test_per_all_false(self): with pytest.raises(ValueError, match="can't all be 'False'"): ConversationHandler( entry_points=[], states={}, fallbacks=[], per_chat=False, per_user=False, per_message=False, ) @pytest.mark.parametrize("raise_ahs", [True, False]) async def test_basic_and_app_handler_stop(self, app, bot, user1, user2, raise_ahs): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks ) app.add_handler(handler) async def callback(_, __): self.test_flag = True app.add_handler(TypeHandler(object, callback), group=100) self.raise_app_handler_stop = raise_ahs # User one, starts the state machine. message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY assert self.test_flag == (not raise_ahs) # The user is thirsty and wants to brew coffee. message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING assert self.test_flag == (not raise_ahs) # Lets see if an invalid command makes sure, no state is changed. message.text = "/nothing" message.entities[0].length = len("/nothing") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING assert self.test_flag is True self.test_flag = False # Lets see if the state machine still works by pouring coffee. message.text = "/pourCoffee" message.entities[0].length = len("/pourCoffee") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING assert self.test_flag == (not raise_ahs) # Let's now verify that for another user, who did not start yet, # the state has not been changed. message.from_user = user2 await app.process_update(Update(update_id=0, message=message)) with pytest.raises(KeyError): self.current_state[user2.id] async def test_conversation_handler_end(self, caplog, app, bot, user1): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks ) app.add_handler(handler) message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=0, message=message)) message.text = "/pourCoffee" message.entities[0].length = len("/pourCoffee") await app.process_update(Update(update_id=0, message=message)) message.text = "/end" message.entities[0].length = len("/end") caplog.clear() with caplog.at_level(logging.ERROR): await app.process_update(Update(update_id=0, message=message)) assert len(caplog.records) == 0 assert self.current_state[user1.id] == self.END # make sure that the conversation has ended by checking that the start command is # accepted again message.text = "/start" message.entities[0].length = len("/start") assert handler.check_update(Update(update_id=0, message=message)) async def test_conversation_handler_fallback(self, app, bot, user1, user2): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks ) app.add_handler(handler) # first check if fallback will not trigger start when not started message = Message( 0, None, self.group, from_user=user1, text="/eat", entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/eat"))], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) with pytest.raises(KeyError): self.current_state[user1.id] # User starts the state machine. message.text = "/start" message.entities[0].length = len("/start") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY # The user is thirsty and wants to brew coffee. message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING # Now a fallback command is issued message.text = "/eat" message.entities[0].length = len("/eat") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY async def test_unknown_state_warning(self, app, bot, user1, recwarn): def build_callback(state): async def callback(_, __): return state return callback handler = ConversationHandler( entry_points=[CommandHandler("start", build_callback(1))], states={ 1: [TypeHandler(Update, build_callback(69))], 2: [TypeHandler(Update, build_callback(42))], }, fallbacks=self.fallbacks, name="xyz", ) app.add_handler(handler) message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) try: await app.process_update(Update(update_id=1, message=message)) except Exception as exc: print(exc) raise exc assert len(recwarn) == 1 assert recwarn[0].category is PTBUserWarning assert ( Path(recwarn[0].filename) == PROJECT_ROOT_PATH / "telegram" / "ext" / "_handlers" / "conversationhandler.py" ), "wrong stacklevel!" assert ( str(recwarn[0].message) == "'callback' returned state 69 which is unknown to the ConversationHandler xyz." ) async def test_conversation_handler_per_chat(self, app, bot, user1, user2): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, per_user=False, ) app.add_handler(handler) # User one, starts the state machine. message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) # The user is thirsty and wants to brew coffee. message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=0, message=message)) # Let's now verify that for another user, who did not start yet, # the state will be changed because they are in the same group. message.from_user = user2 message.text = "/pourCoffee" message.entities[0].length = len("/pourCoffee") await app.process_update(Update(update_id=0, message=message)) # Check that we're in the DRINKING state by checking that the corresponding command # is accepted message.from_user = user1 message.text = "/startCoding" message.entities[0].length = len("/startCoding") assert handler.check_update(Update(update_id=0, message=message)) message.from_user = user2 assert handler.check_update(Update(update_id=0, message=message)) async def test_conversation_handler_per_user(self, app, bot, user1): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, per_chat=False, ) app.add_handler(handler) # User one, starts the state machine. message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() # First check that updates without user won't be handled message.from_user = None assert not handler.check_update(Update(update_id=0, message=message)) message.from_user = user1 async with app: await app.process_update(Update(update_id=0, message=message)) # The user is thirsty and wants to brew coffee. message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=0, message=message)) # Let's now verify that for the same user in a different group, the state will still be # updated message.chat = self.second_group message.text = "/pourCoffee" message.entities[0].length = len("/pourCoffee") await app.process_update(Update(update_id=0, message=message)) # Check that we're in the DRINKING state by checking that the corresponding command # is accepted message.chat = self.group message.text = "/startCoding" message.entities[0].length = len("/startCoding") assert handler.check_update(Update(update_id=0, message=message)) message.chat = self.second_group assert handler.check_update(Update(update_id=0, message=message)) @pytest.mark.parametrize("inline", [True, False]) @pytest.mark.filterwarnings("ignore: If 'per_message=True' is used, 'per_chat=True'") async def test_conversation_handler_per_message(self, app, bot, user1, user2, inline): async def entry(update, context): return 1 async def one(update, context): return 2 async def two(update, context): return ConversationHandler.END handler = ConversationHandler( entry_points=[CallbackQueryHandler(entry)], states={ 1: [CallbackQueryHandler(one, pattern="^1$")], 2: [CallbackQueryHandler(two, pattern="^2$")], }, fallbacks=[], per_message=True, per_chat=not inline, ) app.add_handler(handler) # User one, starts the state machine. message = ( Message(0, None, self.group, from_user=user1, text="msg w/ inlinekeyboard") if not inline else None ) if message: message.set_bot(bot) message._unfreeze() inline_message_id = "42" if inline else None async with app: cbq_1 = CallbackQuery( 0, user1, None, message=message, data="1", inline_message_id=inline_message_id, ) cbq_1.set_bot(bot) cbq_2 = CallbackQuery( 0, user1, None, message=message, data="2", inline_message_id=inline_message_id, ) cbq_2.set_bot(bot) cbq_2._unfreeze() await app.process_update(Update(update_id=0, callback_query=cbq_1)) # Make sure that we're in the correct state assert handler.check_update(Update(0, callback_query=cbq_1)) assert not handler.check_update(Update(0, callback_query=cbq_2)) await app.process_update(Update(update_id=0, callback_query=cbq_1)) # Make sure that we're in the correct state assert not handler.check_update(Update(0, callback_query=cbq_1)) assert handler.check_update(Update(0, callback_query=cbq_2)) # Let's now verify that for a different user in the same group, the state will not be # updated cbq_2.from_user = user2 await app.process_update(Update(update_id=0, callback_query=cbq_2)) cbq_2.from_user = user1 assert not handler.check_update(Update(0, callback_query=cbq_1)) assert handler.check_update(Update(0, callback_query=cbq_2)) async def test_end_on_first_message(self, app, bot, user1): handler = ConversationHandler( entry_points=[CommandHandler("start", self.start_end)], states={}, fallbacks=[] ) app.add_handler(handler) # User starts the state machine and immediately ends it. message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert handler.check_update(Update(update_id=0, message=message)) async def test_end_on_first_message_non_blocking_handler(self, app, bot, user1): handler = ConversationHandler( entry_points=[CommandHandler("start", callback=self.start_end, block=False)], states={}, fallbacks=[], ) app.add_handler(handler) # User starts the state machine with a non-blocking function that immediately ends the # conversation. non-blocking results are resolved when the users state is queried next # time. message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) # give the task a chance to finish await asyncio.sleep(0.1) # Let's check that processing the same update again is accepted. this confirms that # a) the pending state is correctly resolved # b) the conversation has ended assert handler.check_update(Update(0, message=message)) async def test_none_on_first_message(self, app, bot, user1): handler = ConversationHandler( entry_points=[MessageHandler(filters.ALL, self.start_none)], states={}, fallbacks=[] ) app.add_handler(handler) # User starts the state machine and a callback function returns None message = Message(0, None, self.group, from_user=user1, text="/start") message.set_bot(bot) message._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) # Check that the same message is accepted again, i.e. the conversation immediately # ended assert handler.check_update(Update(0, message=message)) async def test_none_on_first_message_non_blocking_handler(self, app, bot, user1): handler = ConversationHandler( entry_points=[CommandHandler("start", self.start_none, block=False)], states={}, fallbacks=[], ) app.add_handler(handler) # User starts the state machine with a non-blocking handler that returns None # non-blocking results are resolved when the users state is queried next time. message = Message( 0, None, self.group, text="/start", from_user=user1, entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) # Give the task a chance to finish await asyncio.sleep(0.1) # Let's check that processing the same update again is accepted. this confirms that # a) the pending state is correctly resolved # b) the conversation has ended assert handler.check_update(Update(0, message=message)) async def test_per_chat_message_without_chat(self, bot, user1): handler = ConversationHandler( entry_points=[CommandHandler("start", self.start_end)], states={}, fallbacks=[] ) cbq = CallbackQuery(0, user1, None, None) cbq.set_bot(bot) update = Update(0, callback_query=cbq) assert not handler.check_update(update) async def test_channel_message_without_chat(self, bot): handler = ConversationHandler( entry_points=[MessageHandler(filters.ALL, self.start_end)], states={}, fallbacks=[] ) message = Message(0, date=None, chat=Chat(0, Chat.CHANNEL, "Misses Test")) message.set_bot(bot) message._unfreeze() update = Update(0, channel_post=message) assert not handler.check_update(update) update = Update(0, edited_channel_post=message) assert not handler.check_update(update) async def test_all_update_types(self, app, bot, user1): handler = ConversationHandler( entry_points=[CommandHandler("start", self.start_end)], states={}, fallbacks=[] ) message = Message(0, None, self.group, from_user=user1, text="ignore") message.set_bot(bot) message._unfreeze() callback_query = CallbackQuery(0, user1, None, message=message, data="data") callback_query.set_bot(bot) chosen_inline_result = ChosenInlineResult(0, user1, "query") chosen_inline_result.set_bot(bot) inline_query = InlineQuery(0, user1, "query", offset="") inline_query.set_bot(bot) pre_checkout_query = PreCheckoutQuery(0, user1, "USD", 100, []) pre_checkout_query.set_bot(bot) shipping_query = ShippingQuery(0, user1, [], None) shipping_query.set_bot(bot) assert not handler.check_update(Update(0, callback_query=callback_query)) assert not handler.check_update(Update(0, chosen_inline_result=chosen_inline_result)) assert not handler.check_update(Update(0, inline_query=inline_query)) assert not handler.check_update(Update(0, message=message)) assert not handler.check_update(Update(0, pre_checkout_query=pre_checkout_query)) assert not handler.check_update(Update(0, shipping_query=shipping_query)) @pytest.mark.parametrize("jq", [True, False]) async def test_no_running_job_queue_warning(self, app, bot, user1, recwarn, jq): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) if not jq: app = ApplicationBuilder().token(bot.token).job_queue(None).build() app.add_handler(handler) message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.5) if jq: assert len(recwarn) == 1 else: assert len(recwarn) == 2 assert str(recwarn[0].message if jq else recwarn[1].message).startswith( "Ignoring `conversation_timeout`" ) assert ("is not running" if jq else "No `JobQueue` set up.") in str(recwarn[0].message) for warning in recwarn: assert warning.category is PTBUserWarning assert ( Path(warning.filename) == PROJECT_ROOT_PATH / "telegram" / "ext" / "_handlers" / "conversationhandler.py" ), "wrong stacklevel!" # now set app.job_queue back to it's original value async def test_schedule_job_exception(self, app, bot, user1, monkeypatch, caplog): def mocked_run_once(*a, **kw): raise Exception("job error") class DictJB(JobQueue): pass app = ApplicationBuilder().token(bot.token).job_queue(DictJB()).build() monkeypatch.setattr(app.job_queue, "run_once", mocked_run_once) handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=100, ) app.add_handler(handler) message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.start() with caplog.at_level(logging.ERROR): await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.5) assert len(caplog.records) == 1 assert caplog.records[0].message == "Failed to schedule timeout." assert caplog.records[0].name == "telegram.ext.ConversationHandler" assert str(caplog.records[0].exc_info[1]) == "job error" await app.stop() @pytest.mark.parametrize(argnames="test_type", argvalues=["none", "exception"]) async def test_non_blocking_exception_or_none(self, app, bot, user1, caplog, test_type): """Here we make sure that when a non-blocking handler raises an exception or returns None, the state isn't changed. """ error = Exception("task exception") async def conv_entry(*a, **kw): return 1 async def raise_error(*a, **kw): if test_type == "none": return raise error handler = ConversationHandler( entry_points=[CommandHandler("start", conv_entry)], states={1: [MessageHandler(filters.Text(["error"]), raise_error)]}, fallbacks=self.fallbacks, block=False, ) app.add_handler(handler) message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() # start the conversation async with app: await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.1) message.text = "error" await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.1) caplog.clear() with caplog.at_level(logging.ERROR): # This also makes sure that we're still in the same state assert handler.check_update(Update(0, message=message)) if test_type == "exception": assert len(caplog.records) == 1 assert caplog.records[0].name == "telegram.ext.ConversationHandler" assert ( caplog.records[0].message == "Task function raised exception. Falling back to old state 1" ) assert caplog.records[0].exc_info[1] is None else: assert len(caplog.records) == 0 async def test_non_blocking_entry_point_exception(self, app, bot, user1, caplog): """Here we make sure that when a non-blocking entry point raises an exception, the state isn't changed. """ error = Exception("task exception") async def raise_error(*a, **kw): raise error handler = ConversationHandler( entry_points=[CommandHandler("start", raise_error, block=False)], states={}, fallbacks=self.fallbacks, ) app.add_handler(handler) message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() # start the conversation async with app: await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.1) caplog.clear() with caplog.at_level(logging.ERROR): # This also makes sure that we're still in the same state assert handler.check_update(Update(0, message=message)) assert len(caplog.records) == 1 assert caplog.records[0].name == "telegram.ext.ConversationHandler" assert ( caplog.records[0].message == "Task function raised exception. Falling back to old state None" ) assert caplog.records[0].exc_info[1] is None async def test_conversation_timeout(self, app, bot, user1): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) app.add_handler(handler) # Start state machine, then reach timeout start_message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) start_message.set_bot(bot) brew_message = Message( 0, None, self.group, from_user=user1, text="/brew", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/brew")) ], ) brew_message.set_bot(bot) pour_coffee_message = Message( 0, None, self.group, from_user=user1, text="/pourCoffee", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/pourCoffee")) ], ) pour_coffee_message.set_bot(bot) async with app: await app.start() await app.process_update(Update(update_id=0, message=start_message)) assert handler.check_update(Update(0, message=brew_message)) await asyncio.sleep(0.75) assert handler.check_update(Update(0, message=start_message)) # Start state machine, do something, then reach timeout await app.process_update(Update(update_id=1, message=start_message)) assert handler.check_update(Update(0, message=brew_message)) # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY # start_message.text = '/brew' # start_message.entities[0].length = len('/brew') await app.process_update(Update(update_id=2, message=brew_message)) assert handler.check_update(Update(0, message=pour_coffee_message)) # assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING await asyncio.sleep(0.75) assert handler.check_update(Update(0, message=start_message)) # assert handler.conversations.get((self.group.id, user1.id)) is None await app.stop() async def test_timeout_not_triggered_on_conv_end_non_blocking(self, bot, app, user1): def timeout(*a, **kw): self.test_flag = True self.states.update({ConversationHandler.TIMEOUT: [TypeHandler(Update, timeout)]}) handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=0.5, block=False, ) app.add_handler(handler) message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: # start the conversation await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.1) message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=1, message=message)) await asyncio.sleep(0.1) message.text = "/pourCoffee" message.entities[0].length = len("/pourCoffee") await app.process_update(Update(update_id=2, message=message)) await asyncio.sleep(0.1) message.text = "/end" message.entities[0].length = len("/end") await app.process_update(Update(update_id=3, message=message)) await asyncio.sleep(1) # assert timeout handler didn't get called assert self.test_flag is False async def test_conversation_timeout_application_handler_stop(self, app, bot, user1, recwarn): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) def timeout(*args, **kwargs): raise ApplicationHandlerStop self.states.update({ConversationHandler.TIMEOUT: [TypeHandler(Update, timeout)]}) app.add_handler(handler) # Start state machine, then reach timeout message = Message( 0, None, self.group, text="/start", from_user=user1, entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() brew_message = Message( 0, None, self.group, from_user=user1, text="/brew", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/brew")) ], ) brew_message.set_bot(bot) async with app: await app.start() await app.process_update(Update(update_id=0, message=message)) # Make sure that we're in the next state assert handler.check_update(Update(0, message=brew_message)) await app.process_update(Update(0, message=brew_message)) await asyncio.sleep(0.9) # Check that conversation has ended by checking that the start messages is accepted # again assert handler.check_update(Update(0, message=message)) assert len(recwarn) == 1 assert str(recwarn[0].message).startswith("ApplicationHandlerStop in TIMEOUT") assert recwarn[0].category is PTBUserWarning assert ( Path(recwarn[0].filename) == PROJECT_ROOT_PATH / "telegram" / "ext" / "_jobqueue.py" ), "wrong stacklevel!" await app.stop() async def test_conversation_handler_timeout_update_and_context(self, app, bot, user1): context = None async def start_callback(u, c): nonlocal context, self context = c return await self.start(u, c) # Start state machine, then reach timeout message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() update = Update(update_id=0, message=message) async def timeout_callback(u, c): nonlocal update, context assert u is update assert c is context self.is_timeout = (u is update) and (c is context) states = self.states timeout_handler = CommandHandler("start", timeout_callback) states.update({ConversationHandler.TIMEOUT: [timeout_handler]}) handler = ConversationHandler( entry_points=[CommandHandler("start", start_callback)], states=states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) app.add_handler(handler) async with app: await app.start() await app.process_update(update) await asyncio.sleep(0.9) # check that the conversation has ended by checking that the start message is accepted assert handler.check_update(Update(0, message=message)) assert self.is_timeout await app.stop() @pytest.mark.flaky(3, 1) async def test_conversation_timeout_keeps_extending(self, app, bot, user1): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) app.add_handler(handler) # Start state machine, wait, do something, verify the timeout is extended. # t=0 /start (timeout=.5) # t=.35 /brew (timeout=.85) # t=.5 original timeout # t=.6 /pourCoffee (timeout=1.1) # t=.85 second timeout # t=1.1 actual timeout message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.start() await app.process_update(Update(update_id=0, message=message)) message.text = "/brew" message.entities[0].length = len("/brew") assert handler.check_update(Update(0, message=message)) await asyncio.sleep(0.35) # t=.35 assert handler.check_update(Update(0, message=message)) await app.process_update(Update(update_id=0, message=message)) message.text = "/pourCoffee" message.entities[0].length = len("/pourCoffee") assert handler.check_update(Update(0, message=message)) await asyncio.sleep(0.25) # t=.6 assert handler.check_update(Update(0, message=message)) await app.process_update(Update(update_id=0, message=message)) message.text = "/startCoding" message.entities[0].length = len("/startCoding") assert handler.check_update(Update(0, message=message)) await asyncio.sleep(0.4) # t=1.0 assert handler.check_update(Update(0, message=message)) await asyncio.sleep(0.3) # t=1.3 assert not handler.check_update(Update(0, message=message)) message.text = "/start" message.entities[0].length = len("/start") assert handler.check_update(Update(0, message=message)) await app.stop() async def test_conversation_timeout_two_users(self, app, bot, user1, user2): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) app.add_handler(handler) # Start state machine, do something as second user, then reach timeout message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.start() await app.process_update(Update(update_id=0, message=message)) message.text = "/brew" message.entities[0].length = len("/brew") assert handler.check_update(Update(0, message=message)) message.from_user = user2 await app.process_update(Update(update_id=0, message=message)) message.text = "/start" message.entities[0].length = len("/start") # Make sure that user2s conversation has not yet started assert handler.check_update(Update(0, message=message)) await app.process_update(Update(update_id=0, message=message)) message.text = "/brew" message.entities[0].length = len("/brew") assert handler.check_update(Update(0, message=message)) await asyncio.sleep(0.7) # check that both conversations have ended by checking that the start message is # accepted again message.text = "/start" message.entities[0].length = len("/start") message.from_user = user1 assert handler.check_update(Update(0, message=message)) message.from_user = user2 assert handler.check_update(Update(0, message=message)) await app.stop() async def test_conversation_handler_timeout_state(self, app, bot, user1): states = self.states states.update( { ConversationHandler.TIMEOUT: [ CommandHandler("brew", self.passout), MessageHandler(~filters.Regex("oding"), self.passout2), ] } ) handler = ConversationHandler( entry_points=self.entry_points, states=states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) app.add_handler(handler) # CommandHandler timeout message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.start() await app.process_update(Update(update_id=0, message=message)) message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again message.text = "/start" message.entities[0].length = len("/start") assert handler.check_update(Update(0, message=message)) assert self.is_timeout # MessageHandler timeout self.is_timeout = False message.text = "/start" message.entities[0].length = len("/start") await app.process_update(Update(update_id=1, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again assert handler.check_update(Update(0, message=message)) assert self.is_timeout # Timeout but no valid handler self.is_timeout = False await app.process_update(Update(update_id=0, message=message)) message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=0, message=message)) message.text = "/startCoding" message.entities[0].length = len("/startCoding") await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again message.text = "/start" message.entities[0].length = len("/start") assert handler.check_update(Update(0, message=message)) assert not self.is_timeout await app.stop() async def test_conversation_handler_timeout_state_context(self, app, bot, user1): states = self.states states.update( { ConversationHandler.TIMEOUT: [ CommandHandler("brew", self.passout_context), MessageHandler(~filters.Regex("oding"), self.passout2_context), ] } ) handler = ConversationHandler( entry_points=self.entry_points, states=states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) app.add_handler(handler) # CommandHandler timeout message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.start() await app.process_update(Update(update_id=0, message=message)) message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again message.text = "/start" message.entities[0].length = len("/start") assert handler.check_update(Update(0, message=message)) assert self.is_timeout # MessageHandler timeout self.is_timeout = False message.text = "/start" message.entities[0].length = len("/start") await app.process_update(Update(update_id=1, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again assert handler.check_update(Update(0, message=message)) assert self.is_timeout # Timeout but no valid handler self.is_timeout = False await app.process_update(Update(update_id=0, message=message)) message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=0, message=message)) message.text = "/startCoding" message.entities[0].length = len("/startCoding") await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again message.text = "/start" message.entities[0].length = len("/start") assert handler.check_update(Update(0, message=message)) assert not self.is_timeout await app.stop() async def test_conversation_timeout_cancel_conflict(self, app, bot, user1): # Start state machine, wait half the timeout, # then call a callback that takes more than the timeout # t=0 /start (timeout=.5) # t=.25 /slowbrew (sleep .5) # | t=.5 original timeout (should not execute) # | t=.75 /slowbrew returns (timeout=1.25) # t=1.25 timeout async def slowbrew(_update, context): await asyncio.sleep(0.25) # Let's give to the original timeout a chance to execute await asyncio.sleep(0.25) # By returning None we do not override the conversation state so # we can see if the timeout has been executed states = self.states states[self.THIRSTY].append(CommandHandler("slowbrew", slowbrew)) states.update({ConversationHandler.TIMEOUT: [MessageHandler(None, self.passout2)]}) handler = ConversationHandler( entry_points=self.entry_points, states=states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) app.add_handler(handler) # CommandHandler timeout message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.start() await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.25) message.text = "/slowbrew" message.entities[0].length = len("/slowbrew") await app.process_update(Update(update_id=0, message=message)) # Check that conversation has not ended by checking that start cmd is not accepted message.text = "/start" message.entities[0].length = len("/start") assert not handler.check_update(Update(0, message=message)) assert not self.is_timeout await asyncio.sleep(0.7) # Check that conversation has ended by checking that start cmd is accepted again message.text = "/start" message.entities[0].length = len("/start") assert handler.check_update(Update(0, message=message)) assert self.is_timeout await app.stop() async def test_nested_conversation_handler(self, app, bot, user1, user2): self.nested_states[self.DRINKING] = [ ConversationHandler( entry_points=self.drinking_entry_points, states=self.drinking_states, fallbacks=self.drinking_fallbacks, map_to_parent=self.drinking_map_to_parent, ) ] handler = ConversationHandler( entry_points=self.entry_points, states=self.nested_states, fallbacks=self.fallbacks ) app.add_handler(handler) # User one, starts the state machine. message = Message( 0, None, self.group, from_user=user1, text="/start", entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY # The user is thirsty and wants to brew coffee. message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING # Lets pour some coffee. message.text = "/pourCoffee" message.entities[0].length = len("/pourCoffee") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING # The user is holding the cup message.text = "/hold" message.entities[0].length = len("/hold") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING # The user is sipping coffee message.text = "/sip" message.entities[0].length = len("/sip") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.SIPPING # The user is swallowing message.text = "/swallow" message.entities[0].length = len("/swallow") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.SWALLOWING # The user is holding the cup again message.text = "/hold" message.entities[0].length = len("/hold") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING # The user wants to replenish the coffee supply message.text = "/replenish" message.entities[0].length = len("/replenish") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.REPLENISHING # check that we're in the right state now by checking that the update is accepted message.text = "/pourCoffee" message.entities[0].length = len("/pourCoffee") assert handler.check_update(Update(0, message=message)) # The user wants to drink their coffee again) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING # The user is now ready to start coding message.text = "/startCoding" message.entities[0].length = len("/startCoding") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.CODING # The user decides it's time to drink again message.text = "/drinkMore" message.entities[0].length = len("/drinkMore") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING # The user is holding their cup message.text = "/hold" message.entities[0].length = len("/hold") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING # The user wants to end with the drinking and go back to coding message.text = "/end" message.entities[0].length = len("/end") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.END # check that we're in the right state now by checking that the update is accepted message.text = "/drinkMore" message.entities[0].length = len("/drinkMore") assert handler.check_update(Update(0, message=message)) # The user wants to drink once more await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING # The user wants to stop altogether message.text = "/stop" message.entities[0].length = len("/stop") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.STOPPING # check that the conversation has ended by checking that the start cmd is accepted message.text = "/start" message.entities[0].length = len("/start") assert handler.check_update(Update(0, message=message)) async def test_nested_conversation_application_handler_stop(self, app, bot, user1, user2): self.nested_states[self.DRINKING] = [ ConversationHandler( entry_points=self.drinking_entry_points, states=self.drinking_states, fallbacks=self.drinking_fallbacks, map_to_parent=self.drinking_map_to_parent, ) ] handler = ConversationHandler( entry_points=self.entry_points, states=self.nested_states, fallbacks=self.fallbacks ) def test_callback(u, c): self.test_flag = True app.add_handler(handler) app.add_handler(TypeHandler(Update, test_callback), group=1) self.raise_app_handler_stop = True # User one, starts the state machine. message = Message( 0, None, self.group, text="/start", from_user=user1, entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/start")) ], ) message.set_bot(bot) message._unfreeze() message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY assert not self.test_flag # The user is thirsty and wants to brew coffee. message.text = "/brew" message.entities[0].length = len("/brew") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING assert not self.test_flag # Lets pour some coffee. message.text = "/pourCoffee" message.entities[0].length = len("/pourCoffee") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING assert not self.test_flag # The user is holding the cup message.text = "/hold" message.entities[0].length = len("/hold") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING assert not self.test_flag # The user is sipping coffee message.text = "/sip" message.entities[0].length = len("/sip") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.SIPPING assert not self.test_flag # The user is swallowing message.text = "/swallow" message.entities[0].length = len("/swallow") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.SWALLOWING assert not self.test_flag # The user is holding the cup again message.text = "/hold" message.entities[0].length = len("/hold") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING assert not self.test_flag # The user wants to replenish the coffee supply message.text = "/replenish" message.entities[0].length = len("/replenish") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.REPLENISHING # check that we're in the right state now by checking that the update is accepted message.text = "/pourCoffee" message.entities[0].length = len("/pourCoffee") assert handler.check_update(Update(0, message=message)) assert not self.test_flag # The user wants to drink their coffee again await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING assert not self.test_flag # The user is now ready to start coding message.text = "/startCoding" message.entities[0].length = len("/startCoding") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.CODING assert not self.test_flag # The user decides it's time to drink again message.text = "/drinkMore" message.entities[0].length = len("/drinkMore") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING assert not self.test_flag # The user is holding their cup message.text = "/hold" message.entities[0].length = len("/hold") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING assert not self.test_flag # The user wants to end with the drinking and go back to coding message.text = "/end" message.entities[0].length = len("/end") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.END # check that we're in the right state now by checking that the update is accepted message.text = "/drinkMore" message.entities[0].length = len("/drinkMore") assert handler.check_update(Update(0, message=message)) assert not self.test_flag # The user wants to drink once more message.text = "/drinkMore" message.entities[0].length = len("/drinkMore") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING assert not self.test_flag # The user wants to stop altogether message.text = "/stop" message.entities[0].length = len("/stop") await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.STOPPING # check that the conv has ended by checking that the start cmd is accepted message.text = "/start" message.entities[0].length = len("/start") assert handler.check_update(Update(0, message=message)) assert not self.test_flag @pytest.mark.parametrize("callback_raises", [True, False]) async def test_timeout_non_block(self, app, user1, callback_raises): event = asyncio.Event() async def callback(_, __): await event.wait() if callback_raises: raise RuntimeError return 1 conv_handler = ConversationHandler( entry_points=[MessageHandler(filters.ALL, callback=callback, block=False)], states={ConversationHandler.TIMEOUT: [TypeHandler(Update, self.passout2)]}, fallbacks=[], conversation_timeout=0.5, ) app.add_handler(conv_handler) async with app: await app.start() message = Message( 0, None, self.group, text="/start", from_user=user1, ) assert conv_handler.check_update(Update(0, message=message)) await app.process_update(Update(0, message=message)) await asyncio.sleep(0.7) tasks = asyncio.all_tasks() assert any(":handle_update:non_blocking_cb" in t.get_name() for t in tasks) assert any(":handle_update:timeout_job" in t.get_name() for t in tasks) assert not self.is_timeout event.set() await asyncio.sleep(0.7) assert self.is_timeout == (not callback_raises) await app.stop() async def test_no_timeout_on_end(self, app, user1): conv_handler = ConversationHandler( entry_points=[MessageHandler(filters.ALL, callback=self.start_end)], states={ConversationHandler.TIMEOUT: [TypeHandler(Update, self.passout2)]}, fallbacks=[], conversation_timeout=0.5, ) app.add_handler(conv_handler) async with app: await app.start() message = Message( 0, None, self.group, text="/start", from_user=user1, ) assert conv_handler.check_update(Update(0, message=message)) await app.process_update(Update(0, message=message)) await asyncio.sleep(0.7) assert not self.is_timeout await app.stop() async def test_conversation_handler_block_dont_override(self, app): """This just makes sure that we don't change any attributes of the handlers of the conv""" conv_handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, block=False, ) all_handlers = conv_handler.entry_points + conv_handler.fallbacks for state_handlers in conv_handler.states.values(): all_handlers += state_handlers for handler in all_handlers: assert handler.block conv_handler = ConversationHandler( entry_points=[CommandHandler("start", self.start_end, block=False)], states={1: [CommandHandler("start", self.start_end, block=False)]}, fallbacks=[CommandHandler("start", self.start_end, block=False)], block=True, ) all_handlers = conv_handler.entry_points + conv_handler.fallbacks for state_handlers in conv_handler.states.values(): all_handlers += state_handlers for handler in all_handlers: assert handler.block is False @pytest.mark.parametrize("default_block", [True, False, None]) @pytest.mark.parametrize("ch_block", [True, False, None]) @pytest.mark.parametrize("handler_block", [True, False, None]) @pytest.mark.parametrize("ext_bot", [True, False], ids=["ExtBot", "Bot"]) async def test_blocking_resolution_order( self, bot_info, default_block, ch_block, handler_block, ext_bot ): event = asyncio.Event() async def callback(_, __): await event.wait() event.clear() self.test_flag = True return 1 if handler_block is not None: handler = CommandHandler("start", callback=callback, block=handler_block) fallback = MessageHandler(filters.ALL, callback, block=handler_block) else: handler = CommandHandler("start", callback=callback) fallback = MessageHandler(filters.ALL, callback, block=handler_block) defaults = Defaults(block=default_block) if default_block is not None else None if ch_block is not None: conv_handler = ConversationHandler( entry_points=[handler], states={1: [handler]}, fallbacks=[fallback], block=ch_block, ) else: conv_handler = ConversationHandler( entry_points=[handler], states={1: [handler]}, fallbacks=[fallback], ) bot = make_bot(bot_info, defaults=defaults) if ext_bot else PytestBot(bot_info["token"]) app = ApplicationBuilder().bot(bot).build() app.add_handler(conv_handler) async with app: start_message = make_command_message("/start") start_message.set_bot(bot) fallback_message = make_command_message("/fallback") fallback_message.set_bot(bot) # This loop makes sure that we test all of entry points, states handler & fallbacks for message in [start_message, fallback_message]: process_update_task = asyncio.create_task( app.process_update(Update(0, message=message)) ) if ( # resolution order is handler_block -> ch_block -> default_block # setting block=True/False on a lower priority setting may only have an effect # if it wasn't set for the higher priority settings (handler_block is False) or ((handler_block is None) and (ch_block is False)) or ( (handler_block is None) and (ch_block is None) and ext_bot and (default_block is False) ) ): # check that the handler was called non-blocking by checking that # `process_update` has finished await asyncio.sleep(0.01) assert process_update_task.done() else: # the opposite assert not process_update_task.done() # In any case, the callback must not have finished assert not self.test_flag # After setting the event, the callback must have finished and in the blocking # case this leads to `process_update` finishing. event.set() await asyncio.sleep(0.01) assert process_update_task.done() assert self.test_flag self.test_flag = False async def test_waiting_state(self, app, user1): event = asyncio.Event() async def callback_1(_, __): self.test_flag = 1 async def callback_2(_, __): self.test_flag = 2 async def callback_3(_, __): self.test_flag = 3 async def blocking(_, __): await event.wait() return 1 conv_handler = ConversationHandler( entry_points=[MessageHandler(filters.ALL, callback=blocking, block=False)], states={ ConversationHandler.WAITING: [ MessageHandler(filters.Regex("1"), callback_1), MessageHandler(filters.Regex("2"), callback_2), ], 1: [MessageHandler(filters.Regex("2"), callback_3)], }, fallbacks=[], ) app.add_handler(conv_handler) message = Message( 0, None, self.group, text="/start", from_user=user1, ) message._unfreeze() async with app: await app.process_update(Update(0, message=message)) assert not self.test_flag message.text = "1" await app.process_update(Update(0, message=message)) assert self.test_flag == 1 message.text = "2" await app.process_update(Update(0, message=message)) assert self.test_flag == 2 event.set() await asyncio.sleep(0.05) self.test_flag = None await app.process_update(Update(0, message=message)) assert self.test_flag == 3 python-telegram-bot-21.1.1/tests/ext/test_defaults.py000066400000000000000000000073441460724040100226650ustar00rootroot00000000000000#!/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 inspect import pytest from telegram import LinkPreviewOptions, User from telegram.ext import Defaults from telegram.warnings import PTBDeprecationWarning from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.slots import mro_slots class TestDefaults: def test_slot_behaviour(self): a = Defaults(parse_mode="HTML", quote=True) for attr in a.__slots__: assert getattr(a, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(a)) == len(set(mro_slots(a))), "duplicate slot" def test_utc(self): defaults = Defaults() if not TEST_WITH_OPT_DEPS: assert defaults.tzinfo is dtm.timezone.utc else: assert defaults.tzinfo is not dtm.timezone.utc def test_data_assignment(self): defaults = Defaults() for name, _val in inspect.getmembers(Defaults, lambda x: isinstance(x, property)): with pytest.raises(AttributeError): setattr(defaults, name, True) def test_equality(self): a = Defaults(parse_mode="HTML", do_quote=True) b = Defaults(parse_mode="HTML", do_quote=True) c = Defaults(parse_mode="HTML", do_quote=True, protect_content=True) d = Defaults(parse_mode="HTML", protect_content=True) e = User(123, "test_user", False) f = Defaults(parse_mode="HTML", disable_web_page_preview=True) g = Defaults(parse_mode="HTML", disable_web_page_preview=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 f == g assert hash(f) == hash(g) def test_mutually_exclusive(self): with pytest.raises(ValueError, match="mutually exclusive"): Defaults(disable_web_page_preview=True, link_preview_options=LinkPreviewOptions(False)) with pytest.raises(ValueError, match="mutually exclusive"): Defaults(quote=True, do_quote=False) def test_deprecation_warning_for_disable_web_page_preview(self): with pytest.warns( PTBDeprecationWarning, match="`Defaults.disable_web_page_preview` is " ) as record: Defaults(disable_web_page_preview=True) assert record[0].filename == __file__, "wrong stacklevel!" assert Defaults(disable_web_page_preview=True).link_preview_options.is_disabled is True assert Defaults(disable_web_page_preview=False).disable_web_page_preview is False def test_deprecation_warning_for_quote(self): with pytest.warns(PTBDeprecationWarning, match="`Defaults.quote` is ") as record: Defaults(quote=True) assert record[0].filename == __file__, "wrong stacklevel!" assert Defaults(quote=True).do_quote is True assert Defaults(quote=False).quote is False python-telegram-bot-21.1.1/tests/ext/test_dictpersistence.py000066400000000000000000000376011460724040100242450ustar00rootroot00000000000000#!/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 json import pytest from telegram.ext import DictPersistence from tests.auxil.slots import mro_slots @pytest.fixture(autouse=True) def _reset_callback_data_cache(cdc_bot): yield cdc_bot.callback_data_cache.clear_callback_data() cdc_bot.callback_data_cache.clear_callback_queries() @pytest.fixture() def bot_data(): return {"test1": "test2", "test3": {"test4": "test5"}} @pytest.fixture() def chat_data(): return {-12345: {"test1": "test2", "test3": {"test4": "test5"}}, -67890: {3: "test4"}} @pytest.fixture() def user_data(): return {12345: {"test1": "test2", "test3": {"test4": "test5"}}, 67890: {3: "test4"}} @pytest.fixture() def callback_data(): return [("test1", 1000, {"button1": "test0", "button2": "test1"})], {"test1": "test2"} @pytest.fixture() def conversations(): return { "name1": {(123, 123): 3, (456, 654): 4}, "name2": {(123, 321): 1, (890, 890): 2}, "name3": {(123, 321): 1, (890, 890): 2}, } @pytest.fixture() def user_data_json(user_data): return json.dumps(user_data) @pytest.fixture() def chat_data_json(chat_data): return json.dumps(chat_data) @pytest.fixture() def bot_data_json(bot_data): return json.dumps(bot_data) @pytest.fixture() def callback_data_json(callback_data): return json.dumps(callback_data) @pytest.fixture() def conversations_json(conversations): return """{"name1": {"[123, 123]": 3, "[456, 654]": 4}, "name2": {"[123, 321]": 1, "[890, 890]": 2}, "name3": {"[123, 321]": 1, "[890, 890]": 2}}""" class TestDictPersistence: """Just tests the DictPersistence interface. Integration of persistence into Applictation is tested in TestBasePersistence!""" async def test_slot_behaviour(self): inst = DictPersistence() 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_json_given(self): dict_persistence = DictPersistence() assert await dict_persistence.get_user_data() == {} assert await dict_persistence.get_chat_data() == {} assert await dict_persistence.get_bot_data() == {} assert await dict_persistence.get_callback_data() is None assert await dict_persistence.get_conversations("noname") == {} async def test_bad_json_string_given(self): bad_user_data = "thisisnojson99900()))(" bad_chat_data = "thisisnojson99900()))(" bad_bot_data = "thisisnojson99900()))(" bad_callback_data = "thisisnojson99900()))(" bad_conversations = "thisisnojson99900()))(" with pytest.raises(TypeError, match="user_data"): DictPersistence(user_data_json=bad_user_data) with pytest.raises(TypeError, match="chat_data"): DictPersistence(chat_data_json=bad_chat_data) with pytest.raises(TypeError, match="bot_data"): DictPersistence(bot_data_json=bad_bot_data) with pytest.raises(TypeError, match="callback_data"): DictPersistence(callback_data_json=bad_callback_data) with pytest.raises(TypeError, match="conversations"): DictPersistence(conversations_json=bad_conversations) async def test_invalid_json_string_given(self): bad_user_data = '["this", "is", "json"]' bad_chat_data = '["this", "is", "json"]' bad_bot_data = '["this", "is", "json"]' bad_conversations = '["this", "is", "json"]' bad_callback_data_1 = '[[["str", 3.14, {"di": "ct"}]], "is"]' bad_callback_data_2 = '[[["str", "non-float", {"di": "ct"}]], {"di": "ct"}]' bad_callback_data_3 = '[[[{"not": "a str"}, 3.14, {"di": "ct"}]], {"di": "ct"}]' bad_callback_data_4 = '[[["wrong", "length"]], {"di": "ct"}]' bad_callback_data_5 = '["this", "is", "json"]' with pytest.raises(TypeError, match="user_data"): DictPersistence(user_data_json=bad_user_data) with pytest.raises(TypeError, match="chat_data"): DictPersistence(chat_data_json=bad_chat_data) with pytest.raises(TypeError, match="bot_data"): DictPersistence(bot_data_json=bad_bot_data) for bad_callback_data in [ bad_callback_data_1, bad_callback_data_2, bad_callback_data_3, bad_callback_data_4, bad_callback_data_5, ]: with pytest.raises(TypeError, match="callback_data"): DictPersistence(callback_data_json=bad_callback_data) with pytest.raises(TypeError, match="conversations"): DictPersistence(conversations_json=bad_conversations) async def test_good_json_input( self, user_data_json, chat_data_json, bot_data_json, conversations_json, callback_data_json ): dict_persistence = DictPersistence( user_data_json=user_data_json, chat_data_json=chat_data_json, bot_data_json=bot_data_json, conversations_json=conversations_json, callback_data_json=callback_data_json, ) user_data = await dict_persistence.get_user_data() assert isinstance(user_data, dict) assert user_data[12345]["test1"] == "test2" assert user_data[67890][3] == "test4" chat_data = await dict_persistence.get_chat_data() assert isinstance(chat_data, dict) assert chat_data[-12345]["test1"] == "test2" assert chat_data[-67890][3] == "test4" bot_data = await dict_persistence.get_bot_data() assert isinstance(bot_data, dict) assert bot_data["test1"] == "test2" assert bot_data["test3"]["test4"] == "test5" assert "test6" not in bot_data callback_data = await dict_persistence.get_callback_data() assert isinstance(callback_data, tuple) assert callback_data[0] == [("test1", 1000, {"button1": "test0", "button2": "test1"})] assert callback_data[1] == {"test1": "test2"} conversation1 = await dict_persistence.get_conversations("name1") assert isinstance(conversation1, dict) assert conversation1[(123, 123)] == 3 assert conversation1[(456, 654)] == 4 with pytest.raises(KeyError): conversation1[(890, 890)] conversation2 = await dict_persistence.get_conversations("name2") assert isinstance(conversation1, dict) assert conversation2[(123, 321)] == 1 assert conversation2[(890, 890)] == 2 with pytest.raises(KeyError): conversation2[(123, 123)] async def test_good_json_input_callback_data_none(self): dict_persistence = DictPersistence(callback_data_json="null") assert dict_persistence.callback_data is None assert dict_persistence.callback_data_json == "null" async def test_dict_outputs( self, user_data, user_data_json, chat_data, chat_data_json, bot_data, bot_data_json, callback_data_json, conversations, conversations_json, ): dict_persistence = DictPersistence( user_data_json=user_data_json, chat_data_json=chat_data_json, bot_data_json=bot_data_json, callback_data_json=callback_data_json, conversations_json=conversations_json, ) assert dict_persistence.user_data == user_data assert dict_persistence.chat_data == chat_data assert dict_persistence.bot_data == bot_data assert dict_persistence.bot_data == bot_data assert dict_persistence.conversations == conversations async def test_json_outputs( self, user_data_json, chat_data_json, bot_data_json, callback_data_json, conversations_json ): dict_persistence = DictPersistence( user_data_json=user_data_json, chat_data_json=chat_data_json, bot_data_json=bot_data_json, callback_data_json=callback_data_json, conversations_json=conversations_json, ) assert dict_persistence.user_data_json == user_data_json assert dict_persistence.chat_data_json == chat_data_json assert dict_persistence.callback_data_json == callback_data_json assert dict_persistence.conversations_json == conversations_json async def test_updating( self, user_data_json, chat_data_json, bot_data_json, callback_data, callback_data_json, conversations, conversations_json, ): dict_persistence = DictPersistence( user_data_json=user_data_json, chat_data_json=chat_data_json, bot_data_json=bot_data_json, callback_data_json=callback_data_json, conversations_json=conversations_json, ) user_data = await dict_persistence.get_user_data() user_data[12345]["test3"]["test4"] = "test6" assert dict_persistence.user_data != user_data assert dict_persistence.user_data_json != json.dumps(user_data) await dict_persistence.update_user_data(12345, user_data[12345]) assert dict_persistence.user_data == user_data assert dict_persistence.user_data_json == json.dumps(user_data) await dict_persistence.drop_user_data(67890) assert 67890 not in dict_persistence.user_data dict_persistence._user_data = None await dict_persistence.drop_user_data(123) assert isinstance(await dict_persistence.get_user_data(), dict) chat_data = await dict_persistence.get_chat_data() chat_data[-12345]["test3"]["test4"] = "test6" assert dict_persistence.chat_data != chat_data assert dict_persistence.chat_data_json != json.dumps(chat_data) await dict_persistence.update_chat_data(-12345, chat_data[-12345]) assert dict_persistence.chat_data == chat_data assert dict_persistence.chat_data_json == json.dumps(chat_data) await dict_persistence.drop_chat_data(-67890) assert -67890 not in dict_persistence.chat_data dict_persistence._chat_data = None await dict_persistence.drop_chat_data(123) assert isinstance(await dict_persistence.get_chat_data(), dict) bot_data = await dict_persistence.get_bot_data() bot_data["test3"]["test4"] = "test6" assert dict_persistence.bot_data != bot_data assert dict_persistence.bot_data_json != json.dumps(bot_data) await dict_persistence.update_bot_data(bot_data) assert dict_persistence.bot_data == bot_data assert dict_persistence.bot_data_json == json.dumps(bot_data) callback_data = await dict_persistence.get_callback_data() callback_data[1]["test3"] = "test4" callback_data[0][0][2]["button2"] = "test41" assert dict_persistence.callback_data != callback_data assert dict_persistence.callback_data_json != json.dumps(callback_data) await dict_persistence.update_callback_data(callback_data) assert dict_persistence.callback_data == callback_data assert dict_persistence.callback_data_json == json.dumps(callback_data) conversation1 = await dict_persistence.get_conversations("name1") conversation1[(123, 123)] = 5 assert dict_persistence.conversations["name1"] != conversation1 await dict_persistence.update_conversation("name1", (123, 123), 5) assert dict_persistence.conversations["name1"] == conversation1 conversations["name1"][(123, 123)] = 5 assert ( dict_persistence.conversations_json == DictPersistence._encode_conversations_to_json(conversations) ) assert await dict_persistence.get_conversations("name1") == conversation1 dict_persistence._conversations = None await dict_persistence.update_conversation("name1", (123, 123), 5) assert dict_persistence.conversations["name1"] == {(123, 123): 5} assert await dict_persistence.get_conversations("name1") == {(123, 123): 5} assert ( dict_persistence.conversations_json == DictPersistence._encode_conversations_to_json({"name1": {(123, 123): 5}}) ) async def test_no_data_on_init( self, bot_data, user_data, chat_data, conversations, callback_data ): dict_persistence = DictPersistence() assert dict_persistence.user_data is None assert dict_persistence.chat_data is None assert dict_persistence.bot_data is None assert dict_persistence.conversations is None assert dict_persistence.callback_data is None assert dict_persistence.user_data_json == "null" assert dict_persistence.chat_data_json == "null" assert dict_persistence.bot_data_json == "null" assert dict_persistence.conversations_json == "null" assert dict_persistence.callback_data_json == "null" await dict_persistence.update_bot_data(bot_data) await dict_persistence.update_user_data(12345, user_data[12345]) await dict_persistence.update_chat_data(-12345, chat_data[-12345]) await dict_persistence.update_conversation("name", (1, 1), "new_state") await dict_persistence.update_callback_data(callback_data) assert dict_persistence.user_data[12345] == user_data[12345] assert dict_persistence.chat_data[-12345] == chat_data[-12345] assert dict_persistence.bot_data == bot_data assert dict_persistence.conversations["name"] == {(1, 1): "new_state"} assert dict_persistence.callback_data == callback_data async def test_no_json_dumping_if_data_did_not_change( self, bot_data, user_data, chat_data, conversations, callback_data, monkeypatch ): dict_persistence = DictPersistence() await dict_persistence.update_bot_data(bot_data) await dict_persistence.update_user_data(12345, user_data[12345]) await dict_persistence.update_chat_data(-12345, chat_data[-12345]) await dict_persistence.update_conversation("name", (1, 1), "new_state") await dict_persistence.update_callback_data(callback_data) assert dict_persistence.user_data_json == json.dumps({12345: user_data[12345]}) assert dict_persistence.chat_data_json == json.dumps({-12345: chat_data[-12345]}) assert dict_persistence.bot_data_json == json.dumps(bot_data) assert ( dict_persistence.conversations_json == DictPersistence._encode_conversations_to_json({"name": {(1, 1): "new_state"}}) ) assert dict_persistence.callback_data_json == json.dumps(callback_data) flag = False def dumps(*args, **kwargs): nonlocal flag flag = True # Since the data doesn't change, json.dumps shoduln't be called beyond this point! monkeypatch.setattr(json, "dumps", dumps) await dict_persistence.update_bot_data(bot_data) await dict_persistence.update_user_data(12345, user_data[12345]) await dict_persistence.update_chat_data(-12345, chat_data[-12345]) await dict_persistence.update_conversation("name", (1, 1), "new_state") await dict_persistence.update_callback_data(callback_data) assert not flag python-telegram-bot-21.1.1/tests/ext/test_filters.py000066400000000000000000003476671460724040100225450ustar00rootroot00000000000000#!/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 re import pytest from telegram import ( CallbackQuery, Chat, Dice, Document, File, Message, MessageEntity, MessageOriginChannel, MessageOriginChat, MessageOriginHiddenUser, MessageOriginUser, Sticker, SuccessfulPayment, Update, User, ) from telegram.ext import filters from tests.auxil.slots import mro_slots @pytest.fixture() def update(): update = Update( 0, Message( 0, datetime.datetime.utcnow(), Chat(0, "private"), from_user=User(0, "Testuser", False), via_bot=User(0, "Testbot", True), sender_chat=Chat(0, "Channel"), forward_origin=MessageOriginUser( datetime.datetime.utcnow(), User(0, "Testuser", False) ), ), ) update._unfreeze() update.message._unfreeze() update.message.chat._unfreeze() update.message.from_user._unfreeze() update.message.via_bot._unfreeze() update.message.sender_chat._unfreeze() update.message.forward_origin._unfreeze() update.message.forward_origin.sender_user._unfreeze() return update @pytest.fixture(params=MessageEntity.ALL_TYPES) def message_entity(request): return MessageEntity(request.param, 0, 0, url="", user=User(1, "first_name", False)) @pytest.fixture( scope="class", params=[{"class": filters.MessageFilter}, {"class": filters.UpdateFilter}], ids=["MessageFilter", "UpdateFilter"], ) def base_class(request): return request.param["class"] @pytest.fixture(scope="class") def message_origin_user(): return MessageOriginUser(datetime.datetime.utcnow(), User(1, "TestOther", False)) class TestFilters: def test_all_filters_slot_behaviour(self): """ Use depth first search to get all nested filters, and instantiate them (which need it) with the correct number of arguments, then test each filter separately. Also tests setting custom attributes on custom filters. """ def filter_class(obj): return bool(inspect.isclass(obj) and "filters" in repr(obj)) # The total no. of filters is about 72 as of 31/10/21. # Gather all the filters to test using DFS- visited = [] classes = inspect.getmembers(filters, predicate=filter_class) # List[Tuple[str, type]] stack = classes.copy() while stack: cls = stack[-1][-1] # get last element and its class for inner_cls in inspect.getmembers( cls, # Get inner filters lambda a: inspect.isclass(a) and not issubclass(a, cls.__class__), # noqa: B023 ): if inner_cls[1] not in visited: stack.append(inner_cls) visited.append(inner_cls[1]) classes.append(inner_cls) break else: stack.pop() # Now start the actual testing for name, cls in classes: # Can't instantiate abstract classes without overriding methods, so skip them for now exclude = {"_MergedFilter", "_XORFilter"} if inspect.isabstract(cls) or name in {"__class__", "__base__"} | exclude: continue assert "__slots__" in cls.__dict__, f"Filter {name!r} doesn't have __slots__" # get no. of args minus the 'self', 'args' and 'kwargs' argument init_sig = inspect.signature(cls.__init__).parameters extra = 0 for param in init_sig: if param in {"self", "args", "kwargs"}: extra += 1 args = len(init_sig) - extra if not args: inst = cls() elif args == 1: inst = cls("1") else: inst = cls(*["blah"]) assert len(mro_slots(inst)) == len(set(mro_slots(inst))), f"same slot in {name}" for attr in cls.__slots__: assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}' for {name}" def test__all__(self): expected = { key for key, member in filters.__dict__.items() if ( not key.startswith("_") # exclude imported stuff and getattr(member, "__module__", "unknown module") == "telegram.ext.filters" and key != "sys" ) } actual = set(filters.__all__) assert ( actual == expected ), f"Members {expected - actual} were not listed in constants.__all__" def test_filters_all(self, update): assert filters.ALL.check_update(update) def test_filters_text(self, update): update.message.text = "test" assert filters.TEXT.check_update(update) update.message.text = "/test" assert filters.Text().check_update(update) def test_filters_text_strings(self, update): update.message.text = "/test" assert filters.Text(("/test", "test1")).check_update(update) assert not filters.Text(["test1", "test2"]).check_update(update) def test_filters_caption(self, update): update.message.caption = "test" assert filters.CAPTION.check_update(update) update.message.caption = None assert not filters.CAPTION.check_update(update) def test_filters_caption_strings(self, update): update.message.caption = "test" assert filters.Caption(("test", "test1")).check_update(update) assert not filters.Caption(["test1", "test2"]).check_update(update) def test_filters_command_default(self, update): update.message.text = "test" assert not filters.COMMAND.check_update(update) update.message.text = "/test" assert not filters.COMMAND.check_update(update) # Only accept commands at the beginning update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 3, 5)] assert not filters.COMMAND.check_update(update) update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] assert filters.COMMAND.check_update(update) def test_filters_command_anywhere(self, update): update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 5, 4)] assert filters.Command(False).check_update(update) def test_filters_regex(self, update): sre_type = type(re.match("", "")) update.message.text = "/start deep-linked param" result = filters.Regex(r"deep-linked param").check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert type(matches[0]) is sre_type update.message.text = "/help" assert filters.Regex(r"help").check_update(update) update.message.text = "test" assert not filters.Regex(r"fail").check_update(update) assert filters.Regex(r"test").check_update(update) assert filters.Regex(re.compile(r"test")).check_update(update) assert filters.Regex(re.compile(r"TEST", re.IGNORECASE)).check_update(update) update.message.text = "i love python" assert filters.Regex(r".\b[lo]{2}ve python").check_update(update) update.message.text = None assert not filters.Regex(r"fail").check_update(update) def test_filters_regex_multiple(self, update): sre_type = type(re.match("", "")) update.message.text = "/start deep-linked param" result = (filters.Regex("deep") & filters.Regex(r"linked param")).check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) result = (filters.Regex("deep") | filters.Regex(r"linked param")).check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) result = (filters.Regex("not int") | filters.Regex(r"linked param")).check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) result = (filters.Regex("not int") & filters.Regex(r"linked param")).check_update(update) assert not result def test_filters_merged_with_regex(self, update): sre_type = type(re.match("", "")) update.message.text = "/start deep-linked param" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = (filters.COMMAND & filters.Regex(r"linked param")).check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) result = (filters.Regex(r"linked param") & filters.COMMAND).check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) result = (filters.Regex(r"linked param") | filters.COMMAND).check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) # Should not give a match since it's a or filter and it short circuits result = (filters.COMMAND | filters.Regex(r"linked param")).check_update(update) assert result is True def test_regex_complex_merges(self, update, message_origin_user): sre_type = type(re.match("", "")) update.message.text = "test it out" test_filter = filters.Regex("test") & ( (filters.StatusUpdate.ALL | filters.AUDIO) | filters.Regex("out") ) result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert len(matches) == 2 assert all(type(res) is sre_type for res in matches) update.message.audio = "test" result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) update.message.text = "test it" result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) update.message.audio = None result = test_filter.check_update(update) assert not result update.message.text = "test it out" result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) update.message.pinned_message = True result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) update.message.text = "it out" result = test_filter.check_update(update) assert not result update.message.text = "test it out" update.message.forward_origin = None update.message.pinned_message = None test_filter = (filters.Regex("test") | filters.COMMAND) & ( filters.Regex("it") | filters.StatusUpdate.ALL ) result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert len(matches) == 2 assert all(type(res) is sre_type for res in matches) update.message.text = "test" result = test_filter.check_update(update) assert not result update.message.pinned_message = True result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert len(matches) == 1 assert all(type(res) is sre_type for res in matches) update.message.text = "nothing" result = test_filter.check_update(update) assert not result update.message.text = "/start" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = test_filter.check_update(update) assert result assert isinstance(result, bool) update.message.text = "/start it" result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert len(matches) == 1 assert all(type(res) is sre_type for res in matches) def test_regex_inverted(self, update): update.message.text = "/start deep-linked param" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] inv = ~filters.Regex(r"deep-linked param") result = inv.check_update(update) assert not result update.message.text = "not it" result = inv.check_update(update) assert result assert isinstance(result, bool) inv = ~filters.Regex("linked") & filters.COMMAND update.message.text = "it's linked" result = inv.check_update(update) assert not result update.message.text = "/start" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = inv.check_update(update) assert result update.message.text = "/linked" result = inv.check_update(update) assert not result inv = ~filters.Regex("linked") | filters.COMMAND update.message.text = "it's linked" update.message.entities = [] result = inv.check_update(update) assert not result update.message.text = "/start linked" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = inv.check_update(update) assert result update.message.text = "/start" result = inv.check_update(update) assert result update.message.text = "nothig" update.message.entities = [] result = inv.check_update(update) assert result def test_filters_caption_regex(self, update): sre_type = type(re.match("", "")) update.message.caption = "/start deep-linked param" result = filters.CaptionRegex(r"deep-linked param").check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert type(matches[0]) is sre_type update.message.caption = "/help" assert filters.CaptionRegex(r"help").check_update(update) update.message.caption = "test" assert not filters.CaptionRegex(r"fail").check_update(update) assert filters.CaptionRegex(r"test").check_update(update) assert filters.CaptionRegex(re.compile(r"test")).check_update(update) assert filters.CaptionRegex(re.compile(r"TEST", re.IGNORECASE)).check_update(update) update.message.caption = "i love python" assert filters.CaptionRegex(r".\b[lo]{2}ve python").check_update(update) update.message.caption = None assert not filters.CaptionRegex(r"fail").check_update(update) def test_filters_caption_regex_multiple(self, update): sre_type = type(re.match("", "")) update.message.caption = "/start deep-linked param" _and = filters.CaptionRegex("deep") & filters.CaptionRegex(r"linked param") result = _and.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) _or = filters.CaptionRegex("deep") | filters.CaptionRegex(r"linked param") result = _or.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) _or = filters.CaptionRegex("not int") | filters.CaptionRegex(r"linked param") result = _or.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) _and = filters.CaptionRegex("not int") & filters.CaptionRegex(r"linked param") result = _and.check_update(update) assert not result def test_filters_merged_with_caption_regex(self, update): sre_type = type(re.match("", "")) update.message.caption = "/start deep-linked param" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = (filters.COMMAND & filters.CaptionRegex(r"linked param")).check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) result = (filters.CaptionRegex(r"linked param") & filters.COMMAND).check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) result = (filters.CaptionRegex(r"linked param") | filters.COMMAND).check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) # Should not give a match since it's a or filter and it short circuits result = (filters.COMMAND | filters.CaptionRegex(r"linked param")).check_update(update) assert result is True def test_caption_regex_complex_merges(self, update, message_origin_user): sre_type = type(re.match("", "")) update.message.caption = "test it out" test_filter = filters.CaptionRegex("test") & ( (filters.StatusUpdate.ALL | filters.AUDIO) | filters.CaptionRegex("out") ) result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert len(matches) == 2 assert all(type(res) is sre_type for res in matches) update.message.audio = "test" result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) update.message.caption = "test it" result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) update.message.audio = None result = test_filter.check_update(update) assert not result update.message.caption = "test it out" result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) update.message.pinned_message = True result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert all(type(res) is sre_type for res in matches) update.message.caption = "it out" result = test_filter.check_update(update) assert not result update.message.caption = "test it out" update.message.forward_origin = None update.message.pinned_message = None test_filter = (filters.CaptionRegex("test") | filters.COMMAND) & ( filters.CaptionRegex("it") | filters.StatusUpdate.ALL ) result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert len(matches) == 2 assert all(type(res) is sre_type for res in matches) update.message.caption = "test" result = test_filter.check_update(update) assert not result update.message.pinned_message = True result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert len(matches) == 1 assert all(type(res) is sre_type for res in matches) update.message.caption = "nothing" result = test_filter.check_update(update) assert not result update.message.caption = "/start" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = test_filter.check_update(update) assert result assert isinstance(result, bool) update.message.caption = "/start it" result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert len(matches) == 1 assert all(type(res) is sre_type for res in matches) def test_caption_regex_inverted(self, update): update.message.caption = "/start deep-linked param" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] test_filter = ~filters.CaptionRegex(r"deep-linked param") result = test_filter.check_update(update) assert not result update.message.caption = "not it" result = test_filter.check_update(update) assert result assert isinstance(result, bool) test_filter = ~filters.CaptionRegex("linked") & filters.COMMAND update.message.caption = "it's linked" result = test_filter.check_update(update) assert not result update.message.caption = "/start" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = test_filter.check_update(update) assert result update.message.caption = "/linked" result = test_filter.check_update(update) assert not result test_filter = ~filters.CaptionRegex("linked") | filters.COMMAND update.message.caption = "it's linked" update.message.entities = [] result = test_filter.check_update(update) assert not result update.message.caption = "/start linked" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = test_filter.check_update(update) assert result update.message.caption = "/start" result = test_filter.check_update(update) assert result update.message.caption = "nothig" update.message.entities = [] result = test_filter.check_update(update) assert result def test_filters_reply(self, update): another_message = Message( 1, datetime.datetime.utcnow(), Chat(0, "private"), from_user=User(1, "TestOther", False), ) update.message.text = "test" assert not filters.REPLY.check_update(update) update.message.reply_to_message = another_message assert filters.REPLY.check_update(update) def test_filters_audio(self, update): assert not filters.AUDIO.check_update(update) update.message.audio = "test" assert filters.AUDIO.check_update(update) def test_filters_document(self, update): assert not filters.Document.ALL.check_update(update) update.message.document = "test" assert filters.Document.ALL.check_update(update) def test_filters_document_type(self, update): update.message.document = Document( "file_id", "unique_id", mime_type="application/vnd.android.package-archive" ) update.message.document._unfreeze() assert filters.Document.APK.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.DOC.check_update(update) assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "application/msword" assert filters.Document.DOC.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.DOCX.check_update(update) assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = ( "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ) assert filters.Document.DOCX.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.EXE.check_update(update) assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "application/octet-stream" assert filters.Document.EXE.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.DOCX.check_update(update) assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "image/gif" assert filters.Document.GIF.check_update(update) assert filters.Document.IMAGE.check_update(update) assert not filters.Document.JPG.check_update(update) assert not filters.Document.TEXT.check_update(update) update.message.document.mime_type = "image/jpeg" assert filters.Document.JPG.check_update(update) assert filters.Document.IMAGE.check_update(update) assert not filters.Document.MP3.check_update(update) assert not filters.Document.VIDEO.check_update(update) update.message.document.mime_type = "audio/mpeg" assert filters.Document.MP3.check_update(update) assert filters.Document.AUDIO.check_update(update) assert not filters.Document.PDF.check_update(update) assert not filters.Document.IMAGE.check_update(update) update.message.document.mime_type = "application/pdf" assert filters.Document.PDF.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.PY.check_update(update) assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "text/x-python" assert filters.Document.PY.check_update(update) assert filters.Document.TEXT.check_update(update) assert not filters.Document.SVG.check_update(update) assert not filters.Document.APPLICATION.check_update(update) update.message.document.mime_type = "image/svg+xml" assert filters.Document.SVG.check_update(update) assert filters.Document.IMAGE.check_update(update) assert not filters.Document.TXT.check_update(update) assert not filters.Document.VIDEO.check_update(update) update.message.document.mime_type = "text/plain" assert filters.Document.TXT.check_update(update) assert filters.Document.TEXT.check_update(update) assert not filters.Document.TARGZ.check_update(update) assert not filters.Document.APPLICATION.check_update(update) update.message.document.mime_type = "application/x-compressed-tar" assert filters.Document.TARGZ.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.WAV.check_update(update) assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "audio/x-wav" assert filters.Document.WAV.check_update(update) assert filters.Document.AUDIO.check_update(update) assert not filters.Document.XML.check_update(update) assert not filters.Document.IMAGE.check_update(update) update.message.document.mime_type = "text/xml" assert filters.Document.XML.check_update(update) assert filters.Document.TEXT.check_update(update) assert not filters.Document.ZIP.check_update(update) assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "application/zip" assert filters.Document.ZIP.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.APK.check_update(update) assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "image/x-rgb" assert not filters.Document.Category("application/").check_update(update) assert not filters.Document.MimeType("application/x-sh").check_update(update) update.message.document.mime_type = "application/x-sh" assert filters.Document.Category("application/").check_update(update) assert filters.Document.MimeType("application/x-sh").check_update(update) update.message.document.mime_type = None assert not filters.Document.Category("application/").check_update(update) assert not filters.Document.MimeType("application/x-sh").check_update(update) def test_filters_file_extension_basic(self, update): update.message.document = Document( "file_id", "unique_id", file_name="file.jpg", mime_type="image/jpeg", ) update.message.document._unfreeze() assert filters.Document.FileExtension("jpg").check_update(update) assert not filters.Document.FileExtension("jpeg").check_update(update) assert not filters.Document.FileExtension("file.jpg").check_update(update) update.message.document.file_name = "file.tar.gz" assert filters.Document.FileExtension("tar.gz").check_update(update) assert filters.Document.FileExtension("gz").check_update(update) assert not filters.Document.FileExtension("tgz").check_update(update) assert not filters.Document.FileExtension("jpg").check_update(update) update.message.document.file_name = None assert not filters.Document.FileExtension("jpg").check_update(update) update.message.document = None assert not filters.Document.FileExtension("jpg").check_update(update) def test_filters_file_extension_minds_dots(self, update): update.message.document = Document( "file_id", "unique_id", file_name="file.jpg", mime_type="image/jpeg", ) update.message.document._unfreeze() assert not filters.Document.FileExtension(".jpg").check_update(update) assert not filters.Document.FileExtension("e.jpg").check_update(update) assert not filters.Document.FileExtension("file.jpg").check_update(update) assert not filters.Document.FileExtension("").check_update(update) update.message.document.file_name = "file..jpg" assert filters.Document.FileExtension("jpg").check_update(update) assert filters.Document.FileExtension(".jpg").check_update(update) assert not filters.Document.FileExtension("..jpg").check_update(update) update.message.document.file_name = "file.docx" assert filters.Document.FileExtension("docx").check_update(update) assert not filters.Document.FileExtension("doc").check_update(update) assert not filters.Document.FileExtension("ocx").check_update(update) update.message.document.file_name = "file" assert not filters.Document.FileExtension("").check_update(update) assert not filters.Document.FileExtension("file").check_update(update) update.message.document.file_name = "file." assert filters.Document.FileExtension("").check_update(update) def test_filters_file_extension_none_arg(self, update): update.message.document = Document( "file_id", "unique_id", file_name="file.jpg", mime_type="image/jpeg", ) update.message.document._unfreeze() assert not filters.Document.FileExtension(None).check_update(update) update.message.document.file_name = "file" assert filters.Document.FileExtension(None).check_update(update) assert not filters.Document.FileExtension("None").check_update(update) update.message.document.file_name = "file." assert not filters.Document.FileExtension(None).check_update(update) update.message.document = None assert not filters.Document.FileExtension(None).check_update(update) def test_filters_file_extension_case_sensitivity(self, update): update.message.document = Document( "file_id", "unique_id", file_name="file.jpg", mime_type="image/jpeg", ) update.message.document._unfreeze() assert filters.Document.FileExtension("JPG").check_update(update) assert filters.Document.FileExtension("jpG").check_update(update) update.message.document.file_name = "file.JPG" assert filters.Document.FileExtension("jpg").check_update(update) assert not filters.Document.FileExtension("jpg", case_sensitive=True).check_update(update) update.message.document.file_name = "file.Dockerfile" assert filters.Document.FileExtension("Dockerfile", case_sensitive=True).check_update( update ) assert not filters.Document.FileExtension("DOCKERFILE", case_sensitive=True).check_update( update ) def test_filters_file_extension_name(self): assert ( filters.Document.FileExtension("jpg").name == "filters.Document.FileExtension('jpg')" ) assert ( filters.Document.FileExtension("JPG").name == "filters.Document.FileExtension('jpg')" ) assert ( filters.Document.FileExtension("jpg", case_sensitive=True).name == "filters.Document.FileExtension('jpg', case_sensitive=True)" ) assert ( filters.Document.FileExtension("JPG", case_sensitive=True).name == "filters.Document.FileExtension('JPG', case_sensitive=True)" ) assert ( filters.Document.FileExtension(".jpg").name == "filters.Document.FileExtension('.jpg')" ) assert filters.Document.FileExtension("").name == "filters.Document.FileExtension('')" assert filters.Document.FileExtension(None).name == "filters.Document.FileExtension(None)" def test_filters_animation(self, update): assert not filters.ANIMATION.check_update(update) update.message.animation = "test" assert filters.ANIMATION.check_update(update) def test_filters_photo(self, update): assert not filters.PHOTO.check_update(update) update.message.photo = "test" assert filters.PHOTO.check_update(update) def test_filters_sticker(self, update): assert not filters.Sticker.ALL.check_update(update) update.message.sticker = Sticker("1", "uniq", 1, 2, False, False, Sticker.REGULAR) update.message.sticker._unfreeze() assert filters.Sticker.ALL.check_update(update) assert filters.Sticker.STATIC.check_update(update) assert not filters.Sticker.VIDEO.check_update(update) assert not filters.Sticker.PREMIUM.check_update(update) update.message.sticker.is_animated = True assert filters.Sticker.ANIMATED.check_update(update) assert not filters.Sticker.VIDEO.check_update(update) assert not filters.Sticker.STATIC.check_update(update) assert not filters.Sticker.PREMIUM.check_update(update) update.message.sticker.is_animated = False update.message.sticker.is_video = True assert not filters.Sticker.ANIMATED.check_update(update) assert not filters.Sticker.STATIC.check_update(update) assert filters.Sticker.VIDEO.check_update(update) assert not filters.Sticker.PREMIUM.check_update(update) update.message.sticker.premium_animation = File("string", "uniqueString") assert not filters.Sticker.ANIMATED.check_update(update) # premium stickers can be animated, video, or probably also static, # it doesn't really matter for the test assert not filters.Sticker.STATIC.check_update(update) assert filters.Sticker.VIDEO.check_update(update) assert filters.Sticker.PREMIUM.check_update(update) def test_filters_story(self, update): assert not filters.STORY.check_update(update) update.message.story = "test" assert filters.STORY.check_update(update) def test_filters_video(self, update): assert not filters.VIDEO.check_update(update) update.message.video = "test" assert filters.VIDEO.check_update(update) def test_filters_voice(self, update): assert not filters.VOICE.check_update(update) update.message.voice = "test" assert filters.VOICE.check_update(update) def test_filters_video_note(self, update): assert not filters.VIDEO_NOTE.check_update(update) update.message.video_note = "test" assert filters.VIDEO_NOTE.check_update(update) def test_filters_contact(self, update): assert not filters.CONTACT.check_update(update) update.message.contact = "test" assert filters.CONTACT.check_update(update) def test_filters_location(self, update): assert not filters.LOCATION.check_update(update) update.message.location = "test" assert filters.LOCATION.check_update(update) def test_filters_venue(self, update): assert not filters.VENUE.check_update(update) update.message.venue = "test" assert filters.VENUE.check_update(update) def test_filters_status_update(self, update): assert not filters.StatusUpdate.ALL.check_update(update) update.message.new_chat_members = ["test"] assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.NEW_CHAT_MEMBERS.check_update(update) update.message.new_chat_members = None update.message.left_chat_member = "test" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.LEFT_CHAT_MEMBER.check_update(update) update.message.left_chat_member = None update.message.new_chat_title = "test" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.NEW_CHAT_TITLE.check_update(update) update.message.new_chat_title = "" update.message.new_chat_photo = "test" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.NEW_CHAT_PHOTO.check_update(update) update.message.new_chat_photo = None update.message.delete_chat_photo = True assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.DELETE_CHAT_PHOTO.check_update(update) update.message.delete_chat_photo = False update.message.group_chat_created = True assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.CHAT_CREATED.check_update(update) update.message.group_chat_created = False update.message.supergroup_chat_created = True assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.CHAT_CREATED.check_update(update) update.message.supergroup_chat_created = False update.message.channel_chat_created = True assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.CHAT_CREATED.check_update(update) update.message.channel_chat_created = False update.message.message_auto_delete_timer_changed = True assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.MESSAGE_AUTO_DELETE_TIMER_CHANGED.check_update(update) update.message.message_auto_delete_timer_changed = False update.message.migrate_to_chat_id = 100 assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.MIGRATE.check_update(update) update.message.migrate_to_chat_id = 0 update.message.migrate_from_chat_id = 100 assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.MIGRATE.check_update(update) update.message.migrate_from_chat_id = 0 update.message.pinned_message = "test" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.PINNED_MESSAGE.check_update(update) update.message.pinned_message = None update.message.connected_website = "https://example.com/" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.CONNECTED_WEBSITE.check_update(update) update.message.connected_website = None update.message.proximity_alert_triggered = "alert" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.PROXIMITY_ALERT_TRIGGERED.check_update(update) update.message.proximity_alert_triggered = None update.message.video_chat_scheduled = "scheduled" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.VIDEO_CHAT_SCHEDULED.check_update(update) update.message.video_chat_scheduled = None update.message.video_chat_started = "hello" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.VIDEO_CHAT_STARTED.check_update(update) update.message.video_chat_started = None update.message.video_chat_ended = "bye" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.VIDEO_CHAT_ENDED.check_update(update) update.message.video_chat_ended = None update.message.video_chat_participants_invited = "invited" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.VIDEO_CHAT_PARTICIPANTS_INVITED.check_update(update) update.message.video_chat_participants_invited = None update.message.web_app_data = "data" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.WEB_APP_DATA.check_update(update) update.message.web_app_data = None update.message.forum_topic_created = "topic" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.FORUM_TOPIC_CREATED.check_update(update) update.message.forum_topic_created = None update.message.forum_topic_closed = "topic" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.FORUM_TOPIC_CLOSED.check_update(update) update.message.forum_topic_closed = None update.message.forum_topic_reopened = "topic" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.FORUM_TOPIC_REOPENED.check_update(update) update.message.forum_topic_reopened = None update.message.forum_topic_edited = "topic_edited" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.FORUM_TOPIC_EDITED.check_update(update) update.message.forum_topic_edited = None update.message.general_forum_topic_hidden = "topic_hidden" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.GENERAL_FORUM_TOPIC_HIDDEN.check_update(update) update.message.general_forum_topic_hidden = None update.message.general_forum_topic_unhidden = "topic_unhidden" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.GENERAL_FORUM_TOPIC_UNHIDDEN.check_update(update) update.message.general_forum_topic_unhidden = None update.message.write_access_allowed = "allowed" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.WRITE_ACCESS_ALLOWED.check_update(update) update.message.write_access_allowed = None update.message.api_kwargs = {"user_shared": "user_shared"} assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.USER_SHARED.check_update(update) update.message.api_kwargs = {} update.message.users_shared = "users_shared" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.USERS_SHARED.check_update(update) update.message.users_shared = None update.message.chat_shared = "user_shared" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.CHAT_SHARED.check_update(update) update.message.chat_shared = None update.message.giveaway_created = "giveaway_created" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.GIVEAWAY_CREATED.check_update(update) update.message.giveaway_created = None update.message.giveaway_completed = "giveaway_completed" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.GIVEAWAY_COMPLETED.check_update(update) update.message.giveaway_completed = None def test_filters_forwarded(self, update, message_origin_user): assert filters.FORWARDED.check_update(update) update.message.forward_origin = MessageOriginHiddenUser(datetime.datetime.utcnow(), 1) assert filters.FORWARDED.check_update(update) update.message.forward_origin = None assert not filters.FORWARDED.check_update(update) def test_filters_game(self, update): assert not filters.GAME.check_update(update) update.message.game = "test" assert filters.GAME.check_update(update) def test_entities_filter(self, update, message_entity): update.message.entities = [message_entity] assert filters.Entity(message_entity.type).check_update(update) update.message.entities = [] assert not filters.Entity(MessageEntity.MENTION).check_update(update) second = message_entity.to_dict() second["type"] = "bold" second = MessageEntity.de_json(second, None) update.message.entities = [message_entity, second] assert filters.Entity(message_entity.type).check_update(update) assert not filters.CaptionEntity(message_entity.type).check_update(update) def test_caption_entities_filter(self, update, message_entity): update.message.caption_entities = [message_entity] assert filters.CaptionEntity(message_entity.type).check_update(update) update.message.caption_entities = [] assert not filters.CaptionEntity(MessageEntity.MENTION).check_update(update) second = message_entity.to_dict() second["type"] = "bold" second = MessageEntity.de_json(second, None) update.message.caption_entities = [message_entity, second] assert filters.CaptionEntity(message_entity.type).check_update(update) assert not filters.Entity(message_entity.type).check_update(update) @pytest.mark.parametrize( ("chat_type", "results"), [ (Chat.PRIVATE, (True, False, False, False, False)), (Chat.GROUP, (False, True, False, True, False)), (Chat.SUPERGROUP, (False, False, True, True, False)), (Chat.CHANNEL, (False, False, False, False, True)), ], ) def test_filters_chat_types(self, update, chat_type, results): update.message.chat.type = chat_type assert filters.ChatType.PRIVATE.check_update(update) is results[0] assert filters.ChatType.GROUP.check_update(update) is results[1] assert filters.ChatType.SUPERGROUP.check_update(update) is results[2] assert filters.ChatType.GROUPS.check_update(update) is results[3] assert filters.ChatType.CHANNEL.check_update(update) is results[4] def test_filters_user_init(self): with pytest.raises(RuntimeError, match="in conjunction with"): filters.User(user_id=1, username="user") def test_filters_user_allow_empty(self, update): assert not filters.User().check_update(update) assert filters.User(allow_empty=True).check_update(update) def test_filters_user_id(self, update): assert not filters.User(user_id=1).check_update(update) update.message.from_user.id = 1 assert filters.User(user_id=1).check_update(update) assert filters.USER.check_update(update) update.message.from_user.id = 2 assert filters.User(user_id=[1, 2]).check_update(update) assert not filters.User(user_id=[3, 4]).check_update(update) update.message.from_user = None assert not filters.USER.check_update(update) assert not filters.User(user_id=[3, 4]).check_update(update) def test_filters_username(self, update): assert not filters.User(username="user").check_update(update) assert not filters.User(username="Testuser").check_update(update) update.message.from_user.username = "user@" assert filters.User(username="@user@").check_update(update) assert filters.User(username="user@").check_update(update) assert filters.User(username=["user1", "user@", "user2"]).check_update(update) assert not filters.User(username=["@username", "@user_2"]).check_update(update) update.message.from_user = None assert not filters.User(username=["@username", "@user_2"]).check_update(update) def test_filters_user_change_id(self, update): f = filters.User(user_id=1) assert f.user_ids == {1} update.message.from_user.id = 1 assert f.check_update(update) update.message.from_user.id = 2 assert not f.check_update(update) f.user_ids = 2 assert f.user_ids == {2} assert f.check_update(update) with pytest.raises(RuntimeError, match="username in conjunction"): f.usernames = "user" def test_filters_user_change_username(self, update): f = filters.User(username="user") update.message.from_user.username = "user" assert f.check_update(update) update.message.from_user.username = "User" assert not f.check_update(update) f.usernames = "User" assert f.check_update(update) with pytest.raises(RuntimeError, match="user_id in conjunction"): f.user_ids = 1 def test_filters_user_add_user_by_name(self, update): users = ["user_a", "user_b", "user_c"] f = filters.User() for user in users: update.message.from_user.username = user assert not f.check_update(update) f.add_usernames("user_a") f.add_usernames(["user_b", "user_c"]) for user in users: update.message.from_user.username = user assert f.check_update(update) with pytest.raises(RuntimeError, match="user_id in conjunction"): f.add_user_ids(1) def test_filters_user_add_user_by_id(self, update): users = [1, 2, 3] f = filters.User() for user in users: update.message.from_user.id = user assert not f.check_update(update) f.add_user_ids(1) f.add_user_ids([2, 3]) for user in users: update.message.from_user.username = user assert f.check_update(update) with pytest.raises(RuntimeError, match="username in conjunction"): f.add_usernames("user") def test_filters_user_remove_user_by_name(self, update): users = ["user_a", "user_b", "user_c"] f = filters.User(username=users) with pytest.raises(RuntimeError, match="user_id in conjunction"): f.remove_user_ids(1) for user in users: update.message.from_user.username = user assert f.check_update(update) f.remove_usernames("user_a") f.remove_usernames(["user_b", "user_c"]) for user in users: update.message.from_user.username = user assert not f.check_update(update) def test_filters_user_remove_user_by_id(self, update): users = [1, 2, 3] f = filters.User(user_id=users) with pytest.raises(RuntimeError, match="username in conjunction"): f.remove_usernames("user") for user in users: update.message.from_user.id = user assert f.check_update(update) f.remove_user_ids(1) f.remove_user_ids([2, 3]) for user in users: update.message.from_user.username = user assert not f.check_update(update) def test_filters_user_repr(self): f = filters.User([1, 2]) assert str(f) == "filters.User(1, 2)" f.remove_user_ids(1) f.remove_user_ids(2) assert str(f) == "filters.User()" f.add_usernames("@foobar") assert str(f) == "filters.User(foobar)" f.add_usernames("@barfoo") assert str(f).startswith("filters.User(") # we don't know th exact order assert "barfoo" in str(f) assert "foobar" in str(f) with pytest.raises(RuntimeError, match="Cannot set name"): f.name = "foo" def test_filters_user_attributes(self, update): assert not filters.USER_ATTACHMENT.check_update(update) assert not filters.PREMIUM_USER.check_update(update) update.message.from_user.added_to_attachment_menu = True assert filters.USER_ATTACHMENT.check_update(update) assert not filters.PREMIUM_USER.check_update(update) update.message.from_user.is_premium = True assert filters.USER_ATTACHMENT.check_update(update) assert filters.PREMIUM_USER.check_update(update) update.message.from_user.added_to_attachment_menu = False assert not filters.USER_ATTACHMENT.check_update(update) assert filters.PREMIUM_USER.check_update(update) def test_filters_chat_init(self): with pytest.raises(RuntimeError, match="in conjunction with"): filters.Chat(chat_id=1, username="chat") def test_filters_chat_allow_empty(self, update): assert not filters.Chat().check_update(update) assert filters.Chat(allow_empty=True).check_update(update) def test_filters_chat_id(self, update): assert not filters.Chat(chat_id=1).check_update(update) assert filters.CHAT.check_update(update) update.message.chat.id = 1 assert filters.Chat(chat_id=1).check_update(update) assert filters.CHAT.check_update(update) update.message.chat.id = 2 assert filters.Chat(chat_id=[1, 2]).check_update(update) assert not filters.Chat(chat_id=[3, 4]).check_update(update) update.message.chat = None assert not filters.CHAT.check_update(update) assert not filters.Chat(chat_id=[3, 4]).check_update(update) def test_filters_chat_username(self, update): assert not filters.Chat(username="chat").check_update(update) assert not filters.Chat(username="Testchat").check_update(update) update.message.chat.username = "chat@" assert filters.Chat(username="@chat@").check_update(update) assert filters.Chat(username="chat@").check_update(update) assert filters.Chat(username=["chat1", "chat@", "chat2"]).check_update(update) assert not filters.Chat(username=["@username", "@chat_2"]).check_update(update) update.message.chat = None assert not filters.Chat(username=["@username", "@chat_2"]).check_update(update) def test_filters_chat_change_id(self, update): f = filters.Chat(chat_id=1) assert f.chat_ids == {1} update.message.chat.id = 1 assert f.check_update(update) update.message.chat.id = 2 assert not f.check_update(update) f.chat_ids = 2 assert f.chat_ids == {2} assert f.check_update(update) with pytest.raises(RuntimeError, match="username in conjunction"): f.usernames = "chat" def test_filters_chat_change_username(self, update): f = filters.Chat(username="chat") update.message.chat.username = "chat" assert f.check_update(update) update.message.chat.username = "User" assert not f.check_update(update) f.usernames = "User" assert f.check_update(update) with pytest.raises(RuntimeError, match="chat_id in conjunction"): f.chat_ids = 1 def test_filters_chat_add_chat_by_name(self, update): chats = ["chat_a", "chat_b", "chat_c"] f = filters.Chat() for chat in chats: update.message.chat.username = chat assert not f.check_update(update) f.add_usernames("chat_a") f.add_usernames(["chat_b", "chat_c"]) for chat in chats: update.message.chat.username = chat assert f.check_update(update) with pytest.raises(RuntimeError, match="chat_id in conjunction"): f.add_chat_ids(1) def test_filters_chat_add_chat_by_id(self, update): chats = [1, 2, 3] f = filters.Chat() for chat in chats: update.message.chat.id = chat assert not f.check_update(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.chat.username = chat assert f.check_update(update) with pytest.raises(RuntimeError, match="username in conjunction"): f.add_usernames("chat") def test_filters_chat_remove_chat_by_name(self, update): chats = ["chat_a", "chat_b", "chat_c"] f = filters.Chat(username=chats) with pytest.raises(RuntimeError, match="chat_id in conjunction"): f.remove_chat_ids(1) for chat in chats: update.message.chat.username = chat assert f.check_update(update) f.remove_usernames("chat_a") f.remove_usernames(["chat_b", "chat_c"]) for chat in chats: update.message.chat.username = chat assert not f.check_update(update) def test_filters_chat_remove_chat_by_id(self, update): chats = [1, 2, 3] f = filters.Chat(chat_id=chats) with pytest.raises(RuntimeError, match="username in conjunction"): f.remove_usernames("chat") for chat in chats: update.message.chat.id = chat assert f.check_update(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.chat.username = chat assert not f.check_update(update) def test_filters_chat_repr(self): f = filters.Chat([1, 2]) assert str(f) == "filters.Chat(1, 2)" f.remove_chat_ids(1) f.remove_chat_ids(2) assert str(f) == "filters.Chat()" f.add_usernames("@foobar") assert str(f) == "filters.Chat(foobar)" f.add_usernames("@barfoo") assert str(f).startswith("filters.Chat(") # we don't know th exact order assert "barfoo" in str(f) assert "foobar" in str(f) with pytest.raises(RuntimeError, match="Cannot set name"): f.name = "foo" def test_filters_forwarded_from_init(self): with pytest.raises(RuntimeError, match="in conjunction with"): filters.ForwardedFrom(chat_id=1, username="chat") def test_filters_forwarded_from_allow_empty(self, update): assert not filters.ForwardedFrom().check_update(update) assert filters.ForwardedFrom(allow_empty=True).check_update(update) update.message.forward_origin = MessageOriginHiddenUser(date=1, sender_user_name="test") assert not filters.ForwardedFrom(allow_empty=True).check_update(update) def test_filters_forwarded_from_id(self, update): # Test with User id- assert not filters.ForwardedFrom(chat_id=1).check_update(update) update.message.forward_origin.sender_user.id = 1 assert filters.ForwardedFrom(chat_id=1).check_update(update) update.message.forward_origin.sender_user.id = 2 assert filters.ForwardedFrom(chat_id=[1, 2]).check_update(update) assert not filters.ForwardedFrom(chat_id=[3, 4]).check_update(update) update.message.forward_origin = None assert not filters.ForwardedFrom(chat_id=[3, 4]).check_update(update) # Test with Chat id- update.message.forward_origin = MessageOriginChat(date=1, sender_chat=Chat(4, "test")) assert filters.ForwardedFrom(chat_id=[4]).check_update(update) assert filters.ForwardedFrom(chat_id=[3, 4]).check_update(update) update.message.forward_origin = MessageOriginChat(date=1, sender_chat=Chat(2, "test")) assert not filters.ForwardedFrom(chat_id=[3, 4]).check_update(update) assert filters.ForwardedFrom(chat_id=2).check_update(update) # Test with Channel id- update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(4, "test"), message_id=1 ) assert filters.ForwardedFrom(chat_id=[4]).check_update(update) assert filters.ForwardedFrom(chat_id=[3, 4]).check_update(update) update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(2, "test"), message_id=1 ) assert not filters.ForwardedFrom(chat_id=[3, 4]).check_update(update) assert filters.ForwardedFrom(chat_id=2).check_update(update) update.message.forward_origin = None def test_filters_forwarded_from_username(self, update): # For User username assert not filters.ForwardedFrom(username="chat").check_update(update) assert not filters.ForwardedFrom(username="Testchat").check_update(update) update.message.forward_origin.sender_user.username = "chat@" assert filters.ForwardedFrom(username="@chat@").check_update(update) assert filters.ForwardedFrom(username="chat@").check_update(update) assert filters.ForwardedFrom(username=["chat1", "chat@", "chat2"]).check_update(update) assert not filters.ForwardedFrom(username=["@username", "@chat_2"]).check_update(update) update.message.forward_origin = None assert not filters.ForwardedFrom(username=["@username", "@chat_2"]).check_update(update) # For Chat username assert not filters.ForwardedFrom(username="chat").check_update(update) assert not filters.ForwardedFrom(username="Testchat").check_update(update) update.message.forward_origin = MessageOriginChat( date=1, sender_chat=Chat(4, username="chat@", type=Chat.SUPERGROUP) ) assert filters.ForwardedFrom(username="@chat@").check_update(update) assert filters.ForwardedFrom(username="chat@").check_update(update) assert filters.ForwardedFrom(username=["chat1", "chat@", "chat2"]).check_update(update) assert not filters.ForwardedFrom(username=["@username", "@chat_2"]).check_update(update) update.message.forward_origin = None assert not filters.ForwardedFrom(username=["@username", "@chat_2"]).check_update(update) # For Channel username assert not filters.ForwardedFrom(username="chat").check_update(update) assert not filters.ForwardedFrom(username="Testchat").check_update(update) update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(4, username="chat@", type=Chat.SUPERGROUP), message_id=1 ) assert filters.ForwardedFrom(username="@chat@").check_update(update) assert filters.ForwardedFrom(username="chat@").check_update(update) assert filters.ForwardedFrom(username=["chat1", "chat@", "chat2"]).check_update(update) assert not filters.ForwardedFrom(username=["@username", "@chat_2"]).check_update(update) update.message.forward_origin = None assert not filters.ForwardedFrom(username=["@username", "@chat_2"]).check_update(update) def test_filters_forwarded_from_change_id(self, update): f = filters.ForwardedFrom(chat_id=1) # For User ids- assert f.chat_ids == {1} update.message.forward_origin.sender_user.id = 1 assert f.check_update(update) update.message.forward_origin.sender_user.id = 2 assert not f.check_update(update) f.chat_ids = 2 assert f.chat_ids == {2} assert f.check_update(update) # For Chat ids- f = filters.ForwardedFrom(chat_id=1) # reset this # and change this to None, only one of them can be True update.message.forward_origin = None assert f.chat_ids == {1} update.message.forward_origin = MessageOriginChat(date=1, sender_chat=Chat(1, "test")) assert f.check_update(update) update.message.forward_origin = MessageOriginChat(date=1, sender_chat=Chat(2, "test")) assert not f.check_update(update) f.chat_ids = 2 assert f.chat_ids == {2} assert f.check_update(update) # For Channel ids- f = filters.ForwardedFrom(chat_id=1) # reset this # and change this to None, only one of them can be True update.message.forward_origin = None assert f.chat_ids == {1} update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(1, "test"), message_id=1 ) assert f.check_update(update) update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(2, "test"), message_id=1 ) assert not f.check_update(update) f.chat_ids = 2 assert f.chat_ids == {2} assert f.check_update(update) with pytest.raises(RuntimeError, match="username in conjunction"): f.usernames = "chat" def test_filters_forwarded_from_change_username(self, update): # For User usernames f = filters.ForwardedFrom(username="chat") update.message.forward_origin.sender_user.username = "chat" assert f.check_update(update) update.message.forward_origin.sender_user.username = "User" assert not f.check_update(update) f.usernames = "User" assert f.check_update(update) # For Chat usernames update.message.forward_origin = None f = filters.ForwardedFrom(username="chat") update.message.forward_origin = MessageOriginChat( date=1, sender_chat=Chat(1, username="chat", type=Chat.SUPERGROUP) ) assert f.check_update(update) update.message.forward_origin = MessageOriginChat( date=1, sender_chat=Chat(2, username="User", type=Chat.SUPERGROUP) ) assert not f.check_update(update) f.usernames = "User" assert f.check_update(update) # For Channel usernames update.message.forward_origin = None f = filters.ForwardedFrom(username="chat") update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(1, username="chat", type=Chat.SUPERGROUP), message_id=1 ) assert f.check_update(update) update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(2, username="User", type=Chat.SUPERGROUP), message_id=1 ) assert not f.check_update(update) f.usernames = "User" assert f.check_update(update) with pytest.raises(RuntimeError, match="chat_id in conjunction"): f.chat_ids = 1 def test_filters_forwarded_from_add_chat_by_name(self, update): chats = ["chat_a", "chat_b", "chat_c"] f = filters.ForwardedFrom() # For User usernames for chat in chats: update.message.forward_origin.sender_user.username = chat assert not f.check_update(update) f.add_usernames("chat_a") f.add_usernames(["chat_b", "chat_c"]) for chat in chats: update.message.forward_origin.sender_user.username = chat assert f.check_update(update) # For Chat usernames update.message.forward_origin = None f = filters.ForwardedFrom() for chat in chats: update.message.forward_origin = MessageOriginChat( date=1, sender_chat=Chat(1, username=chat, type=Chat.SUPERGROUP) ) assert not f.check_update(update) f.add_usernames("chat_a") f.add_usernames(["chat_b", "chat_c"]) for chat in chats: update.message.forward_origin = MessageOriginChat( date=1, sender_chat=Chat(1, username=chat, type=Chat.SUPERGROUP) ) assert f.check_update(update) # For Channel usernames update.message.forward_origin = None f = filters.ForwardedFrom() for chat in chats: update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(1, username=chat, type=Chat.SUPERGROUP), message_id=1 ) assert not f.check_update(update) f.add_usernames("chat_a") f.add_usernames(["chat_b", "chat_c"]) for chat in chats: update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(1, username=chat, type=Chat.SUPERGROUP), message_id=1 ) assert f.check_update(update) with pytest.raises(RuntimeError, match="chat_id in conjunction"): f.add_chat_ids(1) def test_filters_forwarded_from_add_chat_by_id(self, update): chats = [1, 2, 3] f = filters.ForwardedFrom() # For User ids for chat in chats: update.message.forward_origin.sender_user.id = chat assert not f.check_update(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.forward_origin.sender_user.username = chat assert f.check_update(update) # For Chat ids- update.message.forward_origin = None f = filters.ForwardedFrom() for chat in chats: update.message.forward_origin = MessageOriginChat( date=1, sender_chat=Chat(chat, "test") ) assert not f.check_update(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.forward_origin = MessageOriginChat( date=1, sender_chat=Chat(chat, "test") ) assert f.check_update(update) # For Channel ids- update.message.forward_origin = None f = filters.ForwardedFrom() for chat in chats: update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(chat, "test"), message_id=1 ) assert not f.check_update(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(chat, "test"), message_id=1 ) assert f.check_update(update) with pytest.raises(RuntimeError, match="username in conjunction"): f.add_usernames("chat") def test_filters_forwarded_from_remove_chat_by_name(self, update): chats = ["chat_a", "chat_b", "chat_c"] f = filters.ForwardedFrom(username=chats) with pytest.raises(RuntimeError, match="chat_id in conjunction"): f.remove_chat_ids(1) # For User usernames for chat in chats: update.message.forward_origin.sender_user.username = chat assert f.check_update(update) f.remove_usernames("chat_a") f.remove_usernames(["chat_b", "chat_c"]) for chat in chats: update.message.forward_origin.sender_user.username = chat assert not f.check_update(update) # For Chat usernames update.message.forward_origin = None f = filters.ForwardedFrom(username=chats) for chat in chats: update.message.forward_origin = MessageOriginChat( date=1, sender_chat=Chat(1, username=chat, type=Chat.SUPERGROUP) ) assert f.check_update(update) f.remove_usernames("chat_a") f.remove_usernames(["chat_b", "chat_c"]) for chat in chats: update.message.forward_origin = MessageOriginChat( date=1, sender_chat=Chat(1, username=chat, type=Chat.SUPERGROUP) ) assert not f.check_update(update) # For Channel usernames update.message.forward_origin = None f = filters.ForwardedFrom(username=chats) for chat in chats: update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(1, username=chat, type=Chat.SUPERGROUP), message_id=1 ) assert f.check_update(update) f.remove_usernames("chat_a") f.remove_usernames(["chat_b", "chat_c"]) for chat in chats: update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(1, username=chat, type=Chat.SUPERGROUP), message_id=1 ) assert not f.check_update(update) def test_filters_forwarded_from_remove_chat_by_id(self, update): chats = [1, 2, 3] f = filters.ForwardedFrom(chat_id=chats) with pytest.raises(RuntimeError, match="username in conjunction"): f.remove_usernames("chat") # For User ids for chat in chats: update.message.forward_origin.sender_user.id = chat assert f.check_update(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.forward_origin.sender_user.username = chat assert not f.check_update(update) # For Chat ids update.message.forward_origin = None f = filters.ForwardedFrom(chat_id=chats) for chat in chats: update.message.forward_origin = MessageOriginChat( date=1, sender_chat=Chat(chat, "test") ) assert f.check_update(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.forward_origin = MessageOriginChat( date=1, sender_chat=Chat(chat, "test") ) assert not f.check_update(update) # For Channel ids update.message.forward_origin = None f = filters.ForwardedFrom(chat_id=chats) for chat in chats: update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(chat, "test"), message_id=1 ) assert f.check_update(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.forward_origin = MessageOriginChannel( date=1, chat=Chat(chat, "test"), message_id=1 ) assert not f.check_update(update) def test_filters_forwarded_from_repr(self): f = filters.ForwardedFrom([1, 2]) assert str(f) == "filters.ForwardedFrom(1, 2)" f.remove_chat_ids(1) f.remove_chat_ids(2) assert str(f) == "filters.ForwardedFrom()" f.add_usernames("@foobar") assert str(f) == "filters.ForwardedFrom(foobar)" f.add_usernames("@barfoo") assert str(f).startswith("filters.ForwardedFrom(") # we don't know the exact order assert "barfoo" in str(f) assert "foobar" in str(f) with pytest.raises(RuntimeError, match="Cannot set name"): f.name = "foo" def test_filters_sender_chat_init(self): with pytest.raises(RuntimeError, match="in conjunction with"): filters.SenderChat(chat_id=1, username="chat") def test_filters_sender_chat_allow_empty(self, update): assert not filters.SenderChat().check_update(update) assert filters.SenderChat(allow_empty=True).check_update(update) def test_filters_sender_chat_id(self, update): assert not filters.SenderChat(chat_id=1).check_update(update) update.message.sender_chat.id = 1 assert filters.SenderChat(chat_id=1).check_update(update) update.message.sender_chat.id = 2 assert filters.SenderChat(chat_id=[1, 2]).check_update(update) assert not filters.SenderChat(chat_id=[3, 4]).check_update(update) assert filters.SenderChat.ALL.check_update(update) update.message.sender_chat = None assert not filters.SenderChat(chat_id=[3, 4]).check_update(update) assert not filters.SenderChat.ALL.check_update(update) def test_filters_sender_chat_username(self, update): assert not filters.SenderChat(username="chat").check_update(update) assert not filters.SenderChat(username="Testchat").check_update(update) update.message.sender_chat.username = "chat@" assert filters.SenderChat(username="@chat@").check_update(update) assert filters.SenderChat(username="chat@").check_update(update) assert filters.SenderChat(username=["chat1", "chat@", "chat2"]).check_update(update) assert not filters.SenderChat(username=["@username", "@chat_2"]).check_update(update) assert filters.SenderChat.ALL.check_update(update) update.message.sender_chat = None assert not filters.SenderChat(username=["@username", "@chat_2"]).check_update(update) assert not filters.SenderChat.ALL.check_update(update) def test_filters_sender_chat_change_id(self, update): f = filters.SenderChat(chat_id=1) assert f.chat_ids == {1} update.message.sender_chat.id = 1 assert f.check_update(update) update.message.sender_chat.id = 2 assert not f.check_update(update) f.chat_ids = 2 assert f.chat_ids == {2} assert f.check_update(update) with pytest.raises(RuntimeError, match="username in conjunction"): f.usernames = "chat" def test_filters_sender_chat_change_username(self, update): f = filters.SenderChat(username="chat") update.message.sender_chat.username = "chat" assert f.check_update(update) update.message.sender_chat.username = "User" assert not f.check_update(update) f.usernames = "User" assert f.check_update(update) with pytest.raises(RuntimeError, match="chat_id in conjunction"): f.chat_ids = 1 def test_filters_sender_chat_add_sender_chat_by_name(self, update): chats = ["chat_a", "chat_b", "chat_c"] f = filters.SenderChat() for chat in chats: update.message.sender_chat.username = chat assert not f.check_update(update) f.add_usernames("chat_a") f.add_usernames(["chat_b", "chat_c"]) for chat in chats: update.message.sender_chat.username = chat assert f.check_update(update) with pytest.raises(RuntimeError, match="chat_id in conjunction"): f.add_chat_ids(1) def test_filters_sender_chat_add_sender_chat_by_id(self, update): chats = [1, 2, 3] f = filters.SenderChat() for chat in chats: update.message.sender_chat.id = chat assert not f.check_update(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.sender_chat.username = chat assert f.check_update(update) with pytest.raises(RuntimeError, match="username in conjunction"): f.add_usernames("chat") def test_filters_sender_chat_remove_sender_chat_by_name(self, update): chats = ["chat_a", "chat_b", "chat_c"] f = filters.SenderChat(username=chats) with pytest.raises(RuntimeError, match="chat_id in conjunction"): f.remove_chat_ids(1) for chat in chats: update.message.sender_chat.username = chat assert f.check_update(update) f.remove_usernames("chat_a") f.remove_usernames(["chat_b", "chat_c"]) for chat in chats: update.message.sender_chat.username = chat assert not f.check_update(update) def test_filters_sender_chat_remove_sender_chat_by_id(self, update): chats = [1, 2, 3] f = filters.SenderChat(chat_id=chats) with pytest.raises(RuntimeError, match="username in conjunction"): f.remove_usernames("chat") for chat in chats: update.message.sender_chat.id = chat assert f.check_update(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.sender_chat.username = chat assert not f.check_update(update) def test_filters_sender_chat_repr(self): f = filters.SenderChat([1, 2]) assert str(f) == "filters.SenderChat(1, 2)" f.remove_chat_ids(1) f.remove_chat_ids(2) assert str(f) == "filters.SenderChat()" f.add_usernames("@foobar") assert str(f) == "filters.SenderChat(foobar)" f.add_usernames("@barfoo") assert str(f).startswith("filters.SenderChat(") # we don't know th exact order assert "barfoo" in str(f) assert "foobar" in str(f) with pytest.raises(RuntimeError, match="Cannot set name"): f.name = "foo" def test_filters_sender_chat_super_group(self, update): update.message.sender_chat.type = Chat.PRIVATE assert not filters.SenderChat.SUPER_GROUP.check_update(update) assert filters.SenderChat.ALL.check_update(update) update.message.sender_chat.type = Chat.CHANNEL assert not filters.SenderChat.SUPER_GROUP.check_update(update) update.message.sender_chat.type = Chat.SUPERGROUP assert filters.SenderChat.SUPER_GROUP.check_update(update) assert filters.SenderChat.ALL.check_update(update) update.message.sender_chat = None assert not filters.SenderChat.SUPER_GROUP.check_update(update) assert not filters.SenderChat.ALL.check_update(update) def test_filters_sender_chat_channel(self, update): update.message.sender_chat.type = Chat.PRIVATE assert not filters.SenderChat.CHANNEL.check_update(update) update.message.sender_chat.type = Chat.SUPERGROUP assert not filters.SenderChat.CHANNEL.check_update(update) update.message.sender_chat.type = Chat.CHANNEL assert filters.SenderChat.CHANNEL.check_update(update) update.message.sender_chat = None assert not filters.SenderChat.CHANNEL.check_update(update) def test_filters_is_automatic_forward(self, update): assert not filters.IS_AUTOMATIC_FORWARD.check_update(update) update.message.is_automatic_forward = True assert filters.IS_AUTOMATIC_FORWARD.check_update(update) def test_filters_is_from_offline(self, update): assert not filters.IS_FROM_OFFLINE.check_update(update) update.message.is_from_offline = True assert filters.IS_FROM_OFFLINE.check_update(update) def test_filters_is_topic_message(self, update): assert not filters.IS_TOPIC_MESSAGE.check_update(update) update.message.is_topic_message = True assert filters.IS_TOPIC_MESSAGE.check_update(update) def test_filters_has_media_spoiler(self, update): assert not filters.HAS_MEDIA_SPOILER.check_update(update) update.message.has_media_spoiler = True assert filters.HAS_MEDIA_SPOILER.check_update(update) def test_filters_has_protected_content(self, update): assert not filters.HAS_PROTECTED_CONTENT.check_update(update) update.message.has_protected_content = True assert filters.HAS_PROTECTED_CONTENT.check_update(update) def test_filters_invoice(self, update): assert not filters.INVOICE.check_update(update) update.message.invoice = "test" assert filters.INVOICE.check_update(update) def test_filters_successful_payment(self, update): assert not filters.SUCCESSFUL_PAYMENT.check_update(update) update.message.successful_payment = "test" assert filters.SUCCESSFUL_PAYMENT.check_update(update) def test_filters_successful_payment_payloads(self, update): assert not filters.SuccessfulPayment(("custom-payload",)).check_update(update) assert not filters.SuccessfulPayment().check_update(update) update.message.successful_payment = SuccessfulPayment( "USD", 100, "custom-payload", "123", "123" ) assert filters.SuccessfulPayment(("custom-payload",)).check_update(update) assert filters.SuccessfulPayment().check_update(update) assert not filters.SuccessfulPayment(["test1"]).check_update(update) def test_filters_successful_payment_repr(self): f = filters.SuccessfulPayment() assert str(f) == "filters.SUCCESSFUL_PAYMENT" f = filters.SuccessfulPayment(["payload1", "payload2"]) assert str(f) == "filters.SuccessfulPayment(['payload1', 'payload2'])" def test_filters_passport_data(self, update): assert not filters.PASSPORT_DATA.check_update(update) update.message.passport_data = "test" assert filters.PASSPORT_DATA.check_update(update) def test_filters_poll(self, update): assert not filters.POLL.check_update(update) update.message.poll = "test" assert filters.POLL.check_update(update) @pytest.mark.parametrize("emoji", Dice.ALL_EMOJI) def test_filters_dice(self, update, emoji): update.message.dice = Dice(4, emoji) assert filters.Dice.ALL.check_update(update) assert filters.Dice().check_update(update) to_camel = emoji.name.title().replace("_", "") assert repr(filters.Dice.ALL) == "filters.Dice.ALL" assert repr(getattr(filters.Dice, to_camel)(4)) == f"filters.Dice.{to_camel}([4])" update.message.dice = None assert not filters.Dice.ALL.check_update(update) @pytest.mark.parametrize("emoji", Dice.ALL_EMOJI) def test_filters_dice_list(self, update, emoji): update.message.dice = None assert not filters.Dice(5).check_update(update) update.message.dice = Dice(5, emoji) assert filters.Dice(5).check_update(update) assert repr(filters.Dice(5)) == "filters.Dice([5])" assert filters.Dice({5, 6}).check_update(update) assert not filters.Dice(1).check_update(update) assert not filters.Dice([2, 3]).check_update(update) def test_filters_dice_type(self, update): update.message.dice = Dice(5, "🎲") assert filters.Dice.DICE.check_update(update) assert repr(filters.Dice.DICE) == "filters.Dice.DICE" assert filters.Dice.Dice([4, 5]).check_update(update) assert not filters.Dice.Darts(5).check_update(update) assert not filters.Dice.BASKETBALL.check_update(update) assert not filters.Dice.Dice([6]).check_update(update) update.message.dice = Dice(5, "🎯") assert filters.Dice.DARTS.check_update(update) assert filters.Dice.Darts([4, 5]).check_update(update) assert not filters.Dice.Dice(5).check_update(update) assert not filters.Dice.BASKETBALL.check_update(update) assert not filters.Dice.Darts([6]).check_update(update) update.message.dice = Dice(5, "🏀") assert filters.Dice.BASKETBALL.check_update(update) assert filters.Dice.Basketball([4, 5]).check_update(update) assert not filters.Dice.Dice(5).check_update(update) assert not filters.Dice.DARTS.check_update(update) assert not filters.Dice.Basketball([4]).check_update(update) update.message.dice = Dice(5, "⚽") assert filters.Dice.FOOTBALL.check_update(update) assert filters.Dice.Football([4, 5]).check_update(update) assert not filters.Dice.Dice(5).check_update(update) assert not filters.Dice.DARTS.check_update(update) assert not filters.Dice.Football([4]).check_update(update) update.message.dice = Dice(5, "🎰") assert filters.Dice.SLOT_MACHINE.check_update(update) assert filters.Dice.SlotMachine([4, 5]).check_update(update) assert not filters.Dice.Dice(5).check_update(update) assert not filters.Dice.DARTS.check_update(update) assert not filters.Dice.SlotMachine([4]).check_update(update) update.message.dice = Dice(5, "🎳") assert filters.Dice.BOWLING.check_update(update) assert filters.Dice.Bowling([4, 5]).check_update(update) assert not filters.Dice.Dice(5).check_update(update) assert not filters.Dice.DARTS.check_update(update) assert not filters.Dice.Bowling([4]).check_update(update) def test_language_filter_single(self, update): update.message.from_user.language_code = "en_US" assert filters.Language("en_US").check_update(update) assert filters.Language("en").check_update(update) assert not filters.Language("en_GB").check_update(update) assert not filters.Language("da").check_update(update) update.message.from_user.language_code = "da" assert not filters.Language("en_US").check_update(update) assert not filters.Language("en").check_update(update) assert not filters.Language("en_GB").check_update(update) assert filters.Language("da").check_update(update) update.message.from_user = None assert not filters.Language("da").check_update(update) def test_language_filter_multiple(self, update): f = filters.Language(["en_US", "da"]) update.message.from_user.language_code = "en_US" assert f.check_update(update) update.message.from_user.language_code = "en_GB" assert not f.check_update(update) update.message.from_user.language_code = "da" assert f.check_update(update) def test_and_filters(self, update, message_origin_user): update.message.text = "test" update.message.forward_origin = message_origin_user assert (filters.TEXT & filters.FORWARDED).check_update(update) update.message.text = "/test" assert (filters.TEXT & filters.FORWARDED).check_update(update) update.message.text = "test" update.message.forward_origin = None assert not (filters.TEXT & filters.FORWARDED).check_update(update) update.message.text = "test" update.message.forward_origin = message_origin_user assert (filters.TEXT & filters.FORWARDED & filters.ChatType.PRIVATE).check_update(update) def test_or_filters(self, update): update.message.text = "test" assert (filters.TEXT | filters.StatusUpdate.ALL).check_update(update) update.message.group_chat_created = True assert (filters.TEXT | filters.StatusUpdate.ALL).check_update(update) update.message.text = None assert (filters.TEXT | filters.StatusUpdate.ALL).check_update(update) update.message.group_chat_created = False assert not (filters.TEXT | filters.StatusUpdate.ALL).check_update(update) def test_and_or_filters(self, update): update.message.text = "test" update.message.forward_origin = message_origin_user assert (filters.TEXT & (filters.StatusUpdate.ALL | filters.FORWARDED)).check_update(update) update.message.forward_origin = None assert not (filters.TEXT & (filters.FORWARDED | filters.StatusUpdate.ALL)).check_update( update ) update.message.pinned_message = True assert filters.TEXT & (filters.FORWARDED | filters.StatusUpdate.ALL).check_update(update) assert ( str(filters.TEXT & (filters.FORWARDED | filters.Entity(MessageEntity.MENTION))) == ">" ) def test_xor_filters(self, update): update.message.text = "test" update.effective_user.id = 123 assert not (filters.TEXT ^ filters.User(123)).check_update(update) update.message.text = None update.effective_user.id = 1234 assert not (filters.TEXT ^ filters.User(123)).check_update(update) update.message.text = "test" assert (filters.TEXT ^ filters.User(123)).check_update(update) update.message.text = None update.effective_user.id = 123 assert (filters.TEXT ^ filters.User(123)).check_update(update) def test_xor_filters_repr(self, update): assert str(filters.TEXT ^ filters.User(123)) == "" with pytest.raises(RuntimeError, match="Cannot set name"): (filters.TEXT ^ filters.User(123)).name = "foo" def test_and_xor_filters(self, update, message_origin_user): update.message.text = "test" update.message.forward_origin = message_origin_user assert (filters.FORWARDED & (filters.TEXT ^ filters.User(123))).check_update(update) update.message.text = None update.effective_user.id = 123 assert (filters.FORWARDED & (filters.TEXT ^ filters.User(123))).check_update(update) update.message.text = "test" assert not (filters.FORWARDED & (filters.TEXT ^ filters.User(123))).check_update(update) update.message.forward_origin = None update.message.text = None update.effective_user.id = 123 assert not (filters.FORWARDED & (filters.TEXT ^ filters.User(123))).check_update(update) update.message.text = "test" update.effective_user.id = 456 assert not (filters.FORWARDED & (filters.TEXT ^ filters.User(123))).check_update(update) assert ( str(filters.FORWARDED & (filters.TEXT ^ filters.User(123))) == ">" ) def test_xor_regex_filters(self, update, message_origin_user): sre_type = type(re.match("", "")) update.message.text = "test" update.message.forward_origin = message_origin_user assert not (filters.FORWARDED ^ filters.Regex("^test$")).check_update(update) update.message.forward_origin = None result = (filters.FORWARDED ^ filters.Regex("^test$")).check_update(update) assert result assert isinstance(result, dict) matches = result["matches"] assert isinstance(matches, list) assert type(matches[0]) is sre_type update.message.forward_origin = message_origin_user update.message.text = None assert (filters.FORWARDED ^ filters.Regex("^test$")).check_update(update) is True def test_inverted_filters(self, update): update.message.text = "/test" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] assert filters.COMMAND.check_update(update) assert not (~filters.COMMAND).check_update(update) update.message.text = "test" update.message.entities = [] assert not filters.COMMAND.check_update(update) assert (~filters.COMMAND).check_update(update) def test_inverted_filters_repr(self, update): assert str(~filters.TEXT) == "" with pytest.raises(RuntimeError, match="Cannot set name"): (~filters.TEXT).name = "foo" def test_inverted_and_filters(self, update, message_origin_user): update.message.text = "/test" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] update.message.forward_origin = message_origin_user assert (filters.FORWARDED & filters.COMMAND).check_update(update) assert not (~filters.FORWARDED & filters.COMMAND).check_update(update) assert not (filters.FORWARDED & ~filters.COMMAND).check_update(update) assert not (~(filters.FORWARDED & filters.COMMAND)).check_update(update) update.message.forward_origin = None assert not (filters.FORWARDED & filters.COMMAND).check_update(update) assert (~filters.FORWARDED & filters.COMMAND).check_update(update) assert not (filters.FORWARDED & ~filters.COMMAND).check_update(update) assert (~(filters.FORWARDED & filters.COMMAND)).check_update(update) update.message.text = "test" update.message.entities = [] assert not (filters.FORWARDED & filters.COMMAND).check_update(update) assert not (~filters.FORWARDED & filters.COMMAND).check_update(update) assert not (filters.FORWARDED & ~filters.COMMAND).check_update(update) assert (~(filters.FORWARDED & filters.COMMAND)).check_update(update) def test_indirect_message(self, update): class _CustomFilter(filters.MessageFilter): test_flag = False def filter(self, message: Message): self.test_flag = True return self.test_flag c = _CustomFilter() u = Update(0, callback_query=CallbackQuery("0", update.effective_user, "", update.message)) assert not c.check_update(u) assert not c.test_flag assert c.check_update(update) assert c.test_flag def test_custom_unnamed_filter(self, update, base_class): class Unnamed(base_class): def filter(self, _): return True unnamed = Unnamed() assert str(unnamed) == Unnamed.__name__ def test_update_type_message(self, update): assert filters.UpdateType.MESSAGE.check_update(update) assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) assert filters.UpdateType.MESSAGES.check_update(update) assert not filters.UpdateType.CHANNEL_POST.check_update(update) assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) assert not filters.UpdateType.EDITED.check_update(update) assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_update_type_edited_message(self, update): update.edited_message, update.message = update.message, update.edited_message assert not filters.UpdateType.MESSAGE.check_update(update) assert filters.UpdateType.EDITED_MESSAGE.check_update(update) assert filters.UpdateType.MESSAGES.check_update(update) assert not filters.UpdateType.CHANNEL_POST.check_update(update) assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) assert filters.UpdateType.EDITED.check_update(update) assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_update_type_channel_post(self, update): update.channel_post, update.message = update.message, update.edited_message assert not filters.UpdateType.MESSAGE.check_update(update) assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) assert not filters.UpdateType.MESSAGES.check_update(update) assert filters.UpdateType.CHANNEL_POST.check_update(update) assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert filters.UpdateType.CHANNEL_POSTS.check_update(update) assert not filters.UpdateType.EDITED.check_update(update) assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_update_type_edited_channel_post(self, update): update.edited_channel_post, update.message = update.message, update.edited_message assert not filters.UpdateType.MESSAGE.check_update(update) assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) assert not filters.UpdateType.MESSAGES.check_update(update) assert not filters.UpdateType.CHANNEL_POST.check_update(update) assert filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert filters.UpdateType.CHANNEL_POSTS.check_update(update) assert filters.UpdateType.EDITED.check_update(update) assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update) assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_update_type_business_message(self, update): update.business_message, update.message = update.message, update.edited_message assert not filters.UpdateType.MESSAGE.check_update(update) assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) assert not filters.UpdateType.MESSAGES.check_update(update) assert not filters.UpdateType.CHANNEL_POST.check_update(update) assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) assert not filters.UpdateType.EDITED.check_update(update) assert filters.UpdateType.BUSINESS_MESSAGES.check_update(update) assert filters.UpdateType.BUSINESS_MESSAGE.check_update(update) assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_update_type_edited_business_message(self, update): update.edited_business_message, update.message = update.message, update.edited_message assert not filters.UpdateType.MESSAGE.check_update(update) assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) assert not filters.UpdateType.MESSAGES.check_update(update) assert not filters.UpdateType.CHANNEL_POST.check_update(update) assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) assert filters.UpdateType.EDITED.check_update(update) assert filters.UpdateType.BUSINESS_MESSAGES.check_update(update) assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update) assert filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update) def test_merged_short_circuit_and(self, update, base_class): update.message.text = "/test" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] class TestException(Exception): pass class RaisingFilter(base_class): def filter(self, _): raise TestException raising_filter = RaisingFilter() with pytest.raises(TestException): (filters.COMMAND & raising_filter).check_update(update) update.message.text = "test" update.message.entities = [] (filters.COMMAND & raising_filter).check_update(update) def test_merged_filters_repr(self, update): with pytest.raises(RuntimeError, match="Cannot set name"): (filters.TEXT & filters.PHOTO).name = "foo" def test_merged_short_circuit_or(self, update, base_class): update.message.text = "test" class TestException(Exception): pass class RaisingFilter(base_class): def filter(self, _): raise TestException raising_filter = RaisingFilter() with pytest.raises(TestException): (filters.COMMAND | raising_filter).check_update(update) update.message.text = "/test" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] (filters.COMMAND | raising_filter).check_update(update) def test_merged_data_merging_and(self, update, base_class): update.message.text = "/test" update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] class DataFilter(base_class): data_filter = True def __init__(self, data): self.data = data def filter(self, _): return {"test": [self.data], "test2": {"test3": [self.data]}} result = (filters.COMMAND & DataFilter("blah")).check_update(update) assert result["test"] == ["blah"] assert not result["test2"] result = (DataFilter("blah1") & DataFilter("blah2")).check_update(update) assert result["test"] == ["blah1", "blah2"] assert isinstance(result["test2"], list) assert result["test2"][0]["test3"] == ["blah1"] update.message.text = "test" update.message.entities = [] result = (filters.COMMAND & DataFilter("blah")).check_update(update) assert not result def test_merged_data_merging_or(self, update, base_class): update.message.text = "/test" class DataFilter(base_class): data_filter = True def __init__(self, data): self.data = data def filter(self, _): return {"test": [self.data]} result = (filters.COMMAND | DataFilter("blah")).check_update(update) assert result result = (DataFilter("blah1") | DataFilter("blah2")).check_update(update) assert result["test"] == ["blah1"] update.message.text = "test" result = (filters.COMMAND | DataFilter("blah")).check_update(update) assert result["test"] == ["blah"] def test_filters_via_bot_init(self): with pytest.raises(RuntimeError, match="in conjunction with"): filters.ViaBot(bot_id=1, username="bot") def test_filters_via_bot_allow_empty(self, update): assert not filters.ViaBot().check_update(update) assert filters.ViaBot(allow_empty=True).check_update(update) def test_filters_via_bot_id(self, update): assert not filters.ViaBot(bot_id=1).check_update(update) update.message.via_bot.id = 1 assert filters.ViaBot(bot_id=1).check_update(update) update.message.via_bot.id = 2 assert filters.ViaBot(bot_id=[1, 2]).check_update(update) assert not filters.ViaBot(bot_id=[3, 4]).check_update(update) update.message.via_bot = None assert not filters.ViaBot(bot_id=[3, 4]).check_update(update) def test_filters_via_bot_username(self, update): assert not filters.ViaBot(username="bot").check_update(update) assert not filters.ViaBot(username="Testbot").check_update(update) update.message.via_bot.username = "bot@" assert filters.ViaBot(username="@bot@").check_update(update) assert filters.ViaBot(username="bot@").check_update(update) assert filters.ViaBot(username=["bot1", "bot@", "bot2"]).check_update(update) assert not filters.ViaBot(username=["@username", "@bot_2"]).check_update(update) update.message.via_bot = None assert not filters.User(username=["@username", "@bot_2"]).check_update(update) def test_filters_via_bot_change_id(self, update): f = filters.ViaBot(bot_id=3) assert f.bot_ids == {3} update.message.via_bot.id = 3 assert f.check_update(update) update.message.via_bot.id = 2 assert not f.check_update(update) f.bot_ids = 2 assert f.bot_ids == {2} assert f.check_update(update) with pytest.raises(RuntimeError, match="username in conjunction"): f.usernames = "user" def test_filters_via_bot_change_username(self, update): f = filters.ViaBot(username="bot") update.message.via_bot.username = "bot" assert f.check_update(update) update.message.via_bot.username = "Bot" assert not f.check_update(update) f.usernames = "Bot" assert f.check_update(update) with pytest.raises(RuntimeError, match="bot_id in conjunction"): f.bot_ids = 1 def test_filters_via_bot_add_user_by_name(self, update): users = ["bot_a", "bot_b", "bot_c"] f = filters.ViaBot() for user in users: update.message.via_bot.username = user assert not f.check_update(update) f.add_usernames("bot_a") f.add_usernames(["bot_b", "bot_c"]) for user in users: update.message.via_bot.username = user assert f.check_update(update) with pytest.raises(RuntimeError, match="bot_id in conjunction"): f.add_bot_ids(1) def test_filters_via_bot_add_user_by_id(self, update): users = [1, 2, 3] f = filters.ViaBot() for user in users: update.message.via_bot.id = user assert not f.check_update(update) f.add_bot_ids(1) f.add_bot_ids([2, 3]) for user in users: update.message.via_bot.username = user assert f.check_update(update) with pytest.raises(RuntimeError, match="username in conjunction"): f.add_usernames("bot") def test_filters_via_bot_remove_user_by_name(self, update): users = ["bot_a", "bot_b", "bot_c"] f = filters.ViaBot(username=users) with pytest.raises(RuntimeError, match="bot_id in conjunction"): f.remove_bot_ids(1) for user in users: update.message.via_bot.username = user assert f.check_update(update) f.remove_usernames("bot_a") f.remove_usernames(["bot_b", "bot_c"]) for user in users: update.message.via_bot.username = user assert not f.check_update(update) def test_filters_via_bot_remove_user_by_id(self, update): users = [1, 2, 3] f = filters.ViaBot(bot_id=users) with pytest.raises(RuntimeError, match="username in conjunction"): f.remove_usernames("bot") for user in users: update.message.via_bot.id = user assert f.check_update(update) f.remove_bot_ids(1) f.remove_bot_ids([2, 3]) for user in users: update.message.via_bot.username = user assert not f.check_update(update) def test_filters_via_bot_repr(self): f = filters.ViaBot([1, 2]) assert str(f) == "filters.ViaBot(1, 2)" f.remove_bot_ids(1) f.remove_bot_ids(2) assert str(f) == "filters.ViaBot()" f.add_usernames("@foobar") assert str(f) == "filters.ViaBot(foobar)" f.add_usernames("@barfoo") assert str(f).startswith("filters.ViaBot(") # we don't know th exact order assert "barfoo" in str(f) assert "foobar" in str(f) with pytest.raises(RuntimeError, match="Cannot set name"): f.name = "foo" def test_filters_attachment(self, update): assert not filters.ATTACHMENT.check_update(update) # we need to define a new Update (or rather, message class) here because # effective_attachment is only evaluated once per instance, and the filter relies on that up = Update( 0, Message( 0, datetime.datetime.utcnow(), Chat(0, "private"), document=Document("str", "other_str"), ), ) assert filters.ATTACHMENT.check_update(up) def test_filters_mention_no_entities(self, update): update.message.text = "test" assert not filters.Mention("@test").check_update(update) assert not filters.Mention(123456).check_update(update) assert not filters.Mention("123456").check_update(update) assert not filters.Mention(User(1, "first_name", False)).check_update(update) assert not filters.Mention( ["@test", 123456, "123456", User(1, "first_name", False)] ).check_update(update) def test_filters_mention_type_mention(self, update): update.message.text = "@test1 @test2 user" update.message.entities = [ MessageEntity(MessageEntity.MENTION, 0, 6), MessageEntity(MessageEntity.MENTION, 7, 6), ] user_no_username = User(123456, "first_name", False) user_wrong_username = User(123456, "first_name", False, username="wrong") user_1 = User(111, "first_name", False, username="test1") user_2 = User(222, "first_name", False, username="test2") for username in ("@test1", "@test2"): assert filters.Mention(username).check_update(update) assert filters.Mention({username}).check_update(update) for user in (user_1, user_2): assert filters.Mention(user).check_update(update) assert filters.Mention({user}).check_update(update) assert not filters.Mention( ["@test3", 123, user_no_username, user_wrong_username] ).check_update(update) def test_filters_mention_type_text_mention(self, update): user_1 = User(111, "first_name", False, username="test1") user_2 = User(222, "first_name", False, username="test2") user_no_username = User(123456, "first_name", False) user_wrong_username = User(123456, "first_name", False, username="wrong") update.message.text = "test1 test2 user" update.message.entities = [ MessageEntity(MessageEntity.TEXT_MENTION, 0, 5, user=user_1), MessageEntity(MessageEntity.TEXT_MENTION, 6, 5, user=user_2), ] for username in ("@test1", "@test2"): assert filters.Mention(username).check_update(update) assert filters.Mention({username}).check_update(update) for user in (user_1, user_2): assert filters.Mention(user).check_update(update) assert filters.Mention({user}).check_update(update) for user_id in (111, 222): assert filters.Mention(user_id).check_update(update) assert filters.Mention({user_id}).check_update(update) assert not filters.Mention( ["@test3", 123, user_no_username, user_wrong_username] ).check_update(update) def test_filters_giveaway(self, update): assert not filters.GIVEAWAY.check_update(update) update.message.giveaway = "test" assert filters.GIVEAWAY.check_update(update) assert str(filters.GIVEAWAY) == "filters.GIVEAWAY" def test_filters_giveaway_winners(self, update): assert not filters.GIVEAWAY_WINNERS.check_update(update) update.message.giveaway_winners = "test" assert filters.GIVEAWAY_WINNERS.check_update(update) assert str(filters.GIVEAWAY_WINNERS) == "filters.GIVEAWAY_WINNERS" def test_filters_reply_to_story(self, update): assert not filters.REPLY_TO_STORY.check_update(update) update.message.reply_to_story = "test" assert filters.REPLY_TO_STORY.check_update(update) assert str(filters.REPLY_TO_STORY) == "filters.REPLY_TO_STORY" def test_filters_boost_added(self, update): assert not filters.BOOST_ADDED.check_update(update) update.message.boost_added = "test" assert filters.BOOST_ADDED.check_update(update) assert str(filters.BOOST_ADDED) == "filters.BOOST_ADDED" def test_filters_sender_boost_count(self, update): assert not filters.SENDER_BOOST_COUNT.check_update(update) update.message.sender_boost_count = "test" assert filters.SENDER_BOOST_COUNT.check_update(update) assert str(filters.SENDER_BOOST_COUNT) == "filters.SENDER_BOOST_COUNT" python-telegram-bot-21.1.1/tests/ext/test_jobqueue.py000066400000000000000000000576351460724040100227050ustar00rootroot00000000000000#!/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 calendar import datetime as dtm import logging import platform import time import pytest from telegram.ext import ApplicationBuilder, CallbackContext, ContextTypes, Defaults, Job, JobQueue from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS from tests.auxil.pytest_classes import make_bot from tests.auxil.slots import mro_slots if TEST_WITH_OPT_DEPS: import pytz UTC = pytz.utc else: import datetime UTC = datetime.timezone.utc class CustomContext(CallbackContext): pass @pytest.fixture() async def job_queue(app): jq = JobQueue() jq.set_application(app) await jq.start() yield jq await jq.stop() @pytest.mark.skipif( TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is not installed" ) class TestNoJobQueue: def test_init_job_queue(self): with pytest.raises(RuntimeError, match=r"python-telegram-bot\[job-queue\]"): JobQueue() def test_init_job(self): with pytest.raises(RuntimeError, match=r"python-telegram-bot\[job-queue\]"): Job(None) @pytest.mark.skipif( not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed" ) @pytest.mark.skipif( bool(GITHUB_ACTION and platform.system() in ["Windows", "Darwin"]), reason="On Windows & MacOS precise timings are not accurate.", ) @pytest.mark.flaky(10, 1) # Timings aren't quite perfect class TestJobQueue: result = 0 job_time = 0 received_error = None async def test_repr(self, app): jq = JobQueue() jq.set_application(app) assert repr(jq) == f"JobQueue[application={app!r}]" when = dtm.datetime.utcnow() + dtm.timedelta(days=1) callback = self.job_run_once job = jq.run_once(callback, when, name="name2") assert repr(job) == ( f"Job[id={job.job.id}, name={job.name}, callback=job_run_once, " f"trigger=date[" f"{when.strftime('%Y-%m-%d %H:%M:%S UTC')}" f"]]" ) @pytest.fixture(autouse=True) def _reset(self): self.result = 0 self.job_time = 0 self.received_error = None def test_scheduler_configuration(self, job_queue, timezone, bot): # Unfortunately, we can't really test the executor setting explicitly without relying # on protected attributes. However, this should be tested enough implicitly via all the # other tests in here assert job_queue.scheduler_configuration["timezone"] is UTC tz_app = ApplicationBuilder().defaults(Defaults(tzinfo=timezone)).token(bot.token).build() assert tz_app.job_queue.scheduler_configuration["timezone"] is timezone async def job_run_once(self, context): if ( isinstance(context, CallbackContext) and isinstance(context.job, Job) and isinstance(context.update_queue, asyncio.Queue) and context.job.data is None and context.chat_data is None and context.user_data is None and isinstance(context.bot_data, dict) ): self.result += 1 async def job_with_exception(self, context): raise Exception("Test Error") async def job_remove_self(self, context): self.result += 1 context.job.schedule_removal() async def job_run_once_with_data(self, context): self.result += context.job.data async def job_datetime_tests(self, context): self.job_time = time.time() async def error_handler_context(self, update, context): self.received_error = ( str(context.error), context.job, context.user_data, context.chat_data, ) async def error_handler_raise_error(self, *args): raise Exception("Failing bigly") def test_slot_behaviour(self, job_queue): for attr in job_queue.__slots__: assert getattr(job_queue, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(job_queue)) == len(set(mro_slots(job_queue))), "duplicate slot" def test_application_weakref(self, bot): jq = JobQueue() application = ApplicationBuilder().token(bot.token).job_queue(None).build() with pytest.raises(RuntimeError, match="No application was set"): jq.application jq.set_application(application) assert jq.application is application del application with pytest.raises(RuntimeError, match="no longer alive"): jq.application async def test_run_once(self, job_queue): job_queue.run_once(self.job_run_once, 0.1) await asyncio.sleep(0.2) assert self.result == 1 async def test_run_once_timezone(self, job_queue, timezone): """Test the correct handling of aware datetimes""" # we're parametrizing this with two different UTC offsets to exclude the possibility # of an xpass when the test is run in a timezone with the same UTC offset when = dtm.datetime.now(timezone) job_queue.run_once(self.job_run_once, when) await asyncio.sleep(0.1) assert self.result == 1 async def test_job_with_data(self, job_queue): job_queue.run_once(self.job_run_once_with_data, 0.1, data=5) await asyncio.sleep(0.2) assert self.result == 5 async def test_run_repeating(self, job_queue): job_queue.run_repeating(self.job_run_once, 0.1) await asyncio.sleep(0.25) assert self.result == 2 async def test_run_repeating_first(self, job_queue): job_queue.run_repeating(self.job_run_once, 0.5, first=0.2) await asyncio.sleep(0.15) assert self.result == 0 await asyncio.sleep(0.1) assert self.result == 1 async def test_run_repeating_first_timezone(self, job_queue, timezone): """Test correct scheduling of job when passing a timezone-aware datetime as ``first``""" job_queue.run_repeating( self.job_run_once, 0.5, first=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.2) ) await asyncio.sleep(0.05) assert self.result == 0 await asyncio.sleep(0.25) assert self.result == 1 async def test_run_repeating_last(self, job_queue): job_queue.run_repeating(self.job_run_once, 0.25, last=0.4) await asyncio.sleep(0.3) assert self.result == 1 await asyncio.sleep(0.4) assert self.result == 1 async def test_run_repeating_last_timezone(self, job_queue, timezone): """Test correct scheduling of job when passing a timezone-aware datetime as ``last``""" job_queue.run_repeating( self.job_run_once, 0.25, last=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.4) ) await asyncio.sleep(0.3) assert self.result == 1 await asyncio.sleep(0.4) assert self.result == 1 async def test_run_repeating_last_before_first(self, job_queue): with pytest.raises(ValueError, match="'last' must not be before 'first'!"): job_queue.run_repeating(self.job_run_once, 0.5, first=1, last=0.5) async def test_run_repeating_timedelta(self, job_queue): job_queue.run_repeating(self.job_run_once, dtm.timedelta(seconds=0.1)) await asyncio.sleep(0.25) assert self.result == 2 async def test_run_custom(self, job_queue): job_queue.run_custom(self.job_run_once, {"trigger": "interval", "seconds": 0.2}) await asyncio.sleep(0.5) assert self.result == 2 async def test_multiple(self, job_queue): job_queue.run_once(self.job_run_once, 0.1) job_queue.run_once(self.job_run_once, 0.2) job_queue.run_repeating(self.job_run_once, 0.2) await asyncio.sleep(0.55) assert self.result == 4 async def test_disabled(self, job_queue): j1 = job_queue.run_once(self.job_run_once, 0.1) j2 = job_queue.run_repeating(self.job_run_once, 0.5) j1.enabled = False j2.enabled = False await asyncio.sleep(0.6) assert self.result == 0 j1.enabled = True await asyncio.sleep(0.6) assert self.result == 1 async def test_schedule_removal(self, job_queue): j1 = job_queue.run_once(self.job_run_once, 0.3) j2 = job_queue.run_repeating(self.job_run_once, 0.2) await asyncio.sleep(0.25) assert self.result == 1 j1.schedule_removal() j2.schedule_removal() await asyncio.sleep(0.4) assert self.result == 1 async def test_schedule_removal_from_within(self, job_queue): job_queue.run_repeating(self.job_remove_self, 0.1) await asyncio.sleep(0.5) assert self.result == 1 async def test_longer_first(self, job_queue): job_queue.run_once(self.job_run_once, 0.2) job_queue.run_once(self.job_run_once, 0.1) await asyncio.sleep(0.15) assert self.result == 1 async def test_error(self, job_queue): job_queue.run_repeating(self.job_with_exception, 0.1) job_queue.run_repeating(self.job_run_once, 0.2) await asyncio.sleep(0.3) assert self.result == 1 async def test_in_application(self, bot_info): app = ApplicationBuilder().bot(make_bot(bot_info)).build() async with app: assert not app.job_queue.scheduler.running await app.start() assert app.job_queue.scheduler.running app.job_queue.run_repeating(self.job_run_once, 0.2) await asyncio.sleep(0.3) assert self.result == 1 await app.stop() assert not app.job_queue.scheduler.running await asyncio.sleep(1) assert self.result == 1 async def test_time_unit_int(self, job_queue): # Testing seconds in int delta = 0.5 expected_time = time.time() + delta job_queue.run_once(self.job_datetime_tests, delta) await asyncio.sleep(0.6) assert pytest.approx(self.job_time) == expected_time async def test_time_unit_dt_timedelta(self, job_queue): # Testing seconds, minutes and hours as datetime.timedelta object # This is sufficient to test that it actually works. interval = dtm.timedelta(seconds=0.5) expected_time = time.time() + interval.total_seconds() job_queue.run_once(self.job_datetime_tests, interval) await asyncio.sleep(0.6) assert pytest.approx(self.job_time) == expected_time async def test_time_unit_dt_datetime(self, job_queue): # Testing running at a specific datetime delta, now = dtm.timedelta(seconds=0.5), dtm.datetime.now(UTC) when = now + delta expected_time = when.timestamp() job_queue.run_once(self.job_datetime_tests, when) await asyncio.sleep(0.6) assert self.job_time == pytest.approx(expected_time) async def test_time_unit_dt_time_today(self, job_queue): # Testing running at a specific time today delta, now = 0.5, dtm.datetime.now(UTC) expected_time = now + dtm.timedelta(seconds=delta) when = expected_time.time() expected_time = expected_time.timestamp() job_queue.run_once(self.job_datetime_tests, when) await asyncio.sleep(0.6) assert self.job_time == pytest.approx(expected_time) async def test_time_unit_dt_time_tomorrow(self, job_queue): # Testing running at a specific time that has passed today. Since we can't wait a day, we # test if the job's next scheduled execution time has been calculated correctly delta, now = -2, dtm.datetime.now(UTC) when = (now + dtm.timedelta(seconds=delta)).time() expected_time = (now + dtm.timedelta(seconds=delta, days=1)).timestamp() job_queue.run_once(self.job_datetime_tests, when) scheduled_time = job_queue.jobs()[0].next_t.timestamp() assert scheduled_time == pytest.approx(expected_time) async def test_run_daily(self, job_queue): delta, now = 1, dtm.datetime.now(UTC) time_of_day = (now + dtm.timedelta(seconds=delta)).time() expected_reschedule_time = (now + dtm.timedelta(seconds=delta, days=1)).timestamp() job_queue.run_daily(self.job_run_once, time_of_day) await asyncio.sleep(delta + 0.1) assert self.result == 1 scheduled_time = job_queue.jobs()[0].next_t.timestamp() assert scheduled_time == pytest.approx(expected_reschedule_time) @pytest.mark.parametrize("weekday", [0, 1, 2, 3, 4, 5, 6]) async def test_run_daily_days_of_week(self, job_queue, weekday): delta, now = 1, dtm.datetime.now(UTC) time_of_day = (now + dtm.timedelta(seconds=delta)).time() # offset in days until next weekday offset = (weekday + 6 - now.weekday()) % 7 offset = offset if offset > 0 else 7 expected_reschedule_time = (now + dtm.timedelta(seconds=delta, days=offset)).timestamp() job_queue.run_daily(self.job_run_once, time_of_day, days=[weekday]) await asyncio.sleep(delta + 0.1) scheduled_time = job_queue.jobs()[0].next_t.timestamp() assert scheduled_time == pytest.approx(expected_reschedule_time) async def test_run_monthly(self, job_queue, timezone): delta, now = 1, dtm.datetime.now(timezone) expected_reschedule_time = now + dtm.timedelta(seconds=delta) time_of_day = expected_reschedule_time.time().replace(tzinfo=timezone) day = now.day this_months_days = calendar.monthrange(now.year, now.month)[1] if now.month == 12: next_months_days = calendar.monthrange(now.year + 1, 1)[1] else: next_months_days = calendar.monthrange(now.year, now.month + 1)[1] expected_reschedule_time += dtm.timedelta(this_months_days) if day > next_months_days: expected_reschedule_time += dtm.timedelta(next_months_days) expected_reschedule_time = timezone.normalize(expected_reschedule_time) # Adjust the hour for the special case that between now and next month a DST switch happens expected_reschedule_time += dtm.timedelta( hours=time_of_day.hour - expected_reschedule_time.hour ) expected_reschedule_time = expected_reschedule_time.timestamp() job_queue.run_monthly(self.job_run_once, time_of_day, day) await asyncio.sleep(delta + 0.1) assert self.result == 1 scheduled_time = job_queue.jobs()[0].next_t.timestamp() assert scheduled_time == pytest.approx(expected_reschedule_time, rel=1e-3) async def test_run_monthly_non_strict_day(self, job_queue, timezone): delta, now = 1, dtm.datetime.now(timezone) expected_reschedule_time = now + dtm.timedelta(seconds=delta) time_of_day = expected_reschedule_time.time().replace(tzinfo=timezone) expected_reschedule_time += dtm.timedelta( calendar.monthrange(now.year, now.month)[1] ) - dtm.timedelta(days=now.day) # Adjust the hour for the special case that between now & end of month a DST switch happens expected_reschedule_time = timezone.normalize(expected_reschedule_time) expected_reschedule_time += dtm.timedelta( hours=time_of_day.hour - expected_reschedule_time.hour ) expected_reschedule_time = expected_reschedule_time.timestamp() job_queue.run_monthly(self.job_run_once, time_of_day, -1) scheduled_time = job_queue.jobs()[0].next_t.timestamp() assert scheduled_time == pytest.approx(expected_reschedule_time) async def test_default_tzinfo(self, tz_bot): # we're parametrizing this with two different UTC offsets to exclude the possibility # of an xpass when the test is run in a timezone with the same UTC offset app = ApplicationBuilder().bot(tz_bot).build() jq = app.job_queue await jq.start() when = dtm.datetime.now(tz_bot.defaults.tzinfo) + dtm.timedelta(seconds=0.1) jq.run_once(self.job_run_once, when.time()) await asyncio.sleep(0.15) assert self.result == 1 await jq.stop() async def test_get_jobs(self, job_queue): callback = self.job_run_once job1 = job_queue.run_once(callback, 10, name="name1") await asyncio.sleep(0.03) # To stablize tests on windows job2 = job_queue.run_once(callback, 10, name="name1") await asyncio.sleep(0.03) job3 = job_queue.run_once(callback, 10, name="name2") await asyncio.sleep(0.03) assert job_queue.jobs() == (job1, job2, job3) assert job_queue.get_jobs_by_name("name1") == (job1, job2) assert job_queue.get_jobs_by_name("name2") == (job3,) async def test_job_run(self, app): job = app.job_queue.run_repeating(self.job_run_once, 0.02) await asyncio.sleep(0.05) # the job queue has not started yet assert self.result == 0 # so the job will not run await job.run(app) # but this will force it to run assert self.result == 1 async def test_enable_disable_job(self, job_queue): job = job_queue.run_repeating(self.job_run_once, 0.2) await asyncio.sleep(0.5) assert self.result == 2 job.enabled = False assert not job.enabled await asyncio.sleep(0.5) assert self.result == 2 job.enabled = True assert job.enabled await asyncio.sleep(0.5) assert self.result == 4 async def test_remove_job(self, job_queue): job = job_queue.run_repeating(self.job_run_once, 0.2) await asyncio.sleep(0.5) assert self.result == 2 assert not job.removed job.schedule_removal() assert job.removed await asyncio.sleep(0.5) assert self.result == 2 async def test_equality(self, job_queue): job = job_queue.run_repeating(self.job_run_once, 0.2) job_2 = job_queue.run_repeating(self.job_run_once, 0.2) job_3 = Job(self.job_run_once, 0.2) job_3._job = job.job assert job == job # noqa: PLR0124 assert job != job_queue assert job != job_2 assert job == job_3 assert hash(job) == hash(job) assert hash(job) != hash(job_queue) assert hash(job) != hash(job_2) assert hash(job) == hash(job_3) async def test_process_error_context(self, job_queue, app): app.add_error_handler(self.error_handler_context) job = job_queue.run_once(self.job_with_exception, 0.1, chat_id=42, user_id=43) await asyncio.sleep(0.15) assert self.received_error[0] == "Test Error" assert self.received_error[1] is job self.received_error = None await job.run(app) assert self.received_error[0] == "Test Error" assert self.received_error[1] is job assert self.received_error[2] is app.user_data[43] assert self.received_error[3] is app.chat_data[42] # Remove handler app.remove_error_handler(self.error_handler_context) self.received_error = None job = job_queue.run_once(self.job_with_exception, 0.1) await asyncio.sleep(0.15) assert self.received_error is None await job.run(app) assert self.received_error is None async def test_process_error_that_raises_errors(self, job_queue, app, caplog): app.add_error_handler(self.error_handler_raise_error) with caplog.at_level(logging.ERROR): job = job_queue.run_once(self.job_with_exception, 0.1) await asyncio.sleep(0.15) assert len(caplog.records) == 1 rec = caplog.records[-1] assert "An error was raised and an uncaught" in rec.getMessage() caplog.clear() with caplog.at_level(logging.ERROR): await job.run(app) assert len(caplog.records) == 1 rec = caplog.records[-1] assert "uncaught error was raised while handling" in rec.getMessage() caplog.clear() # Remove handler app.remove_error_handler(self.error_handler_raise_error) self.received_error = None with caplog.at_level(logging.ERROR): job = job_queue.run_once(self.job_with_exception, 0.1) await asyncio.sleep(0.15) assert len(caplog.records) == 1 rec = caplog.records[-1] assert "No error handlers are registered" in rec.getMessage() caplog.clear() with caplog.at_level(logging.ERROR): await job.run(app) assert len(caplog.records) == 1 rec = caplog.records[-1] assert rec.name == "telegram.ext.Application" assert "No error handlers are registered" in rec.getMessage() async def test_custom_context(self, bot): application = ( ApplicationBuilder() .token(bot.token) .context_types( ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ) ) .build() ) job_queue = JobQueue() job_queue.set_application(application) async def callback(context): self.result = ( type(context), context.user_data, context.chat_data, type(context.bot_data), ) await job_queue.start() job_queue.run_once(callback, 0.1) await asyncio.sleep(0.15) assert self.result == (CustomContext, None, None, int) await job_queue.stop() async def test_attribute_error(self): job = Job(self.job_run_once) with pytest.raises( AttributeError, match="nor 'apscheduler.job.Job' has attribute 'error'" ): job.error @pytest.mark.parametrize("wait", [True, False]) async def test_wait_on_shut_down(self, job_queue, wait): ready_event = asyncio.Event() async def callback(_): await ready_event.wait() await job_queue.start() job_queue.run_once(callback, when=0.1) await asyncio.sleep(0.15) task = asyncio.create_task(job_queue.stop(wait=wait)) if wait: assert not task.done() ready_event.set() await asyncio.sleep(0.1) # no CancelledError (see source of JobQueue.stop for details) assert task.done() else: await asyncio.sleep(0.1) # unfortunately we will get a CancelledError here assert task.done() async def test_from_aps_job(self, job_queue): job = job_queue.run_once(self.job_run_once, 0.1, name="test_job") aps_job = job_queue.scheduler.get_job(job.id) tg_job = Job.from_aps_job(aps_job) assert tg_job is job assert tg_job.job is aps_job async def test_from_aps_job_missing_reference(self, job_queue): """We manually create a ext.Job and an aps job such that the former has no reference to the latter. Then we test that Job.from_aps_job() still sets the reference correctly. """ job = Job(self.job_run_once) aps_job = job_queue.scheduler.add_job( func=job_queue.job_callback, args=(job_queue, job), trigger="interval", seconds=2, id="test_id", ) assert job.job is None tg_job = Job.from_aps_job(aps_job) assert tg_job is job assert tg_job.job is aps_job python-telegram-bot-21.1.1/tests/ext/test_messagehandler.py000066400000000000000000000172541460724040100240410ustar00rootroot00000000000000#!/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 re import pytest from telegram import ( Bot, CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram.ext import CallbackContext, JobQueue, MessageHandler, filters from telegram.ext.filters import MessageFilter from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=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 = ( "callback_query", "inline_query", "chosen_inline_result", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=1, **request.param) @pytest.fixture(scope="class") def message(bot): message = Message(1, None, Chat(1, ""), from_user=User(1, "", False)) message._unfreeze() message.chat._unfreeze() message.set_bot(bot) return message class TestMessageHandler: test_flag = False SRE_TYPE = type(re.match("", "")) def test_slot_behaviour(self): handler = MessageHandler(filters.ALL, self.callback) for attr in handler.__slots__: assert getattr(handler, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.chat_data, dict) and isinstance(context.bot_data, dict) and ( ( isinstance(context.user_data, dict) and ( isinstance(update.message, Message) or isinstance(update.edited_message, Message) ) ) or ( context.user_data is None and ( isinstance(update.channel_post, Message) or isinstance(update.edited_channel_post, Message) ) ) ) ) def callback_regex1(self, update, context): if context.matches: types = all(type(res) is self.SRE_TYPE for res in context.matches) num = len(context.matches) == 1 self.test_flag = types and num def callback_regex2(self, update, context): if context.matches: types = all(type(res) is self.SRE_TYPE for res in context.matches) num = len(context.matches) == 2 self.test_flag = types and num def test_with_filter(self, message): handler = MessageHandler(filters.ChatType.GROUP, self.callback) message.chat.type = "group" assert handler.check_update(Update(0, message)) message.chat.type = "private" assert not handler.check_update(Update(0, message)) def test_callback_query_with_filter(self, message): class TestFilter(filters.UpdateFilter): flag = False def filter(self, u): self.flag = True test_filter = TestFilter() handler = MessageHandler(test_filter, self.callback) update = Update(1, callback_query=CallbackQuery(1, None, None, message=message)) assert update.effective_message assert not handler.check_update(update) assert not test_filter.flag def test_specific_filters(self, message): f = ( ~filters.UpdateType.MESSAGES & ~filters.UpdateType.CHANNEL_POST & filters.UpdateType.EDITED_CHANNEL_POST ) handler = MessageHandler(f, self.callback) assert not handler.check_update(Update(0, edited_message=message)) assert not handler.check_update(Update(0, message=message)) assert not handler.check_update(Update(0, channel_post=message)) assert handler.check_update(Update(0, edited_channel_post=message)) def test_other_update_types(self, false_update): handler = MessageHandler(None, self.callback) assert not handler.check_update(false_update) assert not handler.check_update("string") def test_filters_returns_empty_dict(self): class DataFilter(MessageFilter): data_filter = True def filter(self, msg: Message): return {} handler = MessageHandler(DataFilter(), self.callback) assert handler.check_update(Update(0, message)) is False async def test_context(self, app, message): handler = MessageHandler( None, self.callback, ) app.add_handler(handler) async with app: await app.process_update(Update(0, message=message)) assert self.test_flag self.test_flag = False await app.process_update(Update(0, edited_message=message)) assert self.test_flag self.test_flag = False await app.process_update(Update(0, channel_post=message)) assert self.test_flag self.test_flag = False await app.process_update(Update(0, edited_channel_post=message)) assert self.test_flag async def test_context_regex(self, app, message): handler = MessageHandler(filters.Regex("one two"), self.callback_regex1) app.add_handler(handler) async with app: message.text = "not it" await app.process_update(Update(0, message)) assert not self.test_flag message.text += " one two now it is" await app.process_update(Update(0, message)) assert self.test_flag async def test_context_multiple_regex(self, app, message): handler = MessageHandler(filters.Regex("one") & filters.Regex("two"), self.callback_regex2) app.add_handler(handler) async with app: message.text = "not it" await app.process_update(Update(0, message)) assert not self.test_flag message.text += " one two now it is" await app.process_update(Update(0, message)) assert self.test_flag python-telegram-bot-21.1.1/tests/ext/test_messagereactionhandler.py000066400000000000000000000313001460724040100255520ustar00rootroot00000000000000#!/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 ( Bot, CallbackQuery, Chat, ChosenInlineResult, Message, MessageReactionCountUpdated, MessageReactionUpdated, PreCheckoutQuery, ReactionCount, ReactionTypeEmoji, ShippingQuery, Update, User, ) from telegram._utils.datetime import UTC from telegram.ext import CallbackContext, JobQueue, MessageReactionHandler from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, {"channel_post": message}, {"edited_channel_post": message}, {"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 = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "chosen_inline_result", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=2, **request.param) @pytest.fixture(scope="class") def time(): return datetime.datetime.now(tz=UTC) @pytest.fixture(scope="class") def message_reaction_updated(time, bot): mr = MessageReactionUpdated( chat=Chat(1, Chat.SUPERGROUP), message_id=1, date=time, old_reaction=[ReactionTypeEmoji("👍")], new_reaction=[ReactionTypeEmoji("👎")], user=User(1, "user_a", False), actor_chat=Chat(2, Chat.SUPERGROUP), ) mr.set_bot(bot) mr._unfreeze() mr.chat._unfreeze() mr.user._unfreeze() return mr @pytest.fixture(scope="class") def message_reaction_count_updated(time, bot): mr = MessageReactionCountUpdated( chat=Chat(1, Chat.SUPERGROUP), message_id=1, date=time, reactions=[ ReactionCount(ReactionTypeEmoji("👍"), 1), ReactionCount(ReactionTypeEmoji("👎"), 1), ], ) mr.set_bot(bot) mr._unfreeze() mr.chat._unfreeze() return mr @pytest.fixture() def message_reaction_update(bot, message_reaction_updated): return Update(0, message_reaction=message_reaction_updated) @pytest.fixture() def message_reaction_count_update(bot, message_reaction_count_updated): return Update(0, message_reaction_count=message_reaction_count_updated) class TestMessageReactionHandler: test_flag = False def test_slot_behaviour(self): action = MessageReactionHandler(self.callback) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update: Update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.user_data, dict if update.effective_user else type(None)) and isinstance(context.chat_data, dict) and isinstance(context.bot_data, dict) and ( isinstance( update.message_reaction, MessageReactionUpdated, ) or isinstance(update.message_reaction_count, MessageReactionCountUpdated) ) ) def test_other_update_types(self, false_update): handler = MessageReactionHandler(self.callback) assert not handler.check_update(false_update) assert not handler.check_update(True) async def test_context(self, app, message_reaction_update, message_reaction_count_update): handler = MessageReactionHandler(callback=self.callback) app.add_handler(handler) async with app: assert handler.check_update(message_reaction_update) await app.process_update(message_reaction_update) assert self.test_flag self.test_flag = False await app.process_update(message_reaction_count_update) assert self.test_flag @pytest.mark.parametrize( argnames=["allowed_types", "expected"], argvalues=[ (MessageReactionHandler.MESSAGE_REACTION_UPDATED, (True, False)), (MessageReactionHandler.MESSAGE_REACTION_COUNT_UPDATED, (False, True)), (MessageReactionHandler.MESSAGE_REACTION, (True, True)), ], ids=["MESSAGE_REACTION_UPDATED", "MESSAGE_REACTION_COUNT_UPDATED", "MESSAGE_REACTION"], ) async def test_message_reaction_types( self, app, message_reaction_update, message_reaction_count_update, expected, allowed_types ): result_1, result_2 = expected handler = MessageReactionHandler(self.callback, message_reaction_types=allowed_types) app.add_handler(handler) async with app: assert handler.check_update(message_reaction_update) == result_1 await app.process_update(message_reaction_update) assert self.test_flag == result_1 self.test_flag = False assert handler.check_update(message_reaction_count_update) == result_2 await app.process_update(message_reaction_count_update) assert self.test_flag == result_2 @pytest.mark.parametrize( argnames=["allowed_types", "kwargs"], argvalues=[ (MessageReactionHandler.MESSAGE_REACTION_COUNT_UPDATED, {"user_username": "user"}), (MessageReactionHandler.MESSAGE_REACTION, {"user_id": 123}), ], ids=["MESSAGE_REACTION_COUNT_UPDATED", "MESSAGE_REACTION"], ) async def test_username_with_anonymous_reaction(self, app, allowed_types, kwargs): with pytest.raises( ValueError, match="You can not filter for users and include anonymous reactions." ): MessageReactionHandler(self.callback, message_reaction_types=allowed_types, **kwargs) @pytest.mark.parametrize( argnames=["chat_id", "expected"], argvalues=[(1, True), ([1], True), (2, False), ([2], False)], ) async def test_with_chat_ids( self, chat_id, expected, message_reaction_update, message_reaction_count_update ): handler = MessageReactionHandler(self.callback, chat_id=chat_id) assert handler.check_update(message_reaction_update) == expected assert handler.check_update(message_reaction_count_update) == expected @pytest.mark.parametrize( argnames=["chat_username"], argvalues=[("group_a",), ("@group_a",), (["group_a"],), (["@group_a"],)], ids=["group_a", "@group_a", "['group_a']", "['@group_a']"], ) async def test_with_chat_usernames( self, chat_username, message_reaction_update, message_reaction_count_update ): handler = MessageReactionHandler(self.callback, chat_username=chat_username) assert not handler.check_update(message_reaction_update) assert not handler.check_update(message_reaction_count_update) message_reaction_update.message_reaction.chat.username = "group_a" message_reaction_count_update.message_reaction_count.chat.username = "group_a" assert handler.check_update(message_reaction_update) assert handler.check_update(message_reaction_count_update) message_reaction_update.message_reaction.chat.username = None message_reaction_count_update.message_reaction_count.chat.username = None @pytest.mark.parametrize( argnames=["user_id", "expected"], argvalues=[(1, True), ([1], True), (2, False), ([2], False)], ) async def test_with_user_ids( self, user_id, expected, message_reaction_update, message_reaction_count_update ): handler = MessageReactionHandler( self.callback, user_id=user_id, message_reaction_types=MessageReactionHandler.MESSAGE_REACTION_UPDATED, ) assert handler.check_update(message_reaction_update) == expected assert not handler.check_update(message_reaction_count_update) @pytest.mark.parametrize( argnames=["user_username"], argvalues=[("user_a",), ("@user_a",), (["user_a"],), (["@user_a"],)], ids=["user_a", "@user_a", "['user_a']", "['@user_a']"], ) async def test_with_user_usernames( self, user_username, message_reaction_update, message_reaction_count_update ): handler = MessageReactionHandler( self.callback, user_username=user_username, message_reaction_types=MessageReactionHandler.MESSAGE_REACTION_UPDATED, ) assert not handler.check_update(message_reaction_update) assert not handler.check_update(message_reaction_count_update) message_reaction_update.message_reaction.user.username = "user_a" assert handler.check_update(message_reaction_update) assert not handler.check_update(message_reaction_count_update) message_reaction_update.message_reaction.user.username = None async def test_message_reaction_count_with_combination( self, message_reaction_count_update, message_reaction_update ): handler = MessageReactionHandler( self.callback, chat_id=2, chat_username="group_a", message_reaction_types=MessageReactionHandler.MESSAGE_REACTION, ) assert not handler.check_update(message_reaction_count_update) message_reaction_count_update.message_reaction_count.chat.id = 2 message_reaction_update.message_reaction.chat.id = 2 assert handler.check_update(message_reaction_count_update) assert handler.check_update(message_reaction_update) message_reaction_count_update.message_reaction_count.chat.id = 1 message_reaction_update.message_reaction.chat.id = 1 message_reaction_count_update.message_reaction_count.chat.username = "group_a" message_reaction_update.message_reaction.chat.username = "group_a" assert handler.check_update(message_reaction_count_update) assert handler.check_update(message_reaction_update) message_reaction_count_update.message_reaction_count.chat.username = None message_reaction_update.message_reaction.chat.username = None async def test_message_reaction_with_combination(self, message_reaction_update): handler = MessageReactionHandler( self.callback, chat_id=2, chat_username="group_a", user_id=2, user_username="user_a", message_reaction_types=MessageReactionHandler.MESSAGE_REACTION_UPDATED, ) assert not handler.check_update(message_reaction_update) message_reaction_update.message_reaction.chat.id = 2 assert handler.check_update(message_reaction_update) message_reaction_update.message_reaction.chat.id = 1 message_reaction_update.message_reaction.chat.username = "group_a" assert handler.check_update(message_reaction_update) message_reaction_update.message_reaction.chat.username = None message_reaction_update.message_reaction.user.id = 2 assert handler.check_update(message_reaction_update) message_reaction_update.message_reaction.user.id = 1 message_reaction_update.message_reaction.user.username = "user_a" assert handler.check_update(message_reaction_update) message_reaction_update.message_reaction.user.username = None python-telegram-bot-21.1.1/tests/ext/test_picklepersistence.py000066400000000000000000001233061460724040100245670ustar00rootroot00000000000000#!/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 gzip import os import pickle import sys from pathlib import Path import pytest from telegram import Chat, Message, TelegramObject, Update, User from telegram.ext import ContextTypes, PersistenceInput, PicklePersistence from telegram.warnings import PTBUserWarning from tests.auxil.files import PROJECT_ROOT_PATH from tests.auxil.pytest_classes import make_bot from tests.auxil.slots import mro_slots @pytest.fixture(autouse=True) def _change_directory(tmp_path: Path): orig_dir = Path.cwd() # Switch to a temporary directory, so we don't have to worry about cleaning up files os.chdir(tmp_path) yield # Go back to original directory os.chdir(orig_dir) @pytest.fixture(autouse=True) def _reset_callback_data_cache(cdc_bot): yield cdc_bot.callback_data_cache.clear_callback_data() cdc_bot.callback_data_cache.clear_callback_queries() @pytest.fixture() def bot_data(): return {"test1": "test2", "test3": {"test4": "test5"}} @pytest.fixture() def chat_data(): return {-12345: {"test1": "test2", "test3": {"test4": "test5"}}, -67890: {3: "test4"}} @pytest.fixture() def user_data(): return {12345: {"test1": "test2", "test3": {"test4": "test5"}}, 67890: {3: "test4"}} @pytest.fixture() def callback_data(): return [("test1", 1000, {"button1": "test0", "button2": "test1"})], {"test1": "test2"} @pytest.fixture() def conversations(): return { "name1": {(123, 123): 3, (456, 654): 4}, "name2": {(123, 321): 1, (890, 890): 2}, "name3": {(123, 321): 1, (890, 890): 2}, } @pytest.fixture() def pickle_persistence(): return PicklePersistence( filepath="pickletest", single_file=False, on_flush=False, ) @pytest.fixture() def pickle_persistence_only_bot(): return PicklePersistence( filepath="pickletest", store_data=PersistenceInput(callback_data=False, user_data=False, chat_data=False), single_file=False, on_flush=False, ) @pytest.fixture() def pickle_persistence_only_chat(): return PicklePersistence( filepath="pickletest", store_data=PersistenceInput(callback_data=False, user_data=False, bot_data=False), single_file=False, on_flush=False, ) @pytest.fixture() def pickle_persistence_only_user(): return PicklePersistence( filepath="pickletest", store_data=PersistenceInput(callback_data=False, chat_data=False, bot_data=False), single_file=False, on_flush=False, ) @pytest.fixture() def pickle_persistence_only_callback(): return PicklePersistence( filepath="pickletest", store_data=PersistenceInput(user_data=False, chat_data=False, bot_data=False), single_file=False, on_flush=False, ) @pytest.fixture() def bad_pickle_files(): for name in [ "pickletest_user_data", "pickletest_chat_data", "pickletest_bot_data", "pickletest_callback_data", "pickletest_conversations", "pickletest", ]: Path(name).write_text("(())") return True @pytest.fixture() def invalid_pickle_files(): for name in [ "pickletest_user_data", "pickletest_chat_data", "pickletest_bot_data", "pickletest_callback_data", "pickletest_conversations", "pickletest", ]: # Just a random way to trigger pickle.UnpicklingError # see https://stackoverflow.com/a/44422239/10606962 with gzip.open(name, "wb") as file: pickle.dump([1, 2, 3], file) return True @pytest.fixture() def good_pickle_files(user_data, chat_data, bot_data, callback_data, conversations): data = { "user_data": user_data, "chat_data": chat_data, "bot_data": bot_data, "callback_data": callback_data, "conversations": conversations, } with Path("pickletest_user_data").open("wb") as f: pickle.dump(user_data, f) with Path("pickletest_chat_data").open("wb") as f: pickle.dump(chat_data, f) with Path("pickletest_bot_data").open("wb") as f: pickle.dump(bot_data, f) with Path("pickletest_callback_data").open("wb") as f: pickle.dump(callback_data, f) with Path("pickletest_conversations").open("wb") as f: pickle.dump(conversations, f) with Path("pickletest").open("wb") as f: pickle.dump(data, f) return True @pytest.fixture() def pickle_files_wo_bot_data(user_data, chat_data, callback_data, conversations): data = { "user_data": user_data, "chat_data": chat_data, "conversations": conversations, "callback_data": callback_data, } with Path("pickletest_user_data").open("wb") as f: pickle.dump(user_data, f) with Path("pickletest_chat_data").open("wb") as f: pickle.dump(chat_data, f) with Path("pickletest_callback_data").open("wb") as f: pickle.dump(callback_data, f) with Path("pickletest_conversations").open("wb") as f: pickle.dump(conversations, f) with Path("pickletest").open("wb") as f: pickle.dump(data, f) return True @pytest.fixture() def pickle_files_wo_callback_data(user_data, chat_data, bot_data, conversations): data = { "user_data": user_data, "chat_data": chat_data, "bot_data": bot_data, "conversations": conversations, } with Path("pickletest_user_data").open("wb") as f: pickle.dump(user_data, f) with Path("pickletest_chat_data").open("wb") as f: pickle.dump(chat_data, f) with Path("pickletest_bot_data").open("wb") as f: pickle.dump(bot_data, f) with Path("pickletest_conversations").open("wb") as f: pickle.dump(conversations, f) with Path("pickletest").open("wb") as f: pickle.dump(data, f) return True @pytest.fixture() def update(bot): user = User(id=321, first_name="test_user", is_bot=False) chat = Chat(id=123, type="group") message = Message(1, datetime.datetime.now(), chat, from_user=user, text="Hi there") message.set_bot(bot) return Update(0, message=message) class TestPicklePersistence: """Just tests the PicklePersistence interface. Integration of persistence into Applictation is tested in TestBasePersistence!""" class DictSub(TelegramObject): # Used for testing our custom (Un)Pickler. def __init__(self, private, normal, b): super().__init__() self._private = private self.normal = normal self._bot = b class SlotsSub(TelegramObject): __slots__ = ("_private", "new_var") def __init__(self, new_var, private): super().__init__() self.new_var = new_var self._private = private class NormalClass: def __init__(self, my_var): self.my_var = my_var async def test_slot_behaviour(self, pickle_persistence): inst = pickle_persistence 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" @pytest.mark.parametrize("on_flush", [True, False]) async def test_on_flush(self, pickle_persistence, on_flush): pickle_persistence.on_flush = on_flush pickle_persistence.single_file = True file_path = Path(pickle_persistence.filepath) await pickle_persistence.update_callback_data("somedata") assert file_path.is_file() != on_flush await pickle_persistence.update_bot_data("data") assert file_path.is_file() != on_flush await pickle_persistence.update_user_data(123, "data") assert file_path.is_file() != on_flush await pickle_persistence.update_chat_data(123, "data") assert file_path.is_file() != on_flush await pickle_persistence.update_conversation("name", (1, 1), "new_state") assert file_path.is_file() != on_flush await pickle_persistence.flush() assert file_path.is_file() async def test_pickle_behaviour_with_slots(self, pickle_persistence): bot_data = await pickle_persistence.get_bot_data() bot_data["message"] = Message(3, datetime.datetime.now(), Chat(2, type="supergroup")) await pickle_persistence.update_bot_data(bot_data) retrieved = await pickle_persistence.get_bot_data() assert retrieved == bot_data async def test_no_files_present_multi_file(self, pickle_persistence): assert await pickle_persistence.get_user_data() == {} assert await pickle_persistence.get_chat_data() == {} assert await pickle_persistence.get_bot_data() == {} assert await pickle_persistence.get_callback_data() is None assert await pickle_persistence.get_conversations("noname") == {} async def test_no_files_present_single_file(self, pickle_persistence): pickle_persistence.single_file = True assert await pickle_persistence.get_user_data() == {} assert await pickle_persistence.get_chat_data() == {} assert await pickle_persistence.get_bot_data() == {} assert await pickle_persistence.get_callback_data() is None assert await pickle_persistence.get_conversations("noname") == {} async def test_with_bad_multi_file(self, pickle_persistence, bad_pickle_files): with pytest.raises(TypeError, match="pickletest_user_data"): await pickle_persistence.get_user_data() with pytest.raises(TypeError, match="pickletest_chat_data"): await pickle_persistence.get_chat_data() with pytest.raises(TypeError, match="pickletest_bot_data"): await pickle_persistence.get_bot_data() with pytest.raises(TypeError, match="pickletest_callback_data"): await pickle_persistence.get_callback_data() with pytest.raises(TypeError, match="pickletest_conversations"): await pickle_persistence.get_conversations("name") async def test_with_invalid_multi_file(self, pickle_persistence, invalid_pickle_files): with pytest.raises(TypeError, match="pickletest_user_data does not contain"): await pickle_persistence.get_user_data() with pytest.raises(TypeError, match="pickletest_chat_data does not contain"): await pickle_persistence.get_chat_data() with pytest.raises(TypeError, match="pickletest_bot_data does not contain"): await pickle_persistence.get_bot_data() with pytest.raises(TypeError, match="pickletest_callback_data does not contain"): await pickle_persistence.get_callback_data() with pytest.raises(TypeError, match="pickletest_conversations does not contain"): await pickle_persistence.get_conversations("name") async def test_with_bad_single_file(self, pickle_persistence, bad_pickle_files): pickle_persistence.single_file = True with pytest.raises(TypeError, match="pickletest"): await pickle_persistence.get_user_data() with pytest.raises(TypeError, match="pickletest"): await pickle_persistence.get_chat_data() with pytest.raises(TypeError, match="pickletest"): await pickle_persistence.get_bot_data() with pytest.raises(TypeError, match="pickletest"): await pickle_persistence.get_callback_data() with pytest.raises(TypeError, match="pickletest"): await pickle_persistence.get_conversations("name") async def test_with_invalid_single_file(self, pickle_persistence, invalid_pickle_files): pickle_persistence.single_file = True with pytest.raises(TypeError, match="pickletest does not contain"): await pickle_persistence.get_user_data() with pytest.raises(TypeError, match="pickletest does not contain"): await pickle_persistence.get_chat_data() with pytest.raises(TypeError, match="pickletest does not contain"): await pickle_persistence.get_bot_data() with pytest.raises(TypeError, match="pickletest does not contain"): await pickle_persistence.get_callback_data() with pytest.raises(TypeError, match="pickletest does not contain"): await pickle_persistence.get_conversations("name") async def test_with_good_multi_file(self, pickle_persistence, good_pickle_files): user_data = await pickle_persistence.get_user_data() assert isinstance(user_data, dict) assert user_data[12345]["test1"] == "test2" assert user_data[67890][3] == "test4" chat_data = await pickle_persistence.get_chat_data() assert isinstance(chat_data, dict) assert chat_data[-12345]["test1"] == "test2" assert chat_data[-67890][3] == "test4" bot_data = await pickle_persistence.get_bot_data() assert isinstance(bot_data, dict) assert bot_data["test1"] == "test2" assert bot_data["test3"]["test4"] == "test5" assert "test0" not in bot_data callback_data = await pickle_persistence.get_callback_data() assert isinstance(callback_data, tuple) assert callback_data[0] == [("test1", 1000, {"button1": "test0", "button2": "test1"})] assert callback_data[1] == {"test1": "test2"} conversation1 = await pickle_persistence.get_conversations("name1") assert isinstance(conversation1, dict) assert conversation1[(123, 123)] == 3 assert conversation1[(456, 654)] == 4 with pytest.raises(KeyError): conversation1[(890, 890)] conversation2 = await pickle_persistence.get_conversations("name2") assert isinstance(conversation1, dict) assert conversation2[(123, 321)] == 1 assert conversation2[(890, 890)] == 2 with pytest.raises(KeyError): conversation2[(123, 123)] async def test_with_good_single_file(self, pickle_persistence, good_pickle_files): pickle_persistence.single_file = True user_data = await pickle_persistence.get_user_data() assert isinstance(user_data, dict) assert user_data[12345]["test1"] == "test2" assert user_data[67890][3] == "test4" chat_data = await pickle_persistence.get_chat_data() assert isinstance(chat_data, dict) assert chat_data[-12345]["test1"] == "test2" assert chat_data[-67890][3] == "test4" bot_data = await pickle_persistence.get_bot_data() assert isinstance(bot_data, dict) assert bot_data["test1"] == "test2" assert bot_data["test3"]["test4"] == "test5" assert "test0" not in bot_data callback_data = await pickle_persistence.get_callback_data() assert isinstance(callback_data, tuple) assert callback_data[0] == [("test1", 1000, {"button1": "test0", "button2": "test1"})] assert callback_data[1] == {"test1": "test2"} conversation1 = await pickle_persistence.get_conversations("name1") assert isinstance(conversation1, dict) assert conversation1[(123, 123)] == 3 assert conversation1[(456, 654)] == 4 with pytest.raises(KeyError): conversation1[(890, 890)] conversation2 = await pickle_persistence.get_conversations("name2") assert isinstance(conversation1, dict) assert conversation2[(123, 321)] == 1 assert conversation2[(890, 890)] == 2 with pytest.raises(KeyError): conversation2[(123, 123)] async def test_with_multi_file_wo_bot_data(self, pickle_persistence, pickle_files_wo_bot_data): user_data = await pickle_persistence.get_user_data() assert isinstance(user_data, dict) assert user_data[12345]["test1"] == "test2" assert user_data[67890][3] == "test4" chat_data = await pickle_persistence.get_chat_data() assert isinstance(chat_data, dict) assert chat_data[-12345]["test1"] == "test2" assert chat_data[-67890][3] == "test4" bot_data = await pickle_persistence.get_bot_data() assert isinstance(bot_data, dict) assert not bot_data.keys() callback_data = await pickle_persistence.get_callback_data() assert isinstance(callback_data, tuple) assert callback_data[0] == [("test1", 1000, {"button1": "test0", "button2": "test1"})] assert callback_data[1] == {"test1": "test2"} conversation1 = await pickle_persistence.get_conversations("name1") assert isinstance(conversation1, dict) assert conversation1[(123, 123)] == 3 assert conversation1[(456, 654)] == 4 with pytest.raises(KeyError): conversation1[(890, 890)] conversation2 = await pickle_persistence.get_conversations("name2") assert isinstance(conversation1, dict) assert conversation2[(123, 321)] == 1 assert conversation2[(890, 890)] == 2 with pytest.raises(KeyError): conversation2[(123, 123)] async def test_with_multi_file_wo_callback_data( self, pickle_persistence, pickle_files_wo_callback_data ): user_data = await pickle_persistence.get_user_data() assert isinstance(user_data, dict) assert user_data[12345]["test1"] == "test2" assert user_data[67890][3] == "test4" chat_data = await pickle_persistence.get_chat_data() assert isinstance(chat_data, dict) assert chat_data[-12345]["test1"] == "test2" assert chat_data[-67890][3] == "test4" bot_data = await pickle_persistence.get_bot_data() assert isinstance(bot_data, dict) assert bot_data["test1"] == "test2" assert bot_data["test3"]["test4"] == "test5" assert "test0" not in bot_data callback_data = await pickle_persistence.get_callback_data() assert callback_data is None conversation1 = await pickle_persistence.get_conversations("name1") assert isinstance(conversation1, dict) assert conversation1[(123, 123)] == 3 assert conversation1[(456, 654)] == 4 with pytest.raises(KeyError): conversation1[(890, 890)] conversation2 = await pickle_persistence.get_conversations("name2") assert isinstance(conversation1, dict) assert conversation2[(123, 321)] == 1 assert conversation2[(890, 890)] == 2 with pytest.raises(KeyError): conversation2[(123, 123)] async def test_with_single_file_wo_bot_data( self, pickle_persistence, pickle_files_wo_bot_data ): pickle_persistence.single_file = True user_data = await pickle_persistence.get_user_data() assert isinstance(user_data, dict) assert user_data[12345]["test1"] == "test2" assert user_data[67890][3] == "test4" chat_data = await pickle_persistence.get_chat_data() assert isinstance(chat_data, dict) assert chat_data[-12345]["test1"] == "test2" assert chat_data[-67890][3] == "test4" bot_data = await pickle_persistence.get_bot_data() assert isinstance(bot_data, dict) assert not bot_data.keys() callback_data = await pickle_persistence.get_callback_data() assert isinstance(callback_data, tuple) assert callback_data[0] == [("test1", 1000, {"button1": "test0", "button2": "test1"})] assert callback_data[1] == {"test1": "test2"} conversation1 = await pickle_persistence.get_conversations("name1") assert isinstance(conversation1, dict) assert conversation1[(123, 123)] == 3 assert conversation1[(456, 654)] == 4 with pytest.raises(KeyError): conversation1[(890, 890)] conversation2 = await pickle_persistence.get_conversations("name2") assert isinstance(conversation1, dict) assert conversation2[(123, 321)] == 1 assert conversation2[(890, 890)] == 2 with pytest.raises(KeyError): conversation2[(123, 123)] async def test_with_single_file_wo_callback_data( self, pickle_persistence, pickle_files_wo_callback_data ): user_data = await pickle_persistence.get_user_data() assert isinstance(user_data, dict) assert user_data[12345]["test1"] == "test2" assert user_data[67890][3] == "test4" chat_data = await pickle_persistence.get_chat_data() assert isinstance(chat_data, dict) assert chat_data[-12345]["test1"] == "test2" assert chat_data[-67890][3] == "test4" bot_data = await pickle_persistence.get_bot_data() assert isinstance(bot_data, dict) assert bot_data["test1"] == "test2" assert bot_data["test3"]["test4"] == "test5" assert "test0" not in bot_data callback_data = await pickle_persistence.get_callback_data() assert callback_data is None conversation1 = await pickle_persistence.get_conversations("name1") assert isinstance(conversation1, dict) assert conversation1[(123, 123)] == 3 assert conversation1[(456, 654)] == 4 with pytest.raises(KeyError): conversation1[(890, 890)] conversation2 = await pickle_persistence.get_conversations("name2") assert isinstance(conversation1, dict) assert conversation2[(123, 321)] == 1 assert conversation2[(890, 890)] == 2 with pytest.raises(KeyError): conversation2[(123, 123)] async def test_updating_multi_file(self, pickle_persistence, good_pickle_files): user_data = await pickle_persistence.get_user_data() user_data[12345]["test3"]["test4"] = "test6" assert pickle_persistence.user_data != user_data await pickle_persistence.update_user_data(12345, user_data[12345]) assert pickle_persistence.user_data == user_data with Path("pickletest_user_data").open("rb") as f: user_data_test = dict(pickle.load(f)) assert user_data_test == user_data await pickle_persistence.drop_user_data(67890) assert 67890 not in await pickle_persistence.get_user_data() chat_data = await pickle_persistence.get_chat_data() chat_data[-12345]["test3"]["test4"] = "test6" assert pickle_persistence.chat_data != chat_data await pickle_persistence.update_chat_data(-12345, chat_data[-12345]) assert pickle_persistence.chat_data == chat_data with Path("pickletest_chat_data").open("rb") as f: chat_data_test = dict(pickle.load(f)) assert chat_data_test == chat_data await pickle_persistence.drop_chat_data(-67890) assert -67890 not in await pickle_persistence.get_chat_data() bot_data = await pickle_persistence.get_bot_data() bot_data["test3"]["test4"] = "test6" assert pickle_persistence.bot_data != bot_data await pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data with Path("pickletest_bot_data").open("rb") as f: bot_data_test = pickle.load(f) assert bot_data_test == bot_data callback_data = await pickle_persistence.get_callback_data() callback_data[1]["test3"] = "test4" assert pickle_persistence.callback_data != callback_data await pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data with Path("pickletest_callback_data").open("rb") as f: callback_data_test = pickle.load(f) assert callback_data_test == callback_data conversation1 = await pickle_persistence.get_conversations("name1") conversation1[(123, 123)] = 5 assert pickle_persistence.conversations["name1"] != conversation1 await pickle_persistence.update_conversation("name1", (123, 123), 5) assert pickle_persistence.conversations["name1"] == conversation1 assert await pickle_persistence.get_conversations("name1") == conversation1 with Path("pickletest_conversations").open("rb") as f: conversations_test = dict(pickle.load(f)) assert conversations_test["name1"] == conversation1 pickle_persistence.conversations = None await pickle_persistence.update_conversation("name1", (123, 123), 5) assert pickle_persistence.conversations["name1"] == {(123, 123): 5} assert await pickle_persistence.get_conversations("name1") == {(123, 123): 5} async def test_updating_single_file(self, pickle_persistence, good_pickle_files): pickle_persistence.single_file = True user_data = await pickle_persistence.get_user_data() user_data[12345]["test3"]["test4"] = "test6" assert pickle_persistence.user_data != user_data await pickle_persistence.update_user_data(12345, user_data[12345]) assert pickle_persistence.user_data == user_data with Path("pickletest").open("rb") as f: user_data_test = dict(pickle.load(f))["user_data"] assert user_data_test == user_data await pickle_persistence.drop_user_data(67890) assert 67890 not in await pickle_persistence.get_user_data() chat_data = await pickle_persistence.get_chat_data() chat_data[-12345]["test3"]["test4"] = "test6" assert pickle_persistence.chat_data != chat_data await pickle_persistence.update_chat_data(-12345, chat_data[-12345]) assert pickle_persistence.chat_data == chat_data with Path("pickletest").open("rb") as f: chat_data_test = dict(pickle.load(f))["chat_data"] assert chat_data_test == chat_data await pickle_persistence.drop_chat_data(-67890) assert -67890 not in await pickle_persistence.get_chat_data() bot_data = await pickle_persistence.get_bot_data() bot_data["test3"]["test4"] = "test6" assert pickle_persistence.bot_data != bot_data await pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data with Path("pickletest").open("rb") as f: bot_data_test = pickle.load(f)["bot_data"] assert bot_data_test == bot_data callback_data = await pickle_persistence.get_callback_data() callback_data[1]["test3"] = "test4" assert pickle_persistence.callback_data != callback_data await pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data with Path("pickletest").open("rb") as f: callback_data_test = pickle.load(f)["callback_data"] assert callback_data_test == callback_data conversation1 = await pickle_persistence.get_conversations("name1") conversation1[(123, 123)] = 5 assert pickle_persistence.conversations["name1"] != conversation1 await pickle_persistence.update_conversation("name1", (123, 123), 5) assert pickle_persistence.conversations["name1"] == conversation1 assert await pickle_persistence.get_conversations("name1") == conversation1 with Path("pickletest").open("rb") as f: conversations_test = dict(pickle.load(f))["conversations"] assert conversations_test["name1"] == conversation1 pickle_persistence.conversations = None await pickle_persistence.update_conversation("name1", (123, 123), 5) assert pickle_persistence.conversations["name1"] == {(123, 123): 5} assert await pickle_persistence.get_conversations("name1") == {(123, 123): 5} async def test_updating_single_file_no_data(self, pickle_persistence): pickle_persistence.single_file = True assert not any( [ pickle_persistence.user_data, pickle_persistence.chat_data, pickle_persistence.bot_data, pickle_persistence.callback_data, pickle_persistence.conversations, ] ) await pickle_persistence.flush() with pytest.raises(FileNotFoundError, match="pickletest"), Path("pickletest").open("rb"): pass async def test_save_on_flush_multi_files(self, pickle_persistence, good_pickle_files): # Should run without error await pickle_persistence.flush() pickle_persistence.on_flush = True user_data = await pickle_persistence.get_user_data() user_data[54321] = {} user_data[54321]["test9"] = "test 10" assert pickle_persistence.user_data != user_data await pickle_persistence.update_user_data(54321, user_data[54321]) assert pickle_persistence.user_data == user_data await pickle_persistence.drop_user_data(0) assert pickle_persistence.user_data == user_data with Path("pickletest_user_data").open("rb") as f: user_data_test = dict(pickle.load(f)) assert user_data_test != user_data chat_data = await pickle_persistence.get_chat_data() chat_data[54321] = {} chat_data[54321]["test9"] = "test 10" assert pickle_persistence.chat_data != chat_data await pickle_persistence.update_chat_data(54321, chat_data[54321]) assert pickle_persistence.chat_data == chat_data await pickle_persistence.drop_chat_data(0) assert pickle_persistence.user_data == user_data with Path("pickletest_chat_data").open("rb") as f: chat_data_test = dict(pickle.load(f)) assert chat_data_test != chat_data bot_data = await pickle_persistence.get_bot_data() bot_data["test6"] = "test 7" assert pickle_persistence.bot_data != bot_data await pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data with Path("pickletest_bot_data").open("rb") as f: bot_data_test = pickle.load(f) assert bot_data_test != bot_data callback_data = await pickle_persistence.get_callback_data() callback_data[1]["test3"] = "test4" assert pickle_persistence.callback_data != callback_data await pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data with Path("pickletest_callback_data").open("rb") as f: callback_data_test = pickle.load(f) assert callback_data_test != callback_data conversation1 = await pickle_persistence.get_conversations("name1") conversation1[(123, 123)] = 5 assert pickle_persistence.conversations["name1"] != conversation1 await pickle_persistence.update_conversation("name1", (123, 123), 5) assert pickle_persistence.conversations["name1"] == conversation1 with Path("pickletest_conversations").open("rb") as f: conversations_test = dict(pickle.load(f)) assert conversations_test["name1"] != conversation1 await pickle_persistence.flush() with Path("pickletest_user_data").open("rb") as f: user_data_test = dict(pickle.load(f)) assert user_data_test == user_data with Path("pickletest_chat_data").open("rb") as f: chat_data_test = dict(pickle.load(f)) assert chat_data_test == chat_data with Path("pickletest_bot_data").open("rb") as f: bot_data_test = pickle.load(f) assert bot_data_test == bot_data with Path("pickletest_conversations").open("rb") as f: conversations_test = dict(pickle.load(f)) assert conversations_test["name1"] == conversation1 async def test_save_on_flush_single_files(self, pickle_persistence, good_pickle_files): # Should run without error await pickle_persistence.flush() pickle_persistence.on_flush = True pickle_persistence.single_file = True user_data = await pickle_persistence.get_user_data() user_data[54321] = {} user_data[54321]["test9"] = "test 10" assert pickle_persistence.user_data != user_data await pickle_persistence.update_user_data(54321, user_data[54321]) assert pickle_persistence.user_data == user_data with Path("pickletest").open("rb") as f: user_data_test = dict(pickle.load(f))["user_data"] assert user_data_test != user_data chat_data = await pickle_persistence.get_chat_data() chat_data[54321] = {} chat_data[54321]["test9"] = "test 10" assert pickle_persistence.chat_data != chat_data await pickle_persistence.update_chat_data(54321, chat_data[54321]) assert pickle_persistence.chat_data == chat_data with Path("pickletest").open("rb") as f: chat_data_test = dict(pickle.load(f))["chat_data"] assert chat_data_test != chat_data bot_data = await pickle_persistence.get_bot_data() bot_data["test6"] = "test 7" assert pickle_persistence.bot_data != bot_data await pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data with Path("pickletest").open("rb") as f: bot_data_test = pickle.load(f)["bot_data"] assert bot_data_test != bot_data callback_data = await pickle_persistence.get_callback_data() callback_data[1]["test3"] = "test4" assert pickle_persistence.callback_data != callback_data await pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data with Path("pickletest").open("rb") as f: callback_data_test = pickle.load(f)["callback_data"] assert callback_data_test != callback_data conversation1 = await pickle_persistence.get_conversations("name1") conversation1[(123, 123)] = 5 assert pickle_persistence.conversations["name1"] != conversation1 await pickle_persistence.update_conversation("name1", (123, 123), 5) assert pickle_persistence.conversations["name1"] == conversation1 with Path("pickletest").open("rb") as f: conversations_test = dict(pickle.load(f))["conversations"] assert conversations_test["name1"] != conversation1 await pickle_persistence.flush() with Path("pickletest").open("rb") as f: user_data_test = dict(pickle.load(f))["user_data"] assert user_data_test == user_data with Path("pickletest").open("rb") as f: chat_data_test = dict(pickle.load(f))["chat_data"] assert chat_data_test == chat_data with Path("pickletest").open("rb") as f: bot_data_test = pickle.load(f)["bot_data"] assert bot_data_test == bot_data with Path("pickletest").open("rb") as f: conversations_test = dict(pickle.load(f))["conversations"] assert conversations_test["name1"] == conversation1 async def test_custom_pickler_unpickler_simple( self, pickle_persistence, good_pickle_files, cdc_bot, recwarn ): bot = cdc_bot update = Update(1) update.set_bot(bot) pickle_persistence.set_bot(bot) # assign the current bot to the persistence data_with_bot = {"current_bot": update} await pickle_persistence.update_chat_data( 12345, data_with_bot ) # also calls BotPickler.dumps() # Test that regular pickle load fails - err_msg = ( "A load persistent id instruction was encountered,\nbut no persistent_load " "function was specified." ) with Path("pickletest_chat_data").open("rb") as f, pytest.raises( pickle.UnpicklingError, match=err_msg if sys.version_info < (3, 12) else err_msg.replace("\n", " "), ): pickle.load(f) # Test that our custom unpickler works as intended -- inserts the current bot # We have to create a new instance otherwise unpickling is skipped pp = PicklePersistence("pickletest", single_file=False, on_flush=False) pp.set_bot(bot) # Set the bot assert (await pp.get_chat_data())[12345]["current_bot"].get_bot() is bot # Now test that pickling of unknown bots in TelegramObjects will be replaced by None- assert not len(recwarn) data_with_bot = {} async with make_bot(token=bot.token) as other_bot: user = User(1, "Dev", False) user.set_bot(other_bot) data_with_bot["unknown_bot_in_user"] = user await pickle_persistence.update_chat_data(12345, data_with_bot) assert len(recwarn) == 1 assert recwarn[-1].category is PTBUserWarning assert str(recwarn[-1].message).startswith("Unknown bot instance found.") assert ( Path(recwarn[-1].filename) == PROJECT_ROOT_PATH / "telegram" / "ext" / "_picklepersistence.py" ), "wrong stacklevel!" pp = PicklePersistence("pickletest", single_file=False, on_flush=False) pp.set_bot(bot) assert (await pp.get_chat_data())[12345]["unknown_bot_in_user"]._bot is None async def test_custom_pickler_unpickler_with_custom_objects( self, cdc_bot, pickle_persistence, good_pickle_files ): bot = cdc_bot dict_s = self.DictSub("private", "normal", bot) slot_s = self.SlotsSub("new_var", "private_var") regular = self.NormalClass(12) pickle_persistence.set_bot(bot) await pickle_persistence.update_user_data( 1232, {"sub_dict": dict_s, "sub_slots": slot_s, "r": regular} ) pp = PicklePersistence("pickletest", single_file=False, on_flush=False) pp.set_bot(bot) # Set the bot data = (await pp.get_user_data())[1232] sub_dict = data["sub_dict"] sub_slots = data["sub_slots"] sub_regular = data["r"] assert sub_dict._bot is bot assert sub_dict.normal == dict_s.normal assert sub_dict._private == dict_s._private assert sub_slots.new_var == slot_s.new_var assert sub_slots._private == slot_s._private assert sub_slots._bot is None # We didn't set the bot, so it shouldn't have it here. assert sub_regular.my_var == regular.my_var @pytest.mark.parametrize( "filepath", ["pickletest", Path("pickletest")], ids=["str filepath", "pathlib.Path filepath"], ) async def test_filepath_argument_types(self, filepath): pick_persist = PicklePersistence( filepath=filepath, on_flush=False, ) await pick_persist.update_user_data(1, 1) assert (await pick_persist.get_user_data())[1] == 1 assert Path(filepath).is_file() @pytest.mark.parametrize("singlefile", [True, False]) @pytest.mark.parametrize("ud", [int, float, complex]) @pytest.mark.parametrize("cd", [int, float, complex]) @pytest.mark.parametrize("bd", [int, float, complex]) async def test_with_context_types(self, ud, cd, bd, singlefile): cc = ContextTypes(user_data=ud, chat_data=cd, bot_data=bd) persistence = PicklePersistence("pickletest", single_file=singlefile, context_types=cc) assert isinstance(await persistence.get_bot_data(), bd) assert await persistence.get_bot_data() == 0 persistence.user_data = None persistence.chat_data = None await persistence.drop_user_data(123) await persistence.drop_chat_data(123) assert isinstance(await persistence.get_user_data(), dict) assert isinstance(await persistence.get_chat_data(), dict) persistence.user_data = None persistence.chat_data = None await persistence.update_user_data(1, ud(1)) await persistence.update_chat_data(1, cd(1)) await persistence.update_bot_data(bd(1)) assert (await persistence.get_user_data())[1] == 1 assert (await persistence.get_chat_data())[1] == 1 assert await persistence.get_bot_data() == 1 await persistence.flush() persistence = PicklePersistence("pickletest", single_file=singlefile, context_types=cc) assert isinstance((await persistence.get_user_data())[1], ud) assert (await persistence.get_user_data())[1] == 1 assert isinstance((await persistence.get_chat_data())[1], cd) assert (await persistence.get_chat_data())[1] == 1 assert isinstance(await persistence.get_bot_data(), bd) assert await persistence.get_bot_data() == 1 async def test_no_write_if_data_did_not_change( self, pickle_persistence, bot_data, user_data, chat_data, conversations, callback_data ): pickle_persistence.single_file = True pickle_persistence.on_flush = False await pickle_persistence.update_bot_data(bot_data) await pickle_persistence.update_user_data(12345, user_data[12345]) await pickle_persistence.update_chat_data(-12345, chat_data[-12345]) await pickle_persistence.update_conversation("name", (1, 1), "new_state") await pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.filepath.is_file() pickle_persistence.filepath.unlink(missing_ok=True) assert not pickle_persistence.filepath.is_file() await pickle_persistence.update_bot_data(bot_data) await pickle_persistence.update_user_data(12345, user_data[12345]) await pickle_persistence.update_chat_data(-12345, chat_data[-12345]) await pickle_persistence.update_conversation("name", (1, 1), "new_state") await pickle_persistence.update_callback_data(callback_data) assert not pickle_persistence.filepath.is_file() python-telegram-bot-21.1.1/tests/ext/test_pollanswerhandler.py000066400000000000000000000070721460724040100246000ustar00rootroot00000000000000#!/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 pytest from telegram import ( Bot, CallbackQuery, Chat, ChosenInlineResult, Message, PollAnswer, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram.ext import CallbackContext, JobQueue, PollAnswerHandler from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, {"channel_post": message}, {"edited_channel_post": message}, {"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 = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "chosen_inline_result", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=2, **request.param) @pytest.fixture() def poll_answer(bot): return Update(0, poll_answer=PollAnswer(1, [0, 1], User(2, "test user", False), Chat(1, ""))) class TestPollAnswerHandler: test_flag = False def test_slot_behaviour(self): handler = PollAnswerHandler(self.callback) for attr in handler.__slots__: assert getattr(handler, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.user_data, dict) and context.chat_data is None and isinstance(context.bot_data, dict) and isinstance(update.poll_answer, PollAnswer) ) def test_other_update_types(self, false_update): handler = PollAnswerHandler(self.callback) assert not handler.check_update(false_update) async def test_context(self, app, poll_answer): handler = PollAnswerHandler(self.callback) app.add_handler(handler) async with app: await app.process_update(poll_answer) assert self.test_flag python-telegram-bot-21.1.1/tests/ext/test_precheckoutqueryhandler.py000066400000000000000000000107771460724040100260220ustar00rootroot00000000000000#!/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 re import pytest from telegram import ( Bot, CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram.ext import CallbackContext, JobQueue, PreCheckoutQueryHandler from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"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)}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, ] ids = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "inline_query", "chosen_inline_result", "shipping_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=1, **request.param) @pytest.fixture(scope="class") def pre_checkout_query(): update = Update( 1, pre_checkout_query=PreCheckoutQuery( "id", User(1, "test user", False), "EUR", 223, "invoice_payload" ), ) update._unfreeze() update.pre_checkout_query._unfreeze() return update class TestPreCheckoutQueryHandler: test_flag = False def test_slot_behaviour(self): inst = PreCheckoutQueryHandler(self.callback) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.user_data, dict) and context.chat_data is None and isinstance(context.bot_data, dict) and isinstance(update.pre_checkout_query, PreCheckoutQuery) ) def test_with_pattern(self, pre_checkout_query): handler = PreCheckoutQueryHandler(self.callback, pattern=".*voice.*") assert handler.check_update(pre_checkout_query) pre_checkout_query.pre_checkout_query.invoice_payload = "nothing here" assert not handler.check_update(pre_checkout_query) def test_with_compiled_pattern(self, pre_checkout_query): handler = PreCheckoutQueryHandler(self.callback, pattern=re.compile(r".*payload")) pre_checkout_query.pre_checkout_query.invoice_payload = "invoice_payload" assert handler.check_update(pre_checkout_query) pre_checkout_query.pre_checkout_query.invoice_payload = "nothing here" assert not handler.check_update(pre_checkout_query) def test_other_update_types(self, false_update): handler = PreCheckoutQueryHandler(self.callback) assert not handler.check_update(false_update) async def test_context(self, app, pre_checkout_query): handler = PreCheckoutQueryHandler(self.callback) app.add_handler(handler) async with app: await app.process_update(pre_checkout_query) assert self.test_flag python-telegram-bot-21.1.1/tests/ext/test_prefixhandler.py000066400000000000000000000153041460724040100237040ustar00rootroot00000000000000# # 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 from telegram.ext import CallbackContext, PrefixHandler, filters from tests.auxil.build_messages import make_command_update, make_message, make_message_update from tests.auxil.slots import mro_slots from tests.ext.test_commandhandler import BaseTest, is_match def combinations(prefixes, commands): return (prefix + command for prefix in prefixes for command in commands) class TestPrefixHandler(BaseTest): # Prefixes and commands with which to test PrefixHandler: PREFIXES = ["!", "#", "mytrig-"] COMMANDS = ["help", "test"] COMBINATIONS = list(combinations(PREFIXES, COMMANDS)) def test_slot_behaviour(self): handler = self.make_default_handler() for attr in handler.__slots__: assert getattr(handler, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" @pytest.fixture(scope="class", params=PREFIXES) def prefix(self, request): return request.param @pytest.fixture(scope="class", params=[1, 2], ids=["single prefix", "multiple prefixes"]) def prefixes(self, request): return TestPrefixHandler.PREFIXES[: request.param] @pytest.fixture(scope="class", params=COMMANDS) def command(self, request): return request.param @pytest.fixture(scope="class", params=[1, 2], ids=["single command", "multiple commands"]) def commands(self, request): return TestPrefixHandler.COMMANDS[: request.param] @pytest.fixture(scope="class") def prefix_message_text(self, prefix, command): return prefix + command @pytest.fixture(scope="class") def prefix_message(self, prefix_message_text): return make_message(prefix_message_text) @pytest.fixture(scope="class") def prefix_message_update(self, prefix_message): return make_message_update(prefix_message) def make_default_handler(self, callback=None, **kwargs): callback = callback or self.callback_basic return PrefixHandler(self.PREFIXES, self.COMMANDS, callback, **kwargs) async def test_basic(self, app, prefix, command): """Test the basic expected response from a prefix handler""" handler = self.make_default_handler() app.add_handler(handler) text = prefix + command assert await self.response(app, make_message_update(text)) assert not is_match(handler, make_message_update(command)) assert not is_match(handler, make_message_update(prefix + "notacommand")) assert not is_match(handler, make_command_update(f"not {text} at start")) assert not is_match( handler, make_message_update(bot=app.bot, message=None, caption="caption") ) handler = PrefixHandler(prefix=["!", "#"], command="cmd", callback=self.callback) assert isinstance(handler.commands, frozenset) assert handler.commands == {"!cmd", "#cmd"} handler = PrefixHandler(prefix="#", command={"cmd", "bmd"}, callback=self.callback) assert isinstance(handler.commands, frozenset) assert handler.commands == {"#cmd", "#bmd"} def test_single_multi_prefixes_commands(self, prefixes, commands, prefix_message_update): """Test various combinations of prefixes and commands""" handler = self.make_default_handler() result = is_match(handler, prefix_message_update) expected = prefix_message_update.message.text in combinations(prefixes, commands) return result == expected def test_edited(self, prefix_message): handler_edited = self.make_default_handler() handler_no_edited = self.make_default_handler(filters=~filters.UpdateType.EDITED_MESSAGE) self._test_edited(prefix_message, handler_edited, handler_no_edited) def test_with_filter(self, prefix_message_text): handler = self.make_default_handler(filters=filters.ChatType.GROUP) text = prefix_message_text assert is_match(handler, make_message_update(text, chat=Chat(-23, Chat.GROUP))) assert not is_match(handler, make_message_update(text, chat=Chat(23, Chat.PRIVATE))) def test_other_update_types(self, false_update): handler = self.make_default_handler() assert not is_match(handler, false_update) def test_filters_for_wrong_command(self, mock_filter): """Filters should not be executed if the command does not match the handler""" handler = self.make_default_handler(filters=mock_filter) assert not is_match(handler, make_message_update("/test")) assert not mock_filter.tested async def test_context(self, app, prefix_message_update): handler = self.make_default_handler(self.callback) app.add_handler(handler) assert await self.response(app, prefix_message_update) async def test_context_args(self, app, prefix_message_text): handler = self.make_default_handler(self.callback_args) await self._test_context_args_or_regex(app, handler, prefix_message_text) async def test_context_regex(self, app, prefix_message_text): handler = self.make_default_handler(self.callback_regex1, filters=filters.Regex("one two")) await self._test_context_args_or_regex(app, handler, prefix_message_text) async def test_context_multiple_regex(self, app, prefix_message_text): handler = self.make_default_handler( self.callback_regex2, filters=filters.Regex("one") & filters.Regex("two") ) await self._test_context_args_or_regex(app, handler, prefix_message_text) def test_collect_additional_context(self, app): handler = self.make_default_handler( self.callback_regex2, filters=filters.Regex("one") & filters.Regex("two") ) context = CallbackContext(application=app) handler.collect_additional_context( context=context, update=None, application=app, check_result=None ) assert context.args is None python-telegram-bot-21.1.1/tests/ext/test_ratelimiter.py000066400000000000000000000331321460724040100233710ustar00rootroot00000000000000#!/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/]. """ We mostly test on directly on AIORateLimiter here, b/c BaseRateLimiter doesn't contain anything notable """ import asyncio import json import platform import time from datetime import datetime from http import HTTPStatus import pytest from telegram import BotCommand, Chat, Message, User from telegram.constants import ParseMode from telegram.error import RetryAfter from telegram.ext import AIORateLimiter, BaseRateLimiter, Defaults, ExtBot from telegram.request import BaseRequest, RequestData from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS @pytest.mark.skipif( TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is not installed" ) class TestNoRateLimiter: def test_init(self): with pytest.raises(RuntimeError, match=r"python-telegram-bot\[rate-limiter\]"): AIORateLimiter() class TestBaseRateLimiter: rl_received = None request_received = None async def test_no_rate_limiter(self, bot): with pytest.raises(ValueError, match="if a `ExtBot.rate_limiter` is set"): await bot.send_message(chat_id=42, text="test", rate_limit_args="something") async def test_argument_passing(self, bot_info, monkeypatch, bot): class TestRateLimiter(BaseRateLimiter): async def initialize(self) -> None: pass async def shutdown(self) -> None: pass async def process_request( self, callback, args, kwargs, endpoint, data, rate_limit_args, ): if TestBaseRateLimiter.rl_received is None: TestBaseRateLimiter.rl_received = [] TestBaseRateLimiter.rl_received.append((endpoint, data, rate_limit_args)) return await callback(*args, **kwargs) class TestRequest(BaseRequest): async def initialize(self) -> None: pass async def shutdown(self) -> None: pass async def do_request(self, *args, **kwargs): if TestBaseRateLimiter.request_received is None: TestBaseRateLimiter.request_received = [] TestBaseRateLimiter.request_received.append((args, kwargs)) # return bot.bot.to_dict() for the `get_me` call in `Bot.initialize` return 200, json.dumps({"ok": True, "result": bot.bot.to_dict()}).encode() defaults = Defaults(parse_mode=ParseMode.HTML) test_request = TestRequest() standard_bot = ExtBot(token=bot.token, defaults=defaults, request=test_request) rl_bot = ExtBot( token=bot.token, defaults=defaults, request=test_request, rate_limiter=TestRateLimiter(), ) async with standard_bot: await standard_bot.set_my_commands( commands=[BotCommand("test", "test")], language_code="en", api_kwargs={"api": "kwargs"}, ) async with rl_bot: await rl_bot.set_my_commands( commands=[BotCommand("test", "test")], language_code="en", rate_limit_args=(43, "test-1"), api_kwargs={"api": "kwargs"}, ) assert len(self.rl_received) == 2 assert self.rl_received[0] == ("getMe", {}, None) assert self.rl_received[1] == ( "setMyCommands", {"commands": [BotCommand("test", "test")], "language_code": "en", "api": "kwargs"}, (43, "test-1"), ) assert len(self.request_received) == 4 # self.request_received[i] = i-th received request # self.request_received[i][0] = i-th received request's args # self.request_received[i][1] = i-th received request's kwargs assert self.request_received[0][1]["url"].endswith("getMe") assert self.request_received[2][1]["url"].endswith("getMe") assert self.request_received[1][0] == self.request_received[3][0] assert self.request_received[1][1].keys() == self.request_received[3][1].keys() for key, value in self.request_received[1][1].items(): if isinstance(value, RequestData): assert value.parameters == self.request_received[3][1][key].parameters assert value.parameters["api"] == "kwargs" else: assert value == self.request_received[3][1][key] @pytest.mark.skipif( not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed" ) @pytest.mark.skipif( bool(GITHUB_ACTION and platform.system() == "Darwin"), reason="The timings are apparently rather inaccurate on MacOS.", ) @pytest.mark.flaky(10, 1) # Timings aren't quite perfect class TestAIORateLimiter: count = 0 call_times = [] class CountRequest(BaseRequest): def __init__(self, retry_after=None): self.retry_after = retry_after async def initialize(self) -> None: pass async def shutdown(self) -> None: pass async def do_request(self, *args, **kwargs): TestAIORateLimiter.count += 1 TestAIORateLimiter.call_times.append(time.time()) if self.retry_after: raise RetryAfter(retry_after=1) url = kwargs.get("url").lower() if url.endswith("getme"): return ( HTTPStatus.OK, json.dumps( {"ok": True, "result": User(id=1, first_name="bot", is_bot=True).to_dict()} ).encode(), ) if url.endswith("sendmessage"): return ( HTTPStatus.OK, json.dumps( { "ok": True, "result": Message( message_id=1, date=datetime.now(), chat=Chat(1, "chat") ).to_dict(), } ).encode(), ) return None @pytest.fixture(autouse=True) def _reset(self): self.count = 0 TestAIORateLimiter.count = 0 self.call_times = [] TestAIORateLimiter.call_times = [] @pytest.mark.parametrize("max_retries", [0, 1, 4]) async def test_max_retries(self, bot, max_retries): bot = ExtBot( token=bot.token, request=self.CountRequest(retry_after=1), rate_limiter=AIORateLimiter( max_retries=max_retries, overall_max_rate=0, group_max_rate=0 ), ) with pytest.raises(RetryAfter): await bot.get_me() # Check that we retried the request the correct number of times assert TestAIORateLimiter.count == max_retries + 1 # Check that the retries were delayed correctly times = TestAIORateLimiter.call_times if len(times) <= 1: return delays = [j - i for i, j in zip(times[:-1], times[1:])] assert delays == pytest.approx([1.1 for _ in range(max_retries)], rel=0.05) async def test_delay_all_pending_on_retry(self, bot): # Makes sure that a RetryAfter blocks *all* pending requests bot = ExtBot( token=bot.token, request=self.CountRequest(retry_after=1), rate_limiter=AIORateLimiter(max_retries=1, overall_max_rate=0, group_max_rate=0), ) task_1 = asyncio.create_task(bot.get_me()) await asyncio.sleep(0.1) task_2 = asyncio.create_task(bot.get_me()) assert not task_1.done() assert not task_2.done() await asyncio.sleep(1.1) assert isinstance(task_1.exception(), RetryAfter) assert not task_2.done() await asyncio.sleep(1.1) assert isinstance(task_2.exception(), RetryAfter) @pytest.mark.parametrize("group_id", [-1, "-1", "@username"]) @pytest.mark.parametrize("chat_id", [1, "1"]) async def test_basic_rate_limiting(self, bot, group_id, chat_id): try: rl_bot = ExtBot( token=bot.token, request=self.CountRequest(retry_after=None), rate_limiter=AIORateLimiter( overall_max_rate=1, overall_time_period=1 / 4, group_max_rate=1, group_time_period=1 / 2, ), ) async with rl_bot: non_group_tasks = {} group_tasks = {} for i in range(4): group_tasks[i] = asyncio.create_task( rl_bot.send_message(chat_id=group_id, text="test") ) for i in range(8): non_group_tasks[i] = asyncio.create_task( rl_bot.send_message(chat_id=chat_id, text="test") ) await asyncio.sleep(0.85) # We expect 5 requests: # 1: `get_me` from `async with rl_bot` # 2: `send_message` at time 0.00 # 3: `send_message` at time 0.25 # 4: `send_message` at time 0.50 # 5: `send_message` at time 0.75 assert TestAIORateLimiter.count == 5 assert sum(1 for task in non_group_tasks.values() if task.done()) < 8 assert sum(1 for task in group_tasks.values() if task.done()) < 4 # 3 seconds after start await asyncio.sleep(3.1 - 0.85) assert all(task.done() for task in non_group_tasks.values()) assert all(task.done() for task in group_tasks.values()) finally: # cleanup await asyncio.gather(*non_group_tasks.values(), *group_tasks.values()) TestAIORateLimiter.count = 0 TestAIORateLimiter.call_times = [] async def test_rate_limiting_no_chat_id(self, bot): try: rl_bot = ExtBot( token=bot.token, request=self.CountRequest(retry_after=None), rate_limiter=AIORateLimiter( overall_max_rate=1, overall_time_period=1 / 2, ), ) async with rl_bot: non_chat_tasks = {} chat_tasks = {} for i in range(4): chat_tasks[i] = asyncio.create_task( rl_bot.send_message(chat_id=-1, text="test") ) for i in range(8): non_chat_tasks[i] = asyncio.create_task(rl_bot.get_me()) await asyncio.sleep(0.6) # We expect 11 requests: # 1: `get_me` from `async with rl_bot` # 2: `send_message` at time 0.00 # 3: `send_message` at time 0.05 # 4: 8 times `get_me` assert TestAIORateLimiter.count == 11 assert sum(1 for task in non_chat_tasks.values() if task.done()) == 8 assert sum(1 for task in chat_tasks.values() if task.done()) == 2 # 1.6 seconds after start await asyncio.sleep(1.6 - 0.6) assert all(task.done() for task in non_chat_tasks.values()) assert all(task.done() for task in chat_tasks.values()) finally: # cleanup await asyncio.gather(*non_chat_tasks.values(), *chat_tasks.values()) TestAIORateLimiter.count = 0 TestAIORateLimiter.call_times = [] @pytest.mark.parametrize("intermediate", [True, False]) async def test_group_caching(self, bot, intermediate): try: max_rate = 1000 rl_bot = ExtBot( token=bot.token, request=self.CountRequest(retry_after=None), rate_limiter=AIORateLimiter( overall_max_rate=max_rate, overall_time_period=1, group_max_rate=max_rate, group_time_period=1, ), ) # Unfortunately, there is no reliable way to test this without checking the internals assert len(rl_bot.rate_limiter._group_limiters) == 0 await asyncio.gather( *(rl_bot.send_message(chat_id=-(i + 1), text=f"{i}") for i in range(513)) ) if intermediate: await rl_bot.send_message(chat_id=-1, text="999") assert 1 <= len(rl_bot.rate_limiter._group_limiters) <= 513 else: await asyncio.sleep(1) await rl_bot.send_message(chat_id=-1, text="999") assert len(rl_bot.rate_limiter._group_limiters) == 1 finally: TestAIORateLimiter.count = 0 TestAIORateLimiter.call_times = [] python-telegram-bot-21.1.1/tests/ext/test_shippingqueryhandler.py000066400000000000000000000073741460724040100253260ustar00rootroot00000000000000#!/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 pytest from telegram import ( Bot, CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message, PreCheckoutQuery, ShippingAddress, ShippingQuery, Update, User, ) from telegram.ext import CallbackContext, JobQueue, ShippingQueryHandler from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"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), "")}, {"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, ] ids = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "inline_query", "chosen_inline_result", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=1, **request.param) @pytest.fixture(scope="class") def shiping_query(): return Update( 1, shipping_query=ShippingQuery( 42, User(1, "test user", False), "invoice_payload", ShippingAddress("EN", "my_state", "my_city", "steer_1", "", "post_code"), ), ) class TestShippingQueryHandler: test_flag = False def test_slot_behaviour(self): inst = ShippingQueryHandler(self.callback) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and isinstance(context.user_data, dict) and context.chat_data is None and isinstance(context.bot_data, dict) and isinstance(update.shipping_query, ShippingQuery) ) def test_other_update_types(self, false_update): handler = ShippingQueryHandler(self.callback) assert not handler.check_update(false_update) async def test_context(self, app, shiping_query): handler = ShippingQueryHandler(self.callback) app.add_handler(handler) async with app: await app.process_update(shiping_query) assert self.test_flag python-telegram-bot-21.1.1/tests/ext/test_stringcommandhandler.py000066400000000000000000000076121460724040100252570ustar00rootroot00000000000000#!/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 pytest from telegram import ( Bot, CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram.ext import CallbackContext, JobQueue, StringCommandHandler from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"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 = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "inline_query", "chosen_inline_result", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=1, **request.param) class TestStringCommandHandler: test_flag = False def test_slot_behaviour(self): inst = StringCommandHandler("sleepy", self.callback) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, str) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and context.user_data is None and context.chat_data is None and isinstance(context.bot_data, dict) ) async def callback_args(self, update, context): self.test_flag = context.args == ["one", "two"] def test_other_update_types(self, false_update): handler = StringCommandHandler("test", self.callback) assert not handler.check_update(false_update) async def test_context(self, app): handler = StringCommandHandler("test", self.callback) app.add_handler(handler) async with app: await app.process_update("/test") assert self.test_flag async def test_context_args(self, app): handler = StringCommandHandler("test", self.callback_args) app.add_handler(handler) async with app: await app.process_update("/test") assert not self.test_flag await app.process_update("/test one two") assert self.test_flag python-telegram-bot-21.1.1/tests/ext/test_stringregexhandler.py000066400000000000000000000106611460724040100247510ustar00rootroot00000000000000#!/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 re import pytest from telegram import ( Bot, CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram.ext import CallbackContext, JobQueue, StringRegexHandler from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"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 = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "inline_query", "chosen_inline_result", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=1, **request.param) class TestStringRegexHandler: test_flag = False def test_slot_behaviour(self): inst = StringRegexHandler("pfft", self.callback) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, str) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) ) async def callback_pattern(self, update, context): if context.matches[0].groups(): self.test_flag = context.matches[0].groups() == ("t", " message") if context.matches[0].groupdict(): self.test_flag = context.matches[0].groupdict() == {"begin": "t", "end": " message"} @pytest.mark.parametrize("compile", [True, False]) async def test_basic(self, app, compile): pattern = "(?P.*)est(?P.*)" if compile: pattern = re.compile("(?P.*)est(?P.*)") handler = StringRegexHandler(pattern, self.callback) app.add_handler(handler) assert handler.check_update("test message") async with app: await app.process_update("test message") assert self.test_flag assert not handler.check_update("does not match") def test_other_update_types(self, false_update): handler = StringRegexHandler("test", self.callback) assert not handler.check_update(false_update) async def test_context_pattern(self, app): handler = StringRegexHandler(r"(t)est(.*)", self.callback_pattern) app.add_handler(handler) async with app: await app.process_update("test message") assert self.test_flag app.remove_handler(handler) handler = StringRegexHandler(r"(t)est(.*)", self.callback_pattern) app.add_handler(handler) await app.process_update("test message") assert self.test_flag python-telegram-bot-21.1.1/tests/ext/test_typehandler.py000066400000000000000000000046501460724040100233720ustar00rootroot00000000000000#!/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 from collections import OrderedDict import pytest from telegram import Bot from telegram.ext import CallbackContext, JobQueue, TypeHandler from tests.auxil.slots import mro_slots class TestTypeHandler: test_flag = False def test_slot_behaviour(self): inst = TypeHandler(dict, self.callback) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, dict) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and context.user_data is None and context.chat_data is None and isinstance(context.bot_data, dict) ) async def test_basic(self, app): handler = TypeHandler(dict, self.callback) app.add_handler(handler) assert handler.check_update({"a": 1, "b": 2}) assert not handler.check_update("not a dict") async with app: await app.process_update({"a": 1, "b": 2}) assert self.test_flag def test_strict(self): handler = TypeHandler(dict, self.callback, strict=True) o = OrderedDict({"a": 1, "b": 2}) assert handler.check_update({"a": 1, "b": 2}) assert not handler.check_update(o) python-telegram-bot-21.1.1/tests/ext/test_updater.py000066400000000000000000001323311460724040100225150ustar00rootroot00000000000000#!/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 logging import platform from collections import defaultdict from http import HTTPStatus from pathlib import Path from random import randrange import pytest from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut from telegram.ext import ExtBot, InvalidCallbackData, Updater from telegram.request import HTTPXRequest from tests.auxil.build_messages import make_message, make_message_update from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.files import TEST_DATA_PATH, data_file from tests.auxil.networking import send_webhook_message from tests.auxil.pytest_classes import PytestBot, make_bot from tests.auxil.slots import mro_slots UNIX_AVAILABLE = False if TEST_WITH_OPT_DEPS: try: from tornado.netutil import bind_unix_socket UNIX_AVAILABLE = True except ImportError: UNIX_AVAILABLE = False from telegram.ext._utils.webhookhandler import WebhookServer @pytest.mark.skipif( TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is not installed" ) class TestNoWebhooks: async def test_no_webhooks(self, bot): async with Updater(bot=bot, update_queue=asyncio.Queue()) as updater: with pytest.raises(RuntimeError, match=r"python-telegram-bot\[webhooks\]"): await updater.start_webhook() @pytest.mark.skipif( not TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is installed", ) class TestUpdater: message_count = 0 received = None attempts = 0 err_handler_called = None cb_handler_called = None offset = 0 test_flag = False response_text = "{1}: {0}{1}: {0}" @pytest.fixture(autouse=True) def _reset(self): self.message_count = 0 self.received = None self.attempts = 0 self.err_handler_called = None self.cb_handler_called = None self.test_flag = False # This is needed instead of pytest's temp_path because the file path gets too long on macOS # otherwise @pytest.fixture() def file_path(self) -> str: path = TEST_DATA_PATH / "test.sock" yield str(path) path.unlink(missing_ok=True) def error_callback(self, error): self.received = error self.err_handler_called.set() def callback(self, update, context): self.received = update.message.text self.cb_handler_called.set() async def test_slot_behaviour(self, updater): async with updater: for at in updater.__slots__: attr = f"_Updater{at}" if at.startswith("__") and not at.endswith("__") else at assert getattr(updater, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(updater)) == len(set(mro_slots(updater))), "duplicate slot" def test_init(self, bot): queue = asyncio.Queue() updater = Updater(bot=bot, update_queue=queue) assert updater.bot is bot assert updater.update_queue is queue def test_repr(self, bot): queue = asyncio.Queue() updater = Updater(bot=bot, update_queue=queue) assert repr(updater) == f"Updater[bot={updater.bot!r}]" async def test_initialize(self, bot, monkeypatch): async def initialize_bot(*args, **kwargs): self.test_flag = True async with make_bot(token=bot.token) as test_bot: monkeypatch.setattr(test_bot, "initialize", initialize_bot) updater = Updater(bot=test_bot, update_queue=asyncio.Queue()) await updater.initialize() assert self.test_flag async def test_shutdown(self, bot, monkeypatch): async def shutdown_bot(*args, **kwargs): self.test_flag = True async with make_bot(token=bot.token) as test_bot: monkeypatch.setattr(test_bot, "shutdown", shutdown_bot) updater = Updater(bot=test_bot, update_queue=asyncio.Queue()) await updater.initialize() await updater.shutdown() assert self.test_flag async def test_multiple_inits_and_shutdowns(self, updater, monkeypatch): self.test_flag = defaultdict(int) async def initialize(*args, **kargs): self.test_flag["init"] += 1 async def shutdown(*args, **kwargs): self.test_flag["shutdown"] += 1 monkeypatch.setattr(updater.bot, "initialize", initialize) monkeypatch.setattr(updater.bot, "shutdown", shutdown) await updater.initialize() await updater.initialize() await updater.initialize() await updater.shutdown() await updater.shutdown() await updater.shutdown() assert self.test_flag["init"] == 1 assert self.test_flag["shutdown"] == 1 async def test_multiple_init_cycles(self, updater): # nothing really to assert - this should just not fail async with updater: await updater.bot.get_me() async with updater: await updater.bot.get_me() @pytest.mark.parametrize("method", ["start_polling", "start_webhook"]) async def test_start_without_initialize(self, updater, method): with pytest.raises(RuntimeError, match="not initialized"): await getattr(updater, method)() @pytest.mark.parametrize("method", ["start_polling", "start_webhook"]) async def test_shutdown_while_running(self, updater, method, monkeypatch): async def set_webhook(*args, **kwargs): return True monkeypatch.setattr(updater.bot, "set_webhook", set_webhook) ip = "127.0.0.1" port = randrange(1024, 49152) # Select random port async with updater: if "webhook" in method: await getattr(updater, method)( ip_address=ip, port=port, ) else: await getattr(updater, method)() with pytest.raises(RuntimeError, match="still running"): await updater.shutdown() await updater.stop() async def test_context_manager(self, monkeypatch, updater): async def initialize(*args, **kwargs): self.test_flag = ["initialize"] async def shutdown(*args, **kwargs): self.test_flag.append("stop") monkeypatch.setattr(Updater, "initialize", initialize) monkeypatch.setattr(Updater, "shutdown", shutdown) async with updater: pass assert self.test_flag == ["initialize", "stop"] async def test_context_manager_exception_on_init(self, monkeypatch, updater): async def initialize(*args, **kwargs): raise RuntimeError("initialize") async def shutdown(*args): self.test_flag = "stop" monkeypatch.setattr(Updater, "initialize", initialize) monkeypatch.setattr(Updater, "shutdown", shutdown) with pytest.raises(RuntimeError, match="initialize"): async with updater: pass assert self.test_flag == "stop" @pytest.mark.parametrize("drop_pending_updates", [True, False]) async def test_polling_basic(self, monkeypatch, updater, drop_pending_updates): updates = asyncio.Queue() await updates.put(Update(update_id=1)) await updates.put(Update(update_id=2)) async def get_updates(*args, **kwargs): if not updates.empty(): next_update = await updates.get() updates.task_done() return [next_update] await asyncio.sleep(0.1) return [] orig_del_webhook = updater.bot.delete_webhook async def delete_webhook(*args, **kwargs): # Dropping pending updates is done by passing the parameter to delete_webhook if kwargs.get("drop_pending_updates"): self.message_count += 1 return await orig_del_webhook(*args, **kwargs) monkeypatch.setattr(updater.bot, "get_updates", get_updates) monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook) async with updater: return_value = await updater.start_polling(drop_pending_updates=drop_pending_updates) assert return_value is updater.update_queue assert updater.running await updates.join() await updater.stop() assert not updater.running assert not (await updater.bot.get_webhook_info()).url if drop_pending_updates: assert self.message_count == 1 else: assert self.message_count == 0 await updates.put(Update(update_id=3)) await updates.put(Update(update_id=4)) # We call the same logic twice to make sure that restarting the updater works as well await updater.start_polling(drop_pending_updates=drop_pending_updates) assert updater.running tasks = asyncio.all_tasks() assert any("Updater:start_polling:polling_task" in t.get_name() for t in tasks) await updates.join() await updater.stop() assert not updater.running assert not (await updater.bot.get_webhook_info()).url self.received = [] self.message_count = 0 while not updater.update_queue.empty(): update = updater.update_queue.get_nowait() self.message_count += 1 self.received.append(update.update_id) assert self.message_count == 4 assert self.received == [1, 2, 3, 4] async def test_polling_mark_updates_as_read(self, monkeypatch, updater, caplog): updates = asyncio.Queue() max_update_id = 3 for i in range(1, max_update_id + 1): await updates.put(Update(update_id=i)) tracking_flag = False received_kwargs = {} expected_kwargs = { "timeout": 0, "read_timeout": "read_timeout", "connect_timeout": "connect_timeout", "write_timeout": "write_timeout", "pool_timeout": "pool_timeout", "allowed_updates": "allowed_updates", } async def get_updates(*args, **kwargs): if tracking_flag: received_kwargs.update(kwargs) if not updates.empty(): next_update = await updates.get() updates.task_done() return [next_update] await asyncio.sleep(0) return [] monkeypatch.setattr(updater.bot, "get_updates", get_updates) async with updater: await updater.start_polling(**expected_kwargs) await updates.join() assert not received_kwargs # Set the flag only now since we want to make sure that the get_updates # is called one last time by updater.stop() tracking_flag = True with caplog.at_level(logging.DEBUG): await updater.stop() # ensure that the last fetched update was still marked as read assert received_kwargs["offset"] == max_update_id + 1 # ensure that the correct arguments where passed to the last `get_updates` call for name, value in expected_kwargs.items(): assert received_kwargs[name] == value assert len(caplog.records) >= 1 log_found = False for record in caplog.records: if not record.getMessage().startswith("Calling `get_updates` one more time"): continue assert record.name == "telegram.ext.Updater" assert record.levelno == logging.DEBUG log_found = True break assert log_found async def test_polling_mark_updates_as_read_timeout(self, monkeypatch, updater, caplog): timeout_event = asyncio.Event() async def get_updates(*args, **kwargs): await asyncio.sleep(0) if timeout_event.is_set(): raise TimedOut("TestMessage") return [] monkeypatch.setattr(updater.bot, "get_updates", get_updates) async with updater: await updater.start_polling() with caplog.at_level(logging.ERROR): timeout_event.set() await updater.stop() assert len(caplog.records) >= 1 log_found = False for record in caplog.records: if not record.getMessage().startswith( "Error while calling `get_updates` one more time" ): continue assert record.name == "telegram.ext.Updater" assert record.exc_info[0] is TimedOut assert str(record.exc_info[1]) == "TestMessage" log_found = True break assert log_found async def test_polling_mark_updates_as_read_failure(self, monkeypatch, updater, caplog): async def get_updates(*args, **kwargs): await asyncio.sleep(0) return [] monkeypatch.setattr(updater.bot, "get_updates", get_updates) async with updater: await updater.start_polling() # Unfortunately, there is no clean way to test this scenario as it should in fact # never happen updater._Updater__polling_cleanup_cb = None with caplog.at_level(logging.DEBUG): await updater.stop() assert len(caplog.records) >= 1 log_found = False for record in caplog.records: if not record.getMessage().startswith("No polling cleanup callback defined"): continue assert record.name == "telegram.ext.Updater" assert record.levelno == logging.WARNING log_found = True break assert log_found async def test_start_polling_already_running(self, updater): async with updater: await updater.start_polling() task = asyncio.create_task(updater.start_polling()) with pytest.raises(RuntimeError, match="already running"): await task await updater.stop() with pytest.raises(RuntimeError, match="not running"): await updater.stop() async def test_start_polling_get_updates_parameters(self, updater, monkeypatch): update_queue = asyncio.Queue() await update_queue.put(Update(update_id=1)) on_stop_flag = False expected = { "timeout": 10, "read_timeout": DEFAULT_NONE, "write_timeout": DEFAULT_NONE, "connect_timeout": DEFAULT_NONE, "pool_timeout": DEFAULT_NONE, "allowed_updates": None, "api_kwargs": None, } async def get_updates(*args, **kwargs): if on_stop_flag: # This is tested in test_polling_mark_updates_as_read await asyncio.sleep(0) return [] for key, value in expected.items(): assert kwargs.pop(key, None) == value offset = kwargs.pop("offset", None) # Check that we don't get any unexpected kwargs assert kwargs == {} if offset is not None and self.message_count != 0: assert offset == self.message_count + 1, "get_updates got wrong `offset` parameter" if not update_queue.empty(): update = await update_queue.get() self.message_count = update.update_id update_queue.task_done() return [update] await asyncio.sleep(0) return [] monkeypatch.setattr(updater.bot, "get_updates", get_updates) async with updater: await updater.start_polling() await update_queue.join() on_stop_flag = True await updater.stop() on_stop_flag = False expected = { "timeout": 42, "read_timeout": 43, "write_timeout": 44, "connect_timeout": 45, "pool_timeout": 46, "allowed_updates": ["message"], "api_kwargs": None, } await update_queue.put(Update(update_id=2)) await updater.start_polling( timeout=42, read_timeout=43, write_timeout=44, connect_timeout=45, pool_timeout=46, allowed_updates=["message"], ) await update_queue.join() on_stop_flag = True await updater.stop() @pytest.mark.parametrize("exception_class", [InvalidToken, TelegramError]) @pytest.mark.parametrize("retries", [3, 0]) async def test_start_polling_bootstrap_retries( self, updater, monkeypatch, exception_class, retries ): async def do_request(*args, **kwargs): self.message_count += 1 raise exception_class(str(self.message_count)) async with updater: # Patch within the context so that updater.bot.initialize can still be called # by the context manager monkeypatch.setattr(HTTPXRequest, "do_request", do_request) if exception_class == InvalidToken: with pytest.raises(InvalidToken, match="1"): await updater.start_polling(bootstrap_retries=retries) else: with pytest.raises(TelegramError, match=str(retries + 1)): await updater.start_polling(bootstrap_retries=retries) @pytest.mark.parametrize( ("error", "callback_should_be_called"), argvalues=[ (TelegramError("TestMessage"), True), (RetryAfter(1), False), (TimedOut("TestMessage"), False), ], ids=("TelegramError", "RetryAfter", "TimedOut"), ) @pytest.mark.parametrize("custom_error_callback", [True, False]) async def test_start_polling_exceptions_and_error_callback( self, monkeypatch, updater, error, callback_should_be_called, custom_error_callback, caplog ): raise_exception = True get_updates_event = asyncio.Event() second_get_updates_event = asyncio.Event() async def get_updates(*args, **kwargs): # So that the main task has a chance to be called await asyncio.sleep(0) if get_updates_event.is_set(): second_get_updates_event.set() if not raise_exception: return [] get_updates_event.set() raise error monkeypatch.setattr(updater.bot, "get_updates", get_updates) monkeypatch.setattr(updater.bot, "set_webhook", lambda *args, **kwargs: True) with pytest.raises(TypeError, match="`error_callback` must not be a coroutine function"): await updater.start_polling(error_callback=get_updates) async with updater: self.err_handler_called = asyncio.Event() with caplog.at_level(logging.ERROR): if custom_error_callback: await updater.start_polling(error_callback=self.error_callback) else: await updater.start_polling() # Also makes sure that the error handler was called await get_updates_event.wait() # wait for get_updates to be called a second time - only now we can expect that # all error handling for the previous call has finished await second_get_updates_event.wait() if callback_should_be_called: # Make sure that the error handler was called if custom_error_callback: assert self.received == error else: assert len(caplog.records) > 0 assert any( "Error while getting Updates: TestMessage" in record.getMessage() and record.name == "telegram.ext.Updater" for record in caplog.records ) # Make sure that get_updates was called assert get_updates_event.is_set() # Make sure that Updater polling keeps running self.err_handler_called.clear() get_updates_event.clear() caplog.clear() # Also makes sure that the error handler was called await get_updates_event.wait() if callback_should_be_called: if custom_error_callback: assert self.received == error else: assert len(caplog.records) > 0 assert any( "Error while getting Updates: TestMessage" in record.getMessage() and record.name == "telegram.ext.Updater" for record in caplog.records ) raise_exception = False await updater.stop() async def test_start_polling_unexpected_shutdown(self, updater, monkeypatch, caplog): update_queue = asyncio.Queue() await update_queue.put(Update(update_id=1)) first_update_event = asyncio.Event() second_update_event = asyncio.Event() async def get_updates(*args, **kwargs): self.message_count = kwargs.get("offset") update = await update_queue.get() first_update_event.set() await second_update_event.wait() return [update] monkeypatch.setattr(updater.bot, "get_updates", get_updates) async with updater: with caplog.at_level(logging.ERROR): await updater.start_polling() await first_update_event.wait() # Unfortunately we need to use the private attribute here to produce the problem updater._running = False second_update_event.set() await asyncio.sleep(1) assert caplog.records assert any( "Updater stopped unexpectedly." in record.getMessage() and record.name == "telegram.ext.Updater" for record in caplog.records ) # Make sure that the update_id offset wasn't increased assert self.message_count < 1 async def test_start_polling_not_running_after_failure(self, updater, monkeypatch): # Unfortunately we have to use some internal logic to trigger an exception async def _start_polling(*args, **kwargs): raise Exception("Test Exception") monkeypatch.setattr(Updater, "_start_polling", _start_polling) async with updater: with pytest.raises(Exception, match="Test Exception"): await updater.start_polling() assert updater.running is False async def test_polling_update_de_json_fails(self, monkeypatch, updater, caplog): updates = asyncio.Queue() raise_exception = True await updates.put(Update(update_id=1)) async def get_updates(*args, **kwargs): if raise_exception: await asyncio.sleep(0.01) raise TypeError("Invalid Data") if not updates.empty(): next_update = await updates.get() updates.task_done() return [next_update] await asyncio.sleep(0) return [] orig_del_webhook = updater.bot.delete_webhook async def delete_webhook(*args, **kwargs): # Dropping pending updates is done by passing the parameter to delete_webhook if kwargs.get("drop_pending_updates"): self.message_count += 1 return await orig_del_webhook(*args, **kwargs) monkeypatch.setattr(updater.bot, "get_updates", get_updates) monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook) async with updater: with caplog.at_level(logging.CRITICAL): await updater.start_polling() assert updater.running await asyncio.sleep(1) assert len(caplog.records) > 0 for record in caplog.records: assert record.getMessage().startswith("Something went wrong processing") assert record.name == "telegram.ext.Updater" # Make sure that everything works fine again when receiving proper updates raise_exception = False await asyncio.sleep(0.5) caplog.clear() with caplog.at_level(logging.CRITICAL): await updates.join() assert len(caplog.records) == 0 await updater.stop() assert not updater.running @pytest.mark.parametrize("ext_bot", [True, False]) @pytest.mark.parametrize("drop_pending_updates", [True, False]) @pytest.mark.parametrize("secret_token", ["SecretToken", None]) @pytest.mark.parametrize( "unix", [None, "file_path", "socket_object"] if UNIX_AVAILABLE else [None] ) async def test_webhook_basic( self, monkeypatch, updater, drop_pending_updates, ext_bot, secret_token, unix, file_path ): # Testing with both ExtBot and Bot to make sure any logic in WebhookHandler # that depends on this distinction works if ext_bot and not isinstance(updater.bot, ExtBot): updater.bot = ExtBot(updater.bot.token) if not ext_bot and type(updater.bot) is not Bot: updater.bot = PytestBot(updater.bot.token) async def delete_webhook(*args, **kwargs): # Dropping pending updates is done by passing the parameter to delete_webhook if kwargs.get("drop_pending_updates"): self.message_count += 1 return True async def set_webhook(*args, **kwargs): return True monkeypatch.setattr(updater.bot, "set_webhook", set_webhook) monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook) ip = "127.0.0.1" port = randrange(1024, 49152) # Select random port async with updater: if unix: socket = file_path if unix == "file_path" else bind_unix_socket(file_path) return_value = await updater.start_webhook( drop_pending_updates=drop_pending_updates, secret_token=secret_token, url_path="TOKEN", unix=socket, webhook_url="string", ) else: return_value = await updater.start_webhook( drop_pending_updates=drop_pending_updates, ip_address=ip, port=port, url_path="TOKEN", secret_token=secret_token, webhook_url="string", ) assert return_value is updater.update_queue assert updater.running # Now, we send an update to the server update = make_message_update("Webhook") await send_webhook_message( ip, port, update.to_json(), "TOKEN", secret_token=secret_token, unix=file_path if unix else None, ) assert (await updater.update_queue.get()).to_dict() == update.to_dict() # Returns Not Found if path is incorrect response = await send_webhook_message( ip, port, "123456", "webhook_handler.py", unix=file_path if unix else None, ) assert response.status_code == HTTPStatus.NOT_FOUND # Returns METHOD_NOT_ALLOWED if method is not allowed response = await send_webhook_message( ip, port, None, "TOKEN", get_method="HEAD", unix=file_path if unix else None, ) assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED if secret_token: # Returns Forbidden if no secret token is set response = await send_webhook_message( ip, port, update.to_json(), "TOKEN", unix=file_path if unix else None, ) assert response.status_code == HTTPStatus.FORBIDDEN assert response.text == self.response_text.format( "Request did not include the secret token", HTTPStatus.FORBIDDEN ) # Returns Forbidden if the secret token is wrong response = await send_webhook_message( ip, port, update.to_json(), "TOKEN", secret_token="NotTheSecretToken", unix=file_path if unix else None, ) assert response.status_code == HTTPStatus.FORBIDDEN assert response.text == self.response_text.format( "Request had the wrong secret token", HTTPStatus.FORBIDDEN ) await updater.stop() assert not updater.running if drop_pending_updates: assert self.message_count == 1 else: assert self.message_count == 0 # We call the same logic twice to make sure that restarting the updater works as well if unix: socket = file_path if unix == "file_path" else bind_unix_socket(file_path) await updater.start_webhook( drop_pending_updates=drop_pending_updates, secret_token=secret_token, unix=socket, webhook_url="string", ) else: await updater.start_webhook( drop_pending_updates=drop_pending_updates, ip_address=ip, port=port, url_path="TOKEN", secret_token=secret_token, webhook_url="string", ) assert updater.running update = make_message_update("Webhook") await send_webhook_message( ip, port, update.to_json(), "" if unix else "TOKEN", secret_token=secret_token, unix=file_path if unix else None, ) assert (await updater.update_queue.get()).to_dict() == update.to_dict() await updater.stop() assert not updater.running async def test_unix_webhook_mutually_exclusive_params(self, updater): async with updater: with pytest.raises(RuntimeError, match="You can not pass unix and listen"): await updater.start_webhook(listen="127.0.0.1", unix="DoesntMatter") with pytest.raises(RuntimeError, match="You can not pass unix and port"): await updater.start_webhook(port=20, unix="DoesntMatter") with pytest.raises(RuntimeError, match="you set unix, you also need to set the URL"): await updater.start_webhook(unix="DoesntMatter") @pytest.mark.skipif( platform.system() != "Windows", reason="Windows is the only platform without unix", ) async def test_no_unix(self, updater): async with updater: with pytest.raises(RuntimeError, match="binding unix sockets."): await updater.start_webhook(unix="DoesntMatter", webhook_url="TOKEN") async def test_start_webhook_already_running(self, updater, monkeypatch): async def return_true(*args, **kwargs): return True monkeypatch.setattr(updater.bot, "set_webhook", return_true) monkeypatch.setattr(updater.bot, "delete_webhook", return_true) ip = "127.0.0.1" port = randrange(1024, 49152) # Select random port async with updater: await updater.start_webhook(ip, port, url_path="TOKEN") task = asyncio.create_task(updater.start_webhook(ip, port, url_path="TOKEN")) with pytest.raises(RuntimeError, match="already running"): await task await updater.stop() with pytest.raises(RuntimeError, match="not running"): await updater.stop() async def test_start_webhook_parameters_passing(self, updater, monkeypatch): expected_delete_webhook = { "drop_pending_updates": None, } expected_set_webhook = dict( certificate=None, max_connections=40, allowed_updates=None, ip_address=None, secret_token=None, **expected_delete_webhook, ) async def set_webhook(*args, **kwargs): for key, value in expected_set_webhook.items(): assert kwargs.pop(key, None) == value, f"set, {key}, {value}" assert kwargs in ( {"url": "http://127.0.0.1:80/"}, {"url": "http://listen:80/"}, {"url": "https://listen-ssl:42/ssl-path"}, ) return True async def delete_webhook(*args, **kwargs): for key, value in expected_delete_webhook.items(): assert kwargs.pop(key, None) == value, f"delete, {key}, {value}" assert kwargs == {} return True async def serve_forever(*args, **kwargs): kwargs.get("ready").set() monkeypatch.setattr(updater.bot, "set_webhook", set_webhook) monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook) monkeypatch.setattr(WebhookServer, "serve_forever", serve_forever) async with updater: await updater.start_webhook() await updater.stop() expected_delete_webhook = { "drop_pending_updates": True, "api_kwargs": None, } expected_set_webhook = dict( certificate=data_file("sslcert.pem").read_bytes(), max_connections=47, allowed_updates=["message"], ip_address="123.456.789", secret_token=None, **expected_delete_webhook, ) await updater.start_webhook( listen="listen", allowed_updates=["message"], drop_pending_updates=True, ip_address="123.456.789", max_connections=47, cert=str(data_file("sslcert.pem").resolve()), ) await updater.stop() await updater.start_webhook( listen="listen-ssl", port=42, url_path="ssl-path", allowed_updates=["message"], drop_pending_updates=True, ip_address="123.456.789", max_connections=47, cert=data_file("sslcert.pem"), key=data_file("sslcert.key"), ) await updater.stop() @pytest.mark.parametrize("invalid_data", [True, False], ids=("invalid data", "valid data")) async def test_webhook_arbitrary_callback_data( self, monkeypatch, cdc_bot, invalid_data, chat_id ): """Here we only test one simple setup. telegram.ext.ExtBot.insert_callback_data is tested extensively in test_bot.py in conjunction with get_updates.""" updater = Updater(bot=cdc_bot, update_queue=asyncio.Queue()) async def return_true(*args, **kwargs): return True try: monkeypatch.setattr(updater.bot, "set_webhook", return_true) monkeypatch.setattr(updater.bot, "delete_webhook", return_true) ip = "127.0.0.1" port = randrange(1024, 49152) # Select random port async with updater: await updater.start_webhook(ip, port, url_path="TOKEN") # Now, we send an update to the server reply_markup = InlineKeyboardMarkup.from_button( InlineKeyboardButton(text="text", callback_data="callback_data") ) if not invalid_data: reply_markup = updater.bot.callback_data_cache.process_keyboard(reply_markup) update = make_message_update( message="test_webhook_arbitrary_callback_data", message_factory=make_message, reply_markup=reply_markup, user=updater.bot.bot, ) await send_webhook_message(ip, port, update.to_json(), "TOKEN") received_update = await updater.update_queue.get() assert received_update.update_id == update.update_id message_dict = update.message.to_dict() received_dict = received_update.message.to_dict() message_dict.pop("reply_markup") received_dict.pop("reply_markup") assert message_dict == received_dict button = received_update.message.reply_markup.inline_keyboard[0][0] if invalid_data: assert isinstance(button.callback_data, InvalidCallbackData) else: assert button.callback_data == "callback_data" await updater.stop() finally: updater.bot.callback_data_cache.clear_callback_data() updater.bot.callback_data_cache.clear_callback_queries() async def test_webhook_invalid_ssl(self, monkeypatch, updater): async def return_true(*args, **kwargs): return True monkeypatch.setattr(updater.bot, "set_webhook", return_true) monkeypatch.setattr(updater.bot, "delete_webhook", return_true) ip = "127.0.0.1" port = randrange(1024, 49152) # Select random port async with updater: with pytest.raises(TelegramError, match="Invalid SSL"): await updater.start_webhook( ip, port, url_path="TOKEN", cert=Path(__file__).as_posix(), key=Path(__file__).as_posix(), bootstrap_retries=0, drop_pending_updates=False, webhook_url=None, allowed_updates=None, ) assert updater.running is False async def test_webhook_ssl_just_for_telegram(self, monkeypatch, updater): """Here we just test that the SSL info is pased to Telegram, but __not__ to the webhook server""" async def set_webhook(**kwargs): self.test_flag.append(bool(kwargs.get("certificate"))) return True async def return_true(*args, **kwargs): return True orig_wh_server_init = WebhookServer.__init__ def webhook_server_init(*args, **kwargs): self.test_flag = [kwargs.get("ssl_ctx") is None] orig_wh_server_init(*args, **kwargs) monkeypatch.setattr(updater.bot, "set_webhook", set_webhook) monkeypatch.setattr(updater.bot, "delete_webhook", return_true) monkeypatch.setattr( "telegram.ext._utils.webhookhandler.WebhookServer.__init__", webhook_server_init ) ip = "127.0.0.1" port = randrange(1024, 49152) # Select random port async with updater: await updater.start_webhook(ip, port, webhook_url=None, cert=Path(__file__).as_posix()) # Now, we send an update to the server update = make_message_update(message="test_message") await send_webhook_message(ip, port, update.to_json()) assert (await updater.update_queue.get()).to_dict() == update.to_dict() assert self.test_flag == [True, True] await updater.stop() @pytest.mark.parametrize("exception_class", [InvalidToken, TelegramError]) @pytest.mark.parametrize("retries", [3, 0]) async def test_start_webhook_bootstrap_retries( self, updater, monkeypatch, exception_class, retries ): async def do_request(*args, **kwargs): self.message_count += 1 raise exception_class(str(self.message_count)) async with updater: # Patch within the context so that updater.bot.initialize can still be called # by the context manager monkeypatch.setattr(HTTPXRequest, "do_request", do_request) if exception_class == InvalidToken: with pytest.raises(InvalidToken, match="1"): await updater.start_webhook(bootstrap_retries=retries) else: with pytest.raises(TelegramError, match=str(retries + 1)): await updater.start_webhook( bootstrap_retries=retries, ) async def test_webhook_invalid_posts(self, updater, monkeypatch): async def return_true(*args, **kwargs): return True monkeypatch.setattr(updater.bot, "set_webhook", return_true) monkeypatch.setattr(updater.bot, "delete_webhook", return_true) ip = "127.0.0.1" port = randrange(1024, 49152) async with updater: await updater.start_webhook(listen=ip, port=port) response = await send_webhook_message(ip, port, None, content_type="invalid") assert response.status_code == HTTPStatus.FORBIDDEN response = await send_webhook_message( ip, port, payload_str="data", content_type="application/xml", ) assert response.status_code == HTTPStatus.FORBIDDEN response = await send_webhook_message(ip, port, "dummy-payload", content_len=None) assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR # httpx already complains about bad content length in _send_webhook_message # before the requests below reach the webhook, but not testing this is probably # okay # response = await send_webhook_message( # ip, port, 'dummy-payload', content_len=-2) # assert response.status_code == HTTPStatus.FORBIDDEN # response = await send_webhook_message( # ip, port, 'dummy-payload', content_len='not-a-number') # assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR await updater.stop() async def test_webhook_update_de_json_fails(self, monkeypatch, updater, caplog): async def delete_webhook(*args, **kwargs): return True async def set_webhook(*args, **kwargs): return True def de_json_fails(*args, **kwargs): raise TypeError("Invalid input") monkeypatch.setattr(updater.bot, "set_webhook", set_webhook) monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook) orig_de_json = Update.de_json monkeypatch.setattr(Update, "de_json", de_json_fails) ip = "127.0.0.1" port = randrange(1024, 49152) # Select random port async with updater: return_value = await updater.start_webhook( ip_address=ip, port=port, url_path="TOKEN", ) assert return_value is updater.update_queue assert updater.running # Now, we send an update to the server update = make_message_update("Webhook") with caplog.at_level(logging.CRITICAL): response = await send_webhook_message(ip, port, update.to_json(), "TOKEN") assert len(caplog.records) == 1 assert caplog.records[-1].getMessage().startswith("Something went wrong processing") assert caplog.records[-1].name == "telegram.ext.Updater" assert response.status_code == 400 assert response.text == self.response_text.format( "Update could not be processed", HTTPStatus.BAD_REQUEST ) # Make sure that everything works fine again when receiving proper updates caplog.clear() with caplog.at_level(logging.CRITICAL): monkeypatch.setattr(Update, "de_json", orig_de_json) await send_webhook_message(ip, port, update.to_json(), "TOKEN") assert (await updater.update_queue.get()).to_dict() == update.to_dict() assert len(caplog.records) == 0 await updater.stop() assert not updater.running python-telegram-bot-21.1.1/tests/request/000077500000000000000000000000001460724040100203255ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/request/__init__.py000066400000000000000000000014661460724040100224450ustar00rootroot00000000000000#!/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/]. python-telegram-bot-21.1.1/tests/request/test_request.py000066400000000000000000000751061460724040100234370ustar00rootroot00000000000000#!/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/]. """Here we run tests directly with HTTPXRequest because that's easier than providing dummy implementations for BaseRequest and we want to test HTTPXRequest anyway.""" import asyncio import json import logging from collections import defaultdict from dataclasses import dataclass from http import HTTPStatus from typing import Any, Callable, Coroutine, Tuple import httpx import pytest from httpx import AsyncHTTPTransport from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.error import ( BadRequest, ChatMigrated, Conflict, Forbidden, InvalidToken, NetworkError, RetryAfter, TelegramError, TimedOut, ) from telegram.request import BaseRequest, RequestData from telegram.request._httpxrequest import HTTPXRequest from telegram.request._requestparameter import RequestParameter from telegram.warnings import PTBDeprecationWarning from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.slots import mro_slots # We only need mixed_rqs fixture, but it uses the others, so pytest needs us to import them as well from .test_requestdata import ( # noqa: F401 file_params, input_media_photo, input_media_video, inputfiles, mixed_params, mixed_rqs, simple_params, ) def mocker_factory( response: bytes, return_code: int = HTTPStatus.OK ) -> Callable[[Tuple[Any]], Coroutine[Any, Any, Tuple[int, bytes]]]: async def make_assertion(*args, **kwargs): return return_code, response return make_assertion @pytest.fixture() async def httpx_request(): async with HTTPXRequest() as rq: yield rq @pytest.mark.skipif( TEST_WITH_OPT_DEPS, reason="Only relevant if the optional dependency is not installed" ) class TestNoSocksHTTP2WithoutRequest: async def test_init(self, bot): with pytest.raises(RuntimeError, match=r"python-telegram-bot\[socks\]"): HTTPXRequest(proxy="socks5://foo") with pytest.raises(RuntimeError, match=r"python-telegram-bot\[http2\]"): HTTPXRequest(http_version="2") @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="Optional dependencies not installed") class TestHTTP2WithRequest: @pytest.mark.parametrize("http_version", ["2", "2.0"]) async def test_http_2_response(self, http_version): httpx_request = HTTPXRequest(http_version=http_version) async with httpx_request: resp = await httpx_request._client.request( url="https://python-telegram-bot.org", method="GET", headers={"User-Agent": httpx_request.USER_AGENT}, ) assert resp.http_version == "HTTP/2" # I picked not TEST_XXX because that's the default, meaning it will run by default for an end-user # who runs pytest. @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="No need to run this twice") class TestRequestWithoutRequest: test_flag = None @pytest.fixture(autouse=True) def _reset(self): self.test_flag = None async def test_init_import_errors(self, monkeypatch): """Makes sure that import errors are forwarded - related to TestNoSocks above""" def __init__(self, *args, **kwargs): raise ImportError("Other Error Message") monkeypatch.setattr(httpx.AsyncClient, "__init__", __init__) # Make sure that other exceptions are forwarded with pytest.raises(ImportError, match=r"Other Error Message"): HTTPXRequest(proxy="socks5://foo") def test_slot_behaviour(self): inst = HTTPXRequest() for attr in inst.__slots__: at = f"_{inst.__class__.__name__}{attr}" if attr.startswith("__") else attr assert getattr(inst, at, "err") != "err", f"got extra slot '{at}'" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" async def test_context_manager(self, monkeypatch): async def initialize(): self.test_flag = ["initialize"] async def shutdown(): self.test_flag.append("stop") httpx_request = HTTPXRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx_request, "shutdown", shutdown) async with httpx_request: pass assert self.test_flag == ["initialize", "stop"] async def test_context_manager_exception_on_init(self, monkeypatch): async def initialize(): raise RuntimeError("initialize") async def shutdown(): self.test_flag = "stop" httpx_request = HTTPXRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx_request, "shutdown", shutdown) with pytest.raises(RuntimeError, match="initialize"): async with httpx_request: pass assert self.test_flag == "stop" async def test_replaced_unprintable_char(self, monkeypatch, httpx_request): """Clients can send arbitrary bytes in callback data. Make sure that we just replace those """ server_response = b'{"result": "test_string\x80"}' monkeypatch.setattr(httpx_request, "do_request", mocker_factory(response=server_response)) assert await httpx_request.post(None, None, None) == "test_string�" # Explicitly call `parse_json_payload` here is well so that this public method is covered # not only implicitly. assert httpx_request.parse_json_payload(server_response) == {"result": "test_string�"} async def test_illegal_json_response(self, monkeypatch, httpx_request: HTTPXRequest, caplog): # for proper JSON it should be `"result":` instead of `result:` server_response = b'{result: "test_string"}' monkeypatch.setattr(httpx_request, "do_request", mocker_factory(response=server_response)) with pytest.raises(TelegramError, match="Invalid server response"), caplog.at_level( logging.ERROR ): await httpx_request.post(None, None, None) assert len(caplog.records) == 1 record = caplog.records[0] assert record.name == "telegram.request.BaseRequest" assert record.getMessage().endswith(f'invalid JSON data: "{server_response.decode()}"') async def test_chat_migrated(self, monkeypatch, httpx_request: HTTPXRequest): server_response = b'{"ok": "False", "parameters": {"migrate_to_chat_id": 123}}' monkeypatch.setattr( httpx_request, "do_request", mocker_factory(response=server_response, return_code=HTTPStatus.BAD_REQUEST), ) with pytest.raises(ChatMigrated, match="New chat id: 123") as exc_info: await httpx_request.post(None, None, None) assert exc_info.value.new_chat_id == 123 async def test_retry_after(self, monkeypatch, httpx_request: HTTPXRequest): server_response = b'{"ok": "False", "parameters": {"retry_after": 42}}' monkeypatch.setattr( httpx_request, "do_request", mocker_factory(response=server_response, return_code=HTTPStatus.BAD_REQUEST), ) with pytest.raises(RetryAfter, match="Retry in 42") as exc_info: await httpx_request.post(None, None, None) assert exc_info.value.retry_after == 42 async def test_unknown_request_params(self, monkeypatch, httpx_request: HTTPXRequest): server_response = b'{"ok": "False", "parameters": {"unknown": "42"}}' monkeypatch.setattr( httpx_request, "do_request", mocker_factory(response=server_response, return_code=HTTPStatus.BAD_REQUEST), ) with pytest.raises( BadRequest, match="{'unknown': '42'}", ): await httpx_request.post(None, None, None) @pytest.mark.parametrize("description", [True, False]) async def test_error_description(self, monkeypatch, httpx_request: HTTPXRequest, description): response_data = {"ok": "False"} if description: match = "ErrorDescription" response_data["description"] = match else: match = "Unknown HTTPError" server_response = json.dumps(response_data).encode("utf-8") monkeypatch.setattr( httpx_request, "do_request", mocker_factory(response=server_response, return_code=-1), ) with pytest.raises(NetworkError, match=match): await httpx_request.post(None, None, None) # Special casing for bad gateway if not description: monkeypatch.setattr( httpx_request, "do_request", mocker_factory(response=server_response, return_code=HTTPStatus.BAD_GATEWAY), ) with pytest.raises(NetworkError, match="Bad Gateway"): await httpx_request.post(None, None, None) @pytest.mark.parametrize( ("code", "exception_class"), [ (HTTPStatus.FORBIDDEN, Forbidden), (HTTPStatus.NOT_FOUND, InvalidToken), (HTTPStatus.UNAUTHORIZED, InvalidToken), (HTTPStatus.BAD_REQUEST, BadRequest), (HTTPStatus.CONFLICT, Conflict), (HTTPStatus.BAD_GATEWAY, NetworkError), (-1, NetworkError), ], ) async def test_special_errors( self, monkeypatch, httpx_request: HTTPXRequest, code, exception_class ): server_response = b'{"ok": "False", "description": "Test Message"}' monkeypatch.setattr( httpx_request, "do_request", mocker_factory(response=server_response, return_code=code), ) with pytest.raises(exception_class, match="Test Message"): await httpx_request.post("", None, None) @pytest.mark.parametrize( ("exception", "catch_class", "match"), [ (TelegramError("TelegramError"), TelegramError, "TelegramError"), ( RuntimeError("CustomError"), NetworkError, r"HTTP implementation: RuntimeError\('CustomError'\)", ), ], ) async def test_exceptions_in_do_request( self, monkeypatch, httpx_request: HTTPXRequest, exception, catch_class, match ): async def do_request(*args, **kwargs): raise exception monkeypatch.setattr( httpx_request, "do_request", do_request, ) with pytest.raises(catch_class, match=match) as exc_info: await httpx_request.post(None, None, None) if catch_class is NetworkError: assert exc_info.value.__cause__ is exception async def test_retrieve(self, monkeypatch, httpx_request): """Here we just test that retrieve gives us the raw bytes instead of trying to parse them as json """ server_response = b'{"result": "test_string\x80"}' monkeypatch.setattr(httpx_request, "do_request", mocker_factory(response=server_response)) assert await httpx_request.retrieve(None, None) == server_response async def test_timeout_propagation_to_do_request(self, monkeypatch, httpx_request): async def make_assertion(*args, **kwargs): self.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": {}}' monkeypatch.setattr(httpx_request, "do_request", make_assertion) await httpx_request.post("url", None) assert self.test_flag == (DEFAULT_NONE, DEFAULT_NONE, DEFAULT_NONE, DEFAULT_NONE) await httpx_request.post( "url", None, read_timeout=1, connect_timeout=2, write_timeout=3, pool_timeout=4 ) assert self.test_flag == (1, 2, 3, 4) def test_read_timeout_not_implemented(self): class SimpleRequest(BaseRequest): async def do_request(self, *args, **kwargs): raise httpx.ReadTimeout("read timeout") async def initialize(self) -> None: pass async def shutdown(self) -> None: pass with pytest.raises(NotImplementedError): SimpleRequest().read_timeout @pytest.mark.parametrize("media", [True, False]) async def test_timeout_propagation_write_timeout( self, monkeypatch, media, input_media_photo, recwarn # noqa: F811 ): 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]: self.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() data = {"string": "string", "int": 1, "float": 1.0} if media: data["media"] = input_media_photo request_data = RequestData( parameters=[RequestParameter.from_input(key, value) for key, value in data.items()], ) # First make sure that custom timeouts are always respected await custom_request.post( "url", request_data, read_timeout=1, connect_timeout=2, write_timeout=3, pool_timeout=4 ) assert self.test_flag == (1, 2, 3, 4) # Now also ensure that the default timeout for media requests is 20 seconds await custom_request.post("url", request_data) assert self.test_flag == ( DEFAULT_NONE, DEFAULT_NONE, 20 if media else DEFAULT_NONE, DEFAULT_NONE, ) print("warnings") for entry in recwarn: print(entry.message) if media: assert len(recwarn) == 1 assert "will default to `BaseRequest.DEFAULT_NONE` instead of 20" in str( recwarn[0].message ) assert recwarn[0].category is PTBDeprecationWarning assert recwarn[0].filename == __file__ else: assert len(recwarn) == 0 @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="No need to run this twice") class TestHTTPXRequestWithoutRequest: test_flag = None @pytest.fixture(autouse=True) def _reset(self): self.test_flag = None # We parametrize this to make sure that the legacy `proxy_url` argument is still supported @pytest.mark.parametrize("proxy_argument", ["proxy", "proxy_url"]) def test_init(self, monkeypatch, proxy_argument): @dataclass class Client: timeout: object proxy: object limits: object http1: object http2: object transport: object = None monkeypatch.setattr(httpx, "AsyncClient", Client) request = HTTPXRequest() assert request._client.timeout == httpx.Timeout(connect=5.0, read=5.0, write=5.0, pool=1.0) assert request._client.proxy is None assert request._client.limits == httpx.Limits( max_connections=1, max_keepalive_connections=1 ) assert request._client.http1 is True assert not request._client.http2 kwargs = { "connection_pool_size": 42, proxy_argument: "proxy", "connect_timeout": 43, "read_timeout": 44, "write_timeout": 45, "pool_timeout": 46, } request = HTTPXRequest(**kwargs) assert request._client.proxy == "proxy" assert request._client.limits == httpx.Limits( max_connections=42, max_keepalive_connections=42 ) assert request._client.timeout == httpx.Timeout(connect=43, read=44, write=45, pool=46) def test_proxy_mutually_exclusive(self): with pytest.raises(ValueError, match="mutually exclusive"): HTTPXRequest(proxy="proxy", proxy_url="proxy_url") def test_proxy_url_deprecation_warning(self, recwarn): HTTPXRequest(proxy_url="http://127.0.0.1:3128") assert len(recwarn) == 1 assert recwarn[0].category is PTBDeprecationWarning assert "`proxy_url` is deprecated" in str(recwarn[0].message) assert recwarn[0].filename == __file__, "incorrect stacklevel" async def test_multiple_inits_and_shutdowns(self, monkeypatch): self.test_flag = defaultdict(int) orig_init = httpx.AsyncClient.__init__ orig_aclose = httpx.AsyncClient.aclose class Client(httpx.AsyncClient): def __init__(*args, **kwargs): orig_init(*args, **kwargs) self.test_flag["init"] += 1 async def aclose(*args, **kwargs): await orig_aclose(*args, **kwargs) self.test_flag["shutdown"] += 1 monkeypatch.setattr(httpx, "AsyncClient", Client) # Create a new one instead of using the fixture so that the mocking can work httpx_request = HTTPXRequest() await httpx_request.initialize() await httpx_request.initialize() await httpx_request.initialize() await httpx_request.shutdown() await httpx_request.shutdown() await httpx_request.shutdown() assert self.test_flag["init"] == 1 assert self.test_flag["shutdown"] == 1 async def test_multiple_init_cycles(self): # nothing really to assert - this should just not fail httpx_request = HTTPXRequest() async with httpx_request: await httpx_request.do_request(url="https://python-telegram-bot.org", method="GET") async with httpx_request: await httpx_request.do_request(url="https://python-telegram-bot.org", method="GET") async def test_http_version_error(self): with pytest.raises(ValueError, match="`http_version` must be either"): HTTPXRequest(http_version="1.0") async def test_http_1_response(self): httpx_request = HTTPXRequest(http_version="1.1") async with httpx_request: resp = await httpx_request._client.request( url="https://python-telegram-bot.org", method="GET", headers={"User-Agent": httpx_request.USER_AGENT}, ) assert resp.http_version == "HTTP/1.1" async def test_do_request_after_shutdown(self, httpx_request): await httpx_request.shutdown() with pytest.raises(RuntimeError, match="not initialized"): await httpx_request.do_request(url="url", method="GET") async def test_context_manager(self, monkeypatch): async def initialize(): self.test_flag = ["initialize"] async def aclose(*args): self.test_flag.append("stop") httpx_request = HTTPXRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx.AsyncClient, "aclose", aclose) async with httpx_request: pass assert self.test_flag == ["initialize", "stop"] async def test_context_manager_exception_on_init(self, monkeypatch): async def initialize(): raise RuntimeError("initialize") async def aclose(*args): self.test_flag = "stop" httpx_request = HTTPXRequest() monkeypatch.setattr(httpx_request, "initialize", initialize) monkeypatch.setattr(httpx.AsyncClient, "aclose", aclose) with pytest.raises(RuntimeError, match="initialize"): async with httpx_request: pass assert self.test_flag == "stop" async def test_do_request_default_timeouts(self, monkeypatch): default_timeouts = httpx.Timeout(connect=42, read=43, write=44, pool=45) async def make_assertion(_, **kwargs): self.test_flag = kwargs.get("timeout") == default_timeouts return httpx.Response(HTTPStatus.OK) async with HTTPXRequest( connect_timeout=default_timeouts.connect, read_timeout=default_timeouts.read, write_timeout=default_timeouts.write, pool_timeout=default_timeouts.pool, ) as httpx_request: monkeypatch.setattr(httpx.AsyncClient, "request", make_assertion) await httpx_request.do_request(method="GET", url="URL") assert self.test_flag async def test_do_request_manual_timeouts(self, monkeypatch, httpx_request): default_timeouts = httpx.Timeout(connect=42, read=43, write=44, pool=45) manual_timeouts = httpx.Timeout(connect=52, read=53, write=54, pool=55) async def make_assertion(_, **kwargs): self.test_flag = kwargs.get("timeout") == manual_timeouts return httpx.Response(HTTPStatus.OK) async with HTTPXRequest( connect_timeout=default_timeouts.connect, read_timeout=default_timeouts.read, write_timeout=default_timeouts.write, pool_timeout=default_timeouts.pool, ) as httpx_request: monkeypatch.setattr(httpx.AsyncClient, "request", make_assertion) await httpx_request.do_request( method="GET", url="URL", connect_timeout=manual_timeouts.connect, read_timeout=manual_timeouts.read, write_timeout=manual_timeouts.write, pool_timeout=manual_timeouts.pool, ) assert self.test_flag async def test_do_request_params_no_data(self, monkeypatch, httpx_request): async def make_assertion(self, **kwargs): method_assertion = kwargs.get("method") == "method" url_assertion = kwargs.get("url") == "url" files_assertion = kwargs.get("files") is None data_assertion = kwargs.get("data") is None if method_assertion and url_assertion and files_assertion and data_assertion: return httpx.Response(HTTPStatus.OK) return httpx.Response(HTTPStatus.BAD_REQUEST) monkeypatch.setattr(httpx.AsyncClient, "request", make_assertion) code, _ = await httpx_request.do_request(method="method", url="url") assert code == HTTPStatus.OK async def test_do_request_params_with_data( self, monkeypatch, httpx_request, mixed_rqs # noqa: F811 ): async def make_assertion(self, **kwargs): method_assertion = kwargs.get("method") == "method" url_assertion = kwargs.get("url") == "url" files_assertion = kwargs.get("files") == mixed_rqs.multipart_data data_assertion = kwargs.get("data") == mixed_rqs.json_parameters if method_assertion and url_assertion and files_assertion and data_assertion: return httpx.Response(HTTPStatus.OK) return httpx.Response(HTTPStatus.BAD_REQUEST) monkeypatch.setattr(httpx.AsyncClient, "request", make_assertion) code, _ = await httpx_request.do_request( method="method", url="url", request_data=mixed_rqs, ) assert code == HTTPStatus.OK async def test_do_request_return_value(self, monkeypatch, httpx_request): async def make_assertion(self, method, url, headers, timeout, files, data): return httpx.Response(123, content=b"content") monkeypatch.setattr(httpx.AsyncClient, "request", make_assertion) code, content = await httpx_request.do_request( "method", "url", ) assert code == 123 assert content == b"content" @pytest.mark.parametrize( ("raised_exception", "expected_class", "expected_message"), [ (httpx.TimeoutException("timeout"), TimedOut, "Timed out"), (httpx.ReadError("read_error"), NetworkError, "httpx.ReadError: read_error"), ], ) async def test_do_request_exceptions( self, monkeypatch, httpx_request, raised_exception, expected_class, expected_message ): async def make_assertion(self, method, url, headers, timeout, files, data): raise raised_exception monkeypatch.setattr(httpx.AsyncClient, "request", make_assertion) with pytest.raises(expected_class, match=expected_message) as exc_info: await httpx_request.do_request( "method", "url", ) assert exc_info.value.__cause__ is raised_exception async def test_do_request_pool_timeout(self, monkeypatch): pool_timeout = httpx.PoolTimeout("pool timeout") async def request(_, **kwargs): if self.test_flag is None: self.test_flag = True else: raise pool_timeout return httpx.Response(HTTPStatus.OK) monkeypatch.setattr(httpx.AsyncClient, "request", request) async with HTTPXRequest(pool_timeout=0.02) as httpx_request: with pytest.raises(TimedOut, match="Pool timeout") as exc_info: await asyncio.gather( httpx_request.do_request(method="GET", url="URL"), httpx_request.do_request(method="GET", url="URL"), ) assert exc_info.value.__cause__ is pool_timeout @pytest.mark.parametrize("media", [True, False]) async def test_do_request_write_timeout( self, monkeypatch, media, httpx_request, input_media_photo, recwarn # noqa: F811 ): async def request(_, **kwargs): self.test_flag = kwargs.get("timeout") return httpx.Response(HTTPStatus.OK, content=b'{"ok": "True", "result": {}}') monkeypatch.setattr(httpx.AsyncClient, "request", request) data = {"string": "string", "int": 1, "float": 1.0} if media: data["media"] = input_media_photo request_data = RequestData( parameters=[RequestParameter.from_input(key, value) for key, value in data.items()], ) # First make sure that custom timeouts are always respected await httpx_request.post( "url", request_data, read_timeout=1, connect_timeout=2, write_timeout=3, pool_timeout=4 ) assert self.test_flag == httpx.Timeout(read=1, connect=2, write=3, pool=4) # Now also ensure that the default timeout for media requests is 20 seconds await httpx_request.post("url", request_data) assert self.test_flag == httpx.Timeout(read=5, connect=5, write=20 if media else 5, pool=1) # Just for double-checking, since warnings are issued for implementations of BaseRequest # other than HTTPXRequest assert len(recwarn) == 0 @pytest.mark.parametrize("init", [True, False]) async def test_setting_media_write_timeout( self, monkeypatch, init, input_media_photo, recwarn # noqa: F811 ): httpx_request = HTTPXRequest(media_write_timeout=42) if init else HTTPXRequest() async def request(_, **kwargs): self.test_flag = kwargs["timeout"].write return httpx.Response(HTTPStatus.OK, content=b'{"ok": "True", "result": {}}') monkeypatch.setattr(httpx.AsyncClient, "request", request) data = {"string": "string", "int": 1, "float": 1.0, "media": input_media_photo} request_data = RequestData( parameters=[RequestParameter.from_input(key, value) for key, value in data.items()], ) # First make sure that custom timeouts are always respected await httpx_request.post( "url", request_data, write_timeout=43, ) assert self.test_flag == 43 # Now also ensure that the init value is respected await httpx_request.post("url", request_data) assert self.test_flag == 42 if init else 20 # Just for double-checking, since warnings are issued for implementations of BaseRequest # other than HTTPXRequest assert len(recwarn) == 0 async def test_socket_opts(self, monkeypatch): transport_kwargs = {} transport_init = AsyncHTTPTransport.__init__ def init_transport(*args, **kwargs): nonlocal transport_kwargs transport_kwargs = kwargs.copy() transport_init(*args, **kwargs) monkeypatch.setattr(AsyncHTTPTransport, "__init__", init_transport) HTTPXRequest() assert "socket_options" not in transport_kwargs transport_kwargs = {} HTTPXRequest(socket_options=((1, 2, 3),)) assert transport_kwargs["socket_options"] == ((1, 2, 3),) @pytest.mark.parametrize("read_timeout", [None, 1, 2, 3]) async def test_read_timeout_property(self, read_timeout): assert HTTPXRequest(read_timeout=read_timeout).read_timeout == read_timeout @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="No need to run this twice") class TestHTTPXRequestWithRequest: async def test_do_request_wait_for_pool(self, httpx_request): """The pool logic is buried rather deeply in httpxcore, so we make actual requests here instead of mocking""" task_1 = asyncio.create_task( httpx_request.do_request( method="GET", url="https://python-telegram-bot.org/static/testfiles/telegram.mp4" ) ) task_2 = asyncio.create_task( httpx_request.do_request( method="GET", url="https://python-telegram-bot.org/static/testfiles/telegram.mp4" ) ) done, pending = await asyncio.wait({task_1, task_2}, return_when=asyncio.FIRST_COMPLETED) assert len(done) == len(pending) == 1 done, pending = await asyncio.wait({task_1, task_2}, return_when=asyncio.ALL_COMPLETED) assert len(done) == 2 assert len(pending) == 0 try: # retrieve exceptions from tasks task_1.exception() task_2.exception() except (asyncio.CancelledError, asyncio.InvalidStateError): pass python-telegram-bot-21.1.1/tests/request/test_requestdata.py000066400000000000000000000202161460724040100242610ustar00rootroot00000000000000#!/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 json from typing import Any, Dict from urllib.parse import quote import pytest from telegram import InputFile, InputMediaPhoto, InputMediaVideo, MessageEntity from telegram.request import RequestData from telegram.request._requestparameter import RequestParameter from tests.auxil.files import data_file from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def inputfiles() -> Dict[bool, InputFile]: return {True: InputFile(obj="data", attach=True), False: InputFile(obj="data", attach=False)} @pytest.fixture(scope="module") def input_media_video() -> InputMediaVideo: return InputMediaVideo( media=data_file("telegram.mp4").read_bytes(), thumbnail=data_file("telegram.jpg").read_bytes(), parse_mode=None, ) @pytest.fixture(scope="module") def input_media_photo() -> InputMediaPhoto: return InputMediaPhoto( media=data_file("telegram.jpg").read_bytes(), parse_mode=None, ) @pytest.fixture(scope="module") def simple_params() -> Dict[str, Any]: return { "string": "string", "integer": 1, "tg_object": MessageEntity("type", 1, 1), "list": [1, "string", MessageEntity("type", 1, 1)], } @pytest.fixture(scope="module") def simple_jsons() -> Dict[str, Any]: return { "string": "string", "integer": json.dumps(1), "tg_object": MessageEntity("type", 1, 1).to_json(), "list": json.dumps([1, "string", MessageEntity("type", 1, 1).to_dict()]), } @pytest.fixture(scope="module") def simple_rqs(simple_params) -> RequestData: return RequestData( [RequestParameter.from_input(key, value) for key, value in simple_params.items()] ) @pytest.fixture(scope="module") def file_params(inputfiles, input_media_video, input_media_photo) -> Dict[str, Any]: return { "inputfile_attach": inputfiles[True], "inputfile_no_attach": inputfiles[False], "inputmedia": input_media_video, "inputmedia_list": [input_media_video, input_media_photo], } @pytest.fixture(scope="module") def file_jsons(inputfiles, input_media_video, input_media_photo) -> Dict[str, Any]: input_media_video_dict = input_media_video.to_dict() input_media_video_dict["media"] = input_media_video.media.attach_uri input_media_video_dict["thumbnail"] = input_media_video.thumbnail.attach_uri input_media_photo_dict = input_media_photo.to_dict() input_media_photo_dict["media"] = input_media_photo.media.attach_uri return { "inputfile_attach": inputfiles[True].attach_uri, "inputmedia": json.dumps(input_media_video_dict), "inputmedia_list": json.dumps([input_media_video_dict, input_media_photo_dict]), } @pytest.fixture(scope="module") def file_rqs(file_params) -> RequestData: return RequestData( [RequestParameter.from_input(key, value) for key, value in file_params.items()] ) @pytest.fixture(scope="module") def mixed_params(file_params, simple_params) -> Dict[str, Any]: both = file_params.copy() both.update(simple_params) return both @pytest.fixture(scope="module") def mixed_jsons(file_jsons, simple_jsons) -> Dict[str, Any]: both = file_jsons.copy() both.update(simple_jsons) return both @pytest.fixture(scope="module") def mixed_rqs(mixed_params) -> RequestData: return RequestData( [RequestParameter.from_input(key, value) for key, value in mixed_params.items()] ) class TestRequestDataWithoutRequest: def test_slot_behaviour(self, simple_rqs): for attr in simple_rqs.__slots__: assert getattr(simple_rqs, attr, "err") != "err", f"got extra slot '{attr}'" assert len(mro_slots(simple_rqs)) == len(set(mro_slots(simple_rqs))), "duplicate slot" def test_contains_files(self, simple_rqs, file_rqs, mixed_rqs): assert not simple_rqs.contains_files assert file_rqs.contains_files assert mixed_rqs.contains_files def test_parameters( self, simple_rqs, file_rqs, mixed_rqs, inputfiles, input_media_video, input_media_photo ): simple_params_expected = { "string": "string", "integer": 1, "tg_object": MessageEntity("type", 1, 1).to_dict(), "list": [1, "string", MessageEntity("type", 1, 1).to_dict()], } video_value = { "media": input_media_video.media.attach_uri, "thumbnail": input_media_video.thumbnail.attach_uri, "type": input_media_video.type, } photo_value = {"media": input_media_photo.media.attach_uri, "type": input_media_photo.type} file_params_expected = { "inputfile_attach": inputfiles[True].attach_uri, "inputmedia": video_value, "inputmedia_list": [video_value, photo_value], } mixed_params_expected = simple_params_expected.copy() mixed_params_expected.update(file_params_expected) assert simple_rqs.parameters == simple_params_expected assert file_rqs.parameters == file_params_expected assert mixed_rqs.parameters == mixed_params_expected def test_json_parameters( self, simple_rqs, file_rqs, mixed_rqs, simple_jsons, file_jsons, mixed_jsons ): assert simple_rqs.json_parameters == simple_jsons assert file_rqs.json_parameters == file_jsons assert mixed_rqs.json_parameters == mixed_jsons def test_json_payload( self, simple_rqs, file_rqs, mixed_rqs, simple_jsons, file_jsons, mixed_jsons ): assert simple_rqs.json_payload == json.dumps(simple_jsons).encode() assert file_rqs.json_payload == json.dumps(file_jsons).encode() assert mixed_rqs.json_payload == json.dumps(mixed_jsons).encode() def test_multipart_data( self, simple_rqs, file_rqs, mixed_rqs, inputfiles, input_media_video, input_media_photo, ): expected = { inputfiles[True].attach_name: inputfiles[True].field_tuple, "inputfile_no_attach": inputfiles[False].field_tuple, input_media_photo.media.attach_name: input_media_photo.media.field_tuple, input_media_video.media.attach_name: input_media_video.media.field_tuple, input_media_video.thumbnail.attach_name: input_media_video.thumbnail.field_tuple, } assert simple_rqs.multipart_data == {} assert file_rqs.multipart_data == expected assert mixed_rqs.multipart_data == expected def test_url_encoding(self): data = RequestData( [ RequestParameter.from_input("chat_id", 123), RequestParameter.from_input("text", "Hello there/!"), ] ) expected_params = "chat_id=123&text=Hello+there%2F%21" expected_url = "https://te.st/method?" + expected_params assert data.url_encoded_parameters() == expected_params assert data.parametrized_url("https://te.st/method") == expected_url expected_params = "chat_id=123&text=Hello%20there/!" expected_url = "https://te.st/method?" + expected_params assert ( data.url_encoded_parameters(encode_kwargs={"quote_via": quote, "safe": "/!"}) == expected_params ) assert ( data.parametrized_url( "https://te.st/method", encode_kwargs={"quote_via": quote, "safe": "/!"} ) == expected_url ) python-telegram-bot-21.1.1/tests/request/test_requestparameter.py000066400000000000000000000217711460724040100253370ustar00rootroot00000000000000#!/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 typing import Sequence import pytest from telegram import InputFile, InputMediaPhoto, InputMediaVideo, InputSticker, MessageEntity from telegram.constants import ChatType from telegram.request._requestparameter import RequestParameter from tests.auxil.files import data_file from tests.auxil.slots import mro_slots class TestRequestParameterWithoutRequest: def test_slot_behaviour(self): inst = RequestParameter("name", "value", [1, 2]) 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_init(self): request_parameter = RequestParameter("name", "value", [1, 2]) assert request_parameter.name == "name" assert request_parameter.value == "value" assert request_parameter.input_files == [1, 2] request_parameter = RequestParameter("name", "value", None) assert request_parameter.name == "name" assert request_parameter.value == "value" assert request_parameter.input_files is None @pytest.mark.parametrize( ("value", "expected"), [ (1, "1"), ("one", "one"), (True, "true"), (None, None), ([1, "1"], '[1, "1"]'), ({True: None}, '{"true": null}'), ((1,), "[1]"), ], ) def test_json_value(self, value, expected): request_parameter = RequestParameter("name", value, None) assert request_parameter.json_value == expected def test_multiple_multipart_data(self): assert RequestParameter("name", "value", []).multipart_data is None input_file_1 = InputFile("data1", attach=True) input_file_2 = InputFile("data2", filename="custom") request_parameter = RequestParameter( value="value", name="name", input_files=[input_file_1, input_file_2] ) files = request_parameter.multipart_data assert files[input_file_1.attach_name] == input_file_1.field_tuple assert files["name"] == input_file_2.field_tuple @pytest.mark.parametrize( ("value", "expected_value"), [ (True, True), ("str", "str"), ({1: 1.0}, {1: 1.0}), (ChatType.PRIVATE, "private"), (MessageEntity("type", 1, 1), {"type": "type", "offset": 1, "length": 1}), (datetime.datetime(2019, 11, 11, 0, 26, 16, 10**5), 1573431976), ( [ True, "str", MessageEntity("type", 1, 1), ChatType.PRIVATE, datetime.datetime(2019, 11, 11, 0, 26, 16, 10**5), ], [True, "str", {"type": "type", "offset": 1, "length": 1}, "private", 1573431976], ), ], ) def test_from_input_no_media(self, value, expected_value): request_parameter = RequestParameter.from_input("key", value) assert request_parameter.value == expected_value assert request_parameter.input_files is None def test_from_input_inputfile(self): inputfile_1 = InputFile("data1", filename="inputfile_1", attach=True) inputfile_2 = InputFile("data2", filename="inputfile_2") request_parameter = RequestParameter.from_input("key", inputfile_1) assert request_parameter.value == inputfile_1.attach_uri assert request_parameter.input_files == [inputfile_1] request_parameter = RequestParameter.from_input("key", inputfile_2) assert request_parameter.value is None assert request_parameter.input_files == [inputfile_2] request_parameter = RequestParameter.from_input("key", [inputfile_1, inputfile_2]) assert request_parameter.value == [inputfile_1.attach_uri] assert request_parameter.input_files == [inputfile_1, inputfile_2] def test_from_input_input_media(self): input_media_no_thumb = InputMediaPhoto(media=data_file("telegram.jpg").read_bytes()) input_media_thumb = InputMediaVideo( media=data_file("telegram.mp4").read_bytes(), thumbnail=data_file("telegram.jpg").read_bytes(), ) request_parameter = RequestParameter.from_input("key", input_media_no_thumb) expected_no_thumb = input_media_no_thumb.to_dict() expected_no_thumb.update({"media": input_media_no_thumb.media.attach_uri}) assert request_parameter.value == expected_no_thumb assert request_parameter.input_files == [input_media_no_thumb.media] request_parameter = RequestParameter.from_input("key", input_media_thumb) expected_thumb = input_media_thumb.to_dict() expected_thumb.update({"media": input_media_thumb.media.attach_uri}) expected_thumb.update({"thumbnail": input_media_thumb.thumbnail.attach_uri}) assert request_parameter.value == expected_thumb assert request_parameter.input_files == [ input_media_thumb.media, input_media_thumb.thumbnail, ] request_parameter = RequestParameter.from_input( "key", [input_media_thumb, input_media_no_thumb] ) assert request_parameter.value == [expected_thumb, expected_no_thumb] assert request_parameter.input_files == [ input_media_thumb.media, input_media_thumb.thumbnail, input_media_no_thumb.media, ] def test_from_input_inputmedia_without_attach(self): """This case will never happen, but we test it for completeness""" input_media = InputMediaVideo( data_file("telegram.png").read_bytes(), thumbnail=data_file("telegram.png").read_bytes(), parse_mode=None, ) input_media.media.attach_name = None input_media.thumbnail.attach_name = None request_parameter = RequestParameter.from_input("key", input_media) assert request_parameter.value == {"type": "video"} assert request_parameter.input_files == [input_media.media, input_media.thumbnail] def test_from_input_inputsticker(self): input_sticker = InputSticker(data_file("telegram.png").read_bytes(), ["emoji"], "static") expected = input_sticker.to_dict() expected.update({"sticker": input_sticker.sticker.attach_uri}) request_parameter = RequestParameter.from_input("key", input_sticker) assert request_parameter.value == expected assert request_parameter.input_files == [input_sticker.sticker] def test_from_input_str_and_bytes(self): input_str = "test_input" request_parameter = RequestParameter.from_input("input", input_str) assert request_parameter.value == input_str assert request_parameter.name == "input" assert request_parameter.input_files is None input_bytes = b"test_input" request_parameter = RequestParameter.from_input("input", input_bytes) assert request_parameter.value == input_bytes assert request_parameter.name == "input" assert request_parameter.input_files is None def test_from_input_different_sequences(self): input_list = ["item1", "item2"] request_parameter = RequestParameter.from_input("input", input_list) assert request_parameter.value == input_list assert request_parameter.name == "input" assert request_parameter.input_files is None input_tuple = tuple(input_list) request_parameter = RequestParameter.from_input("input", input_tuple) assert request_parameter.value == input_list assert request_parameter.name == "input" assert request_parameter.input_files is None class CustomSequence(Sequence): def __init__(self, items): self.items = items def __getitem__(self, index): return self.items[index] def __len__(self): return len(self.items) input_custom_sequence = CustomSequence(input_list) request_parameter = RequestParameter.from_input("input", input_custom_sequence) assert request_parameter.value == input_list assert request_parameter.name == "input" assert request_parameter.input_files is None python-telegram-bot-21.1.1/tests/test_birthdate.py000066400000000000000000000054331460724040100222210ustar00rootroot00000000000000#!/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 Birthdate from tests.auxil.slots import mro_slots class TestBirthdateBase: day = 1 month = 1 year = 2022 @pytest.fixture(scope="module") def birthdate(): return Birthdate(TestBirthdateBase.day, TestBirthdateBase.month, TestBirthdateBase.year) class TestBirthdateWithoutRequest(TestBirthdateBase): 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(), datetime) assert birthdate.to_date() == datetime(self.year, self.month, self.day) new_bd = birthdate.to_date(2023) assert new_bd == datetime(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() python-telegram-bot-21.1.1/tests/test_bot.py000066400000000000000000005142631460724040100210450ustar00rootroot00000000000000#!/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, ChatPermissions, Dice, InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, InlineQueryResultDocument, InlineQueryResultsButton, InlineQueryResultVoice, InputFile, InputMediaDocument, InputMediaPhoto, InputMessageContent, InputTextMessageContent, LabeledPrice, LinkPreviewOptions, MenuButton, MenuButtonCommands, MenuButtonDefault, MenuButtonWebApp, Message, MessageEntity, Poll, PollOption, ReactionTypeCustomEmoji, ReactionTypeEmoji, ReplyParameters, SentWebAppMessage, ShippingOption, 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 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) 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_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 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.request, "post", make_assertion) web_app_msg = await bot.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 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.request, "post", make_assertion) assert await bot.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.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}, 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), ) @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") 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, ) 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 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("business_connection_id") == 42 monkeypatch.setattr(bot.request, "post", make_assertion) assert await bot.send_message(2, "text", business_connection_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) 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 = await bot.send_message(chat_id, text="will be deleted") msg2 = await bot.send_message(chat_id, text="will be deleted") await asyncio.sleep(2) assert await bot.delete_messages(chat_id=chat_id, message_ids=(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", "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] 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] 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 reply 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 reply 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, ) assert message.caption == "new_caption" 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): chat = await bot.get_chat(super_group_id) assert chat.type == "supergroup" assert chat.title == f">>> telegram.Bot(test) @{bot.username}" assert chat.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 == "administrator" 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 reply 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, 95205500, 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") == 95205500 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, 95205500, 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 reply 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, ) # 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=(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" chat = await bot.get_chat(channel_id) assert chat.pinned_message == message assert chat.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: chat = await bot.get_chat(super_group_id) assert isinstance(chat, Chat) assert int(chat.id) == int(super_group_id) assert chat.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(PTBDeprecationWarning, 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") 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.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.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.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 python-telegram-bot-21.1.1/tests/test_botcommand.py000066400000000000000000000047661460724040100224060ustar00rootroot00000000000000#!/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) python-telegram-bot-21.1.1/tests/test_botcommandscope.py000066400000000000000000000161451460724040100234320ustar00rootroot00000000000000#!/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) python-telegram-bot-21.1.1/tests/test_botdescription.py000066400000000000000000000062641460724040100233060ustar00rootroot00000000000000#!/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(TestBotDescriptionBase.description) @pytest.fixture(scope="module") def bot_short_description(bot): return BotShortDescription(TestBotDescriptionBase.short_description) class TestBotDescriptionBase: description = "This is a test description" short_description = "This is a test short description" class TestBotDescriptionWithoutRequest(TestBotDescriptionBase): 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(TestBotDescriptionBase): 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) python-telegram-bot-21.1.1/tests/test_botname.py000066400000000000000000000034141460724040100216750ustar00rootroot00000000000000#!/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(TestBotNameBase.name) class TestBotNameBase: name = "This is a test name" class TestBotNameWithoutRequest(TestBotNameBase): 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) python-telegram-bot-21.1.1/tests/test_business.py000066400000000000000000000350571460724040100221130ustar00rootroot00000000000000#!/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 TestBusinessBase: 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( TestBusinessBase.id_, TestBusinessBase.user, TestBusinessBase.user_chat_id, TestBusinessBase.date, TestBusinessBase.can_reply, TestBusinessBase.is_enabled, ) @pytest.fixture(scope="module") def business_messages_deleted(): return BusinessMessagesDeleted( TestBusinessBase.business_connection_id, TestBusinessBase.chat, TestBusinessBase.message_ids, ) @pytest.fixture(scope="module") def business_intro(): return BusinessIntro( TestBusinessBase.title, TestBusinessBase.message, TestBusinessBase.sticker, ) @pytest.fixture(scope="module") def business_location(): return BusinessLocation( TestBusinessBase.address, TestBusinessBase.location, ) @pytest.fixture(scope="module") def business_opening_hours_interval(): return BusinessOpeningHoursInterval( TestBusinessBase.opening_minute, TestBusinessBase.closing_minute, ) @pytest.fixture(scope="module") def business_opening_hours(): return BusinessOpeningHours( TestBusinessBase.time_zone_name, TestBusinessBase.opening_hours, ) class TestBusinessConnectionWithoutRequest(TestBusinessBase): 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(TestBusinessBase): 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(TestBusinessBase): 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(TestBusinessBase): 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(TestBusinessBase): 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(TestBusinessBase): 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) python-telegram-bot-21.1.1/tests/test_callbackquery.py000066400000000000000000000535521460724040100231020ustar00rootroot00000000000000#!/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( TestCallbackQueryBase.id_, TestCallbackQueryBase.from_user, TestCallbackQueryBase.chat_instance, data=TestCallbackQueryBase.data, game_short_name=TestCallbackQueryBase.game_short_name, ) cbq.set_bot(bot) cbq._unfreeze() if request.param == "message": cbq.message = TestCallbackQueryBase.message cbq.message.set_bot(bot) elif request.param == "inline": cbq.inline_message_id = TestCallbackQueryBase.inline_message_id elif request.param == "inaccessible_message": cbq.message = InaccessibleMessage( chat=TestCallbackQueryBase.message.chat, message_id=TestCallbackQueryBase.message.message_id, ) return cbq class TestCallbackQueryBase: 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(TestCallbackQueryBase): @staticmethod def skip_params(callback_query: CallbackQuery): if callback_query.inline_message_id: return {"message_id", "chat_id"} return {"inline_message_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"], [], ) 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"], [], ) 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"], [], ) 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"], [], ) 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 ids = self.check_passed_ids(callback_query, kwargs) return ids and latitude and longitude assert check_shortcut_signature( CallbackQuery.edit_message_live_location, Bot.edit_message_live_location, ["inline_message_id", "message_id", "chat_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) assert await callback_query.edit_message_live_location(1, 2) 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"], [], ) 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"], [], ) assert await check_shortcut_call( callback_query.pin_message, callback_query.get_bot(), "pin_chat_message" ) 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"], [], ) assert await check_shortcut_call( callback_query.unpin_message, callback_query.get_bot(), "unpin_chat_message", shortcut_kwargs=["message_id", "chat_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) python-telegram-bot-21.1.1/tests/test_chat.py000066400000000000000000002147171460724040100212010ustar00rootroot00000000000000#!/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, Bot, BusinessIntro, BusinessLocation, BusinessOpeningHours, BusinessOpeningHoursInterval, Chat, ChatLocation, ChatPermissions, Location, ReactionTypeCustomEmoji, ReactionTypeEmoji, User, ) from telegram._utils.datetime import UTC, to_timestamp 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( TestChatBase.id_, title=TestChatBase.title, type=TestChatBase.type_, username=TestChatBase.username, sticker_set_name=TestChatBase.sticker_set_name, can_set_sticker_set=TestChatBase.can_set_sticker_set, permissions=TestChatBase.permissions, slow_mode_delay=TestChatBase.slow_mode_delay, bio=TestChatBase.bio, linked_chat_id=TestChatBase.linked_chat_id, location=TestChatBase.location, has_private_forwards=True, has_protected_content=True, has_visible_history=True, join_to_send_messages=True, join_by_request=True, has_restricted_voice_and_video_messages=True, is_forum=True, active_usernames=TestChatBase.active_usernames, emoji_status_custom_emoji_id=TestChatBase.emoji_status_custom_emoji_id, emoji_status_expiration_date=TestChatBase.emoji_status_expiration_date, has_aggressive_anti_spam_enabled=TestChatBase.has_aggressive_anti_spam_enabled, has_hidden_members=TestChatBase.has_hidden_members, available_reactions=TestChatBase.available_reactions, accent_color_id=TestChatBase.accent_color_id, background_custom_emoji_id=TestChatBase.background_custom_emoji_id, profile_accent_color_id=TestChatBase.profile_accent_color_id, profile_background_custom_emoji_id=TestChatBase.profile_background_custom_emoji_id, unrestrict_boost_count=TestChatBase.unrestrict_boost_count, custom_emoji_sticker_set_name=TestChatBase.custom_emoji_sticker_set_name, business_intro=TestChatBase.business_intro, business_location=TestChatBase.business_location, business_opening_hours=TestChatBase.business_opening_hours, birthdate=Birthdate(1, 1), personal_chat=TestChatBase.personal_chat, ) chat.set_bot(bot) chat._unfreeze() return chat class TestChatBase: id_ = -28767330 title = "ToledosPalaceBot - Group" type_ = "group" username = "username" all_members_are_administrators = False 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") class TestChatWithoutRequest(TestChatBase): 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, "all_members_are_administrators": self.all_members_are_administrators, "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], "accent_color_id": self.accent_color_id, "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(), } 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.sticker_set_name == self.sticker_set_name assert chat.can_set_sticker_set == self.can_set_sticker_set assert chat.permissions == self.permissions assert chat.slow_mode_delay == self.slow_mode_delay assert chat.bio == self.bio assert chat.business_intro == self.business_intro assert chat.business_location == self.business_location assert chat.business_opening_hours == self.business_opening_hours assert chat.has_protected_content == self.has_protected_content assert chat.has_visible_history == self.has_visible_history assert chat.has_private_forwards == self.has_private_forwards assert chat.linked_chat_id == self.linked_chat_id assert chat.location.location == self.location.location assert chat.location.address == self.location.address assert chat.join_to_send_messages == self.join_to_send_messages assert chat.join_by_request == self.join_by_request assert ( chat.has_restricted_voice_and_video_messages == self.has_restricted_voice_and_video_messages ) assert chat.api_kwargs == { "all_members_are_administrators": self.all_members_are_administrators } assert chat.is_forum == self.is_forum assert chat.active_usernames == tuple(self.active_usernames) assert chat.emoji_status_custom_emoji_id == self.emoji_status_custom_emoji_id assert chat.emoji_status_expiration_date == (self.emoji_status_expiration_date) assert chat.has_aggressive_anti_spam_enabled == self.has_aggressive_anti_spam_enabled assert chat.has_hidden_members == self.has_hidden_members assert chat.available_reactions == tuple(self.available_reactions) assert chat.accent_color_id == self.accent_color_id assert chat.background_custom_emoji_id == self.background_custom_emoji_id assert chat.profile_accent_color_id == self.profile_accent_color_id assert chat.profile_background_custom_emoji_id == self.profile_background_custom_emoji_id assert chat.unrestrict_boost_count == self.unrestrict_boost_count assert chat.custom_emoji_sticker_set_name == self.custom_emoji_sticker_set_name assert chat.birthdate == self.birthdate assert chat.personal_chat == self.personal_chat def test_de_json_localization(self, bot, raw_bot, tz_bot): json_dict = { "id": self.id_, "type": self.type_, "emoji_status_expiration_date": to_timestamp(self.emoji_status_expiration_date), } chat_bot = Chat.de_json(json_dict, bot) chat_bot_raw = Chat.de_json(json_dict, raw_bot) chat_bot_tz = Chat.de_json(json_dict, tz_bot) # comparing utcoffsets because comparing tzinfo objects is not reliable emoji_expire_offset = chat_bot_tz.emoji_status_expiration_date.utcoffset() emoji_expire_offset_tz = tz_bot.defaults.tzinfo.utcoffset( chat_bot_tz.emoji_status_expiration_date.replace(tzinfo=None) ) assert chat_bot.emoji_status_expiration_date.tzinfo == UTC assert chat_bot_raw.emoji_status_expiration_date.tzinfo == UTC assert emoji_expire_offset_tz == emoji_expire_offset 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["permissions"] == chat.permissions.to_dict() assert chat_dict["slow_mode_delay"] == chat.slow_mode_delay assert chat_dict["bio"] == chat.bio assert chat_dict["business_intro"] == chat.business_intro.to_dict() assert chat_dict["business_location"] == chat.business_location.to_dict() assert chat_dict["business_opening_hours"] == chat.business_opening_hours.to_dict() assert chat_dict["has_private_forwards"] == chat.has_private_forwards assert chat_dict["has_protected_content"] == chat.has_protected_content assert chat_dict["has_visible_history"] == chat.has_visible_history assert chat_dict["linked_chat_id"] == chat.linked_chat_id assert chat_dict["location"] == chat.location.to_dict() assert chat_dict["join_to_send_messages"] == chat.join_to_send_messages assert chat_dict["join_by_request"] == chat.join_by_request assert ( chat_dict["has_restricted_voice_and_video_messages"] == chat.has_restricted_voice_and_video_messages ) assert chat_dict["is_forum"] == chat.is_forum assert chat_dict["active_usernames"] == list(chat.active_usernames) assert chat_dict["emoji_status_custom_emoji_id"] == chat.emoji_status_custom_emoji_id assert chat_dict["emoji_status_expiration_date"] == to_timestamp( chat.emoji_status_expiration_date ) assert ( chat_dict["has_aggressive_anti_spam_enabled"] == chat.has_aggressive_anti_spam_enabled ) assert chat_dict["has_hidden_members"] == chat.has_hidden_members assert chat_dict["available_reactions"] == [ reaction.to_dict() for reaction in chat.available_reactions ] assert chat_dict["accent_color_id"] == chat.accent_color_id assert chat_dict["background_custom_emoji_id"] == chat.background_custom_emoji_id assert chat_dict["profile_accent_color_id"] == chat.profile_accent_color_id assert ( chat_dict["profile_background_custom_emoji_id"] == chat.profile_background_custom_emoji_id ) assert chat_dict["custom_emoji_sticker_set_name"] == chat.custom_emoji_sticker_set_name assert chat_dict["unrestrict_boost_count"] == chat.unrestrict_boost_count assert chat_dict["birthdate"] == chat.birthdate.to_dict() assert chat_dict["personal_chat"] == chat.personal_chat.to_dict() def test_always_tuples_attributes(self): chat = Chat( id=123, title="title", type=Chat.PRIVATE, ) assert isinstance(chat.active_usernames, tuple) assert chat.active_usernames == () 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"] == self.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=self.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.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.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_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 ) 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() python-telegram-bot-21.1.1/tests/test_chatadministratorrights.py000066400000000000000000000143531460724040100252150ustar00rootroot00000000000000#!/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 python-telegram-bot-21.1.1/tests/test_chatboost.py000066400000000000000000000476621460724040100222530ustar00rootroot00000000000000# 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) python-telegram-bot-21.1.1/tests/test_chatinvitelink.py000066400000000000000000000162431460724040100232700ustar00rootroot00000000000000#!/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( TestChatInviteLinkBase.link, creator, TestChatInviteLinkBase.creates_join_request, TestChatInviteLinkBase.primary, TestChatInviteLinkBase.revoked, expire_date=TestChatInviteLinkBase.expire_date, member_limit=TestChatInviteLinkBase.member_limit, name=TestChatInviteLinkBase.name, pending_join_request_count=TestChatInviteLinkBase.pending_join_request_count, ) class TestChatInviteLinkBase: 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 class TestChatInviteLinkWithoutRequest(TestChatInviteLinkBase): 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), } 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 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 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) python-telegram-bot-21.1.1/tests/test_chatjoinrequest.py000066400000000000000000000172751460724040100234720ustar00rootroot00000000000000#!/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=TestChatJoinRequestBase.chat, from_user=TestChatJoinRequestBase.from_user, date=time, bio=TestChatJoinRequestBase.bio, invite_link=TestChatJoinRequestBase.invite_link, user_chat_id=TestChatJoinRequestBase.from_user.id, ) cjr.set_bot(bot) return cjr class TestChatJoinRequestBase: 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(TestChatJoinRequestBase): 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() python-telegram-bot-21.1.1/tests/test_chatlocation.py000066400000000000000000000051671460724040100227270ustar00rootroot00000000000000#!/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(TestChatLocationBase.location, TestChatLocationBase.address) class TestChatLocationBase: location = Location(123, 456) address = "The Shire" class TestChatLocationWithoutRequest(TestChatLocationBase): 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) python-telegram-bot-21.1.1/tests/test_chatmember.py000066400000000000000000000255711460724040100223670ustar00rootroot00000000000000#!/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) 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)): 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) python-telegram-bot-21.1.1/tests/test_chatmemberupdated.py000066400000000000000000000273641460724040100237400ustar00rootroot00000000000000#!/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, TestChatMemberUpdatedBase.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=TestChatMemberUpdatedBase.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) class TestChatMemberUpdatedBase: old_status = ChatMember.MEMBER new_status = ChatMember.ADMINISTRATOR class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase): 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, } 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 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 ) 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 == {} python-telegram-bot-21.1.1/tests/test_chatpermissions.py000066400000000000000000000204171460724040100234650ustar00rootroot00000000000000#!/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 TestChatPermissionsBase: 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(TestChatPermissionsBase): 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 python-telegram-bot-21.1.1/tests/test_choseninlineresult.py000066400000000000000000000072331460724040100241700ustar00rootroot00000000000000#!/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( TestChosenInlineResultBase.result_id, user, TestChosenInlineResultBase.query ) class TestChosenInlineResultBase: result_id = "result id" query = "query text" class TestChosenInlineResultWithoutRequest(TestChosenInlineResultBase): 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) python-telegram-bot-21.1.1/tests/test_constants.py000066400000000000000000000215731460724040100222720ustar00rootroot00000000000000#!/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", # attribute is deprecated, no need to add it to MessageType "user_shared", "via_bot", "is_from_offline", } @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" 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 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 assert good_msg.caption == good_caption assert isinstance(bad_msg, BadRequest) assert "Message caption is too long" in str(bad_msg) python-telegram-bot-21.1.1/tests/test_dice.py000066400000000000000000000044361460724040100211610ustar00rootroot00000000000000#!/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 TestDiceBase: value = 4 class TestDiceWithoutRequest(TestDiceBase): 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) python-telegram-bot-21.1.1/tests/test_enum_types.py000066400000000000000000000043541460724040100224440ustar00rootroot00000000000000#!/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 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"))} 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="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." ) python-telegram-bot-21.1.1/tests/test_error.py000066400000000000000000000167131460724040100214070ustar00rootroot00000000000000#!/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" python-telegram-bot-21.1.1/tests/test_forcereply.py000066400000000000000000000053351460724040100224260ustar00rootroot00000000000000#!/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(TestForceReplyBase.selective, TestForceReplyBase.input_field_placeholder) class TestForceReplyBase: force_reply = True selective = True input_field_placeholder = "force replies can be annoying if not used properly" class TestForceReplyWithoutRequest(TestForceReplyBase): 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(TestForceReplyBase): 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" python-telegram-bot-21.1.1/tests/test_forum.py000066400000000000000000000450401460724040100214010ustar00rootroot00000000000000#!/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 == {} python-telegram-bot-21.1.1/tests/test_giveaway.py000066400000000000000000000415661460724040100220760ustar00rootroot00000000000000#!/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) python-telegram-bot-21.1.1/tests/test_helpers.py000066400000000000000000000146711460724040100217210ustar00rootroot00000000000000#!/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) python-telegram-bot-21.1.1/tests/test_inlinequeryresultsbutton.py000066400000000000000000000065011460724040100254720ustar00rootroot00000000000000#!/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=TestInlineQueryResultsButtonBase.text, start_parameter=TestInlineQueryResultsButtonBase.start_parameter, web_app=TestInlineQueryResultsButtonBase.web_app, ) class TestInlineQueryResultsButtonBase: text = "text" start_parameter = "start_parameter" web_app = WebAppInfo(url="https://python-telegram-bot.org") class TestInlineQueryResultsButtonWithoutRequest(TestInlineQueryResultsButtonBase): 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) python-telegram-bot-21.1.1/tests/test_keyboardbutton.py000066400000000000000000000134331460724040100233060ustar00rootroot00000000000000#!/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( TestKeyboardButtonBase.text, request_location=TestKeyboardButtonBase.request_location, request_contact=TestKeyboardButtonBase.request_contact, request_poll=TestKeyboardButtonBase.request_poll, web_app=TestKeyboardButtonBase.web_app, request_chat=TestKeyboardButtonBase.request_chat, request_users=TestKeyboardButtonBase.request_users, ) class TestKeyboardButtonBase: 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(TestKeyboardButtonBase): 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) python-telegram-bot-21.1.1/tests/test_keyboardbuttonpolltype.py000066400000000000000000000046531460724040100251030ustar00rootroot00000000000000#!/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(TestKeyboardButtonPollTypeBase.type) class TestKeyboardButtonPollTypeBase: type = Poll.QUIZ class TestKeyboardButtonPollTypeWithoutRequest(TestKeyboardButtonPollTypeBase): 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) python-telegram-bot-21.1.1/tests/test_keyboardbuttonrequest.py000066400000000000000000000161231460724040100247160ustar00rootroot00000000000000#!/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( TestKeyboardButtonRequestUsersBase.request_id, TestKeyboardButtonRequestUsersBase.user_is_bot, TestKeyboardButtonRequestUsersBase.user_is_premium, TestKeyboardButtonRequestUsersBase.max_quantity, ) class TestKeyboardButtonRequestUsersBase: request_id = 123 user_is_bot = True user_is_premium = False max_quantity = 10 class TestKeyboardButtonRequestUsersWithoutRequest(TestKeyboardButtonRequestUsersBase): 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( TestKeyboardButtonRequestChatBase.request_id, TestKeyboardButtonRequestChatBase.chat_is_channel, TestKeyboardButtonRequestChatBase.chat_is_forum, TestKeyboardButtonRequestChatBase.chat_has_username, TestKeyboardButtonRequestChatBase.chat_is_created, TestKeyboardButtonRequestChatBase.user_administrator_rights, TestKeyboardButtonRequestChatBase.bot_administrator_rights, TestKeyboardButtonRequestChatBase.bot_is_member, ) class TestKeyboardButtonRequestChatBase: 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(TestKeyboardButtonRequestChatBase): 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) python-telegram-bot-21.1.1/tests/test_linkpreviewoptions.py000066400000000000000000000077701460724040100242340ustar00rootroot00000000000000#!/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=TestLinkPreviewOptionsBase.is_disabled, url=TestLinkPreviewOptionsBase.url, prefer_small_media=TestLinkPreviewOptionsBase.prefer_small_media, prefer_large_media=TestLinkPreviewOptionsBase.prefer_large_media, show_above_text=TestLinkPreviewOptionsBase.show_above_text, ) class TestLinkPreviewOptionsBase: is_disabled = True url = "https://www.example.com" prefer_small_media = True prefer_large_media = False show_above_text = True class TestLinkPreviewOptionsWithoutRequest(TestLinkPreviewOptionsBase): 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) python-telegram-bot-21.1.1/tests/test_loginurl.py000066400000000000000000000050571460724040100221100ustar00rootroot00000000000000#!/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=TestLoginUrlBase.url, forward_text=TestLoginUrlBase.forward_text, bot_username=TestLoginUrlBase.bot_username, request_write_access=TestLoginUrlBase.request_write_access, ) class TestLoginUrlBase: url = "http://www.google.com" forward_text = "Send me forward!" bot_username = "botname" request_write_access = True class TestLoginUrlWithoutRequest(TestLoginUrlBase): 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) python-telegram-bot-21.1.1/tests/test_maybeinaccessiblemessage.py000066400000000000000000000121471460724040100252620ustar00rootroot00000000000000#!/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( TestMaybeInaccessibleMessageBase.chat, TestMaybeInaccessibleMessageBase.message_id, TestMaybeInaccessibleMessageBase.date, ) class TestMaybeInaccessibleMessageBase: chat = Chat(1, "title") message_id = 123 date = dtm.datetime.now(dtm.timezone.utc).replace(microsecond=0) class TestMaybeInaccessibleMessageWithoutRequest(TestMaybeInaccessibleMessageBase): 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 python-telegram-bot-21.1.1/tests/test_menubutton.py000066400000000000000000000136501460724040100224530ustar00rootroot00000000000000#!/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": TestMenuButtonselfBase.text, "web_app": TestMenuButtonselfBase.web_app.to_dict(), }, bot=None, ) class TestMenuButtonselfBase: 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(TestMenuButtonselfBase): 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_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) python-telegram-bot-21.1.1/tests/test_message.py000066400000000000000000003242021460724040100216750ustar00rootroot00000000000000#!/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, Bot, Chat, ChatBoostAdded, ChatShared, Contact, Dice, Document, ExternalReplyInfo, Game, Giveaway, GiveawayCompleted, GiveawayCreated, GiveawayWinners, Invoice, LinkPreviewOptions, Location, Message, MessageAutoDeleteTimerChanged, MessageEntity, MessageOriginChat, PassportData, PhotoSize, Poll, PollOption, ProximityAlertTriggered, 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(scope="module") def message(bot): message = PytestMessage( message_id=TestMessageBase.id_, date=TestMessageBase.date, chat=copy(TestMessageBase.chat), from_user=copy(TestMessageBase.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"}, ], 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", ], ) def message_params(bot, request): message = Message( message_id=TestMessageBase.id_, from_user=TestMessageBase.from_user, date=TestMessageBase.date, chat=TestMessageBase.chat, **request.param, ) message.set_bot(bot) return message class TestMessageBase: 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." ) 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(TestMessageBase): @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=TestMessageBase.id_, date=TestMessageBase.date, chat=copy(TestMessageBase.chat), from_user=copy(TestMessageBase.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.
" ) 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.
" ) 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*\." ) 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*\." ) 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.
" ) 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.
" ) 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*\." ) 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*\." ) 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}" def test_link_with_topics(self, message): message.chat.username = None message.chat.id = -1003 message.is_topic_message = True message.message_thread_id = 123 assert message.link == f"https://t.me/c/3/{message.message_id}?thread=123" def test_link_with_reply(self, message): message.chat.username = None message.chat.id = -1003 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", "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*\." ) 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.
" ) 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"], [], ) 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"], ) 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"], [], ) 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"], ) 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"], [], ) 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"], ) 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"], [], ) 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"], ) 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 return chat_id and message_id and longitude and latitude assert check_shortcut_signature( Message.edit_live_location, Bot.edit_message_live_location, ["chat_id", "message_id", "inline_message_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"], ) 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) 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"], [], ) 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"], ) 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"], [] ) assert await check_shortcut_call(message.stop_poll, message.get_bot(), "stop_poll") 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"], [] ) assert await check_shortcut_call(message.pin, message.get_bot(), "pin_chat_message") 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"], [] ) assert await check_shortcut_call( message.unpin, message.get_bot(), "unpin_chat_message", shortcut_kwargs=["chat_id", "message_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 ): message.set_bot(PytestExtBot(token=bot.token, defaults=Defaults(do_quote=default_quote))) 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.get_bot()._defaults = None 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() python-telegram-bot-21.1.1/tests/test_messageautodeletetimerchanged.py000066400000000000000000000043441460724040100263260ustar00rootroot00000000000000#!/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) python-telegram-bot-21.1.1/tests/test_messageentity.py000066400000000000000000000066571460724040100231450ustar00rootroot00000000000000#!/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 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 TestMessageEntityBase: type_ = "url" offset = 1 length = 2 url = "url" class TestMessageEntityWithoutRequest(TestMessageEntityBase): 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_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) python-telegram-bot-21.1.1/tests/test_messageid.py000066400000000000000000000041361460724040100222130ustar00rootroot00000000000000#!/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) python-telegram-bot-21.1.1/tests/test_messageorigin.py000066400000000000000000000203471460724040100231100ustar00rootroot00000000000000#!/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) python-telegram-bot-21.1.1/tests/test_meta.py000066400000000000000000000025661460724040100212050ustar00rootroot00000000000000#!/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 setup.py bdist_dumb") == 0 # pragma: no cover @skip_disabled def test_build_raw(): assert os.system("python setup_raw.py bdist_dumb") == 0 # pragma: no cover python-telegram-bot-21.1.1/tests/test_modules.py000066400000000000000000000045671460724040100217320ustar00rootroot00000000000000#!/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")] python-telegram-bot-21.1.1/tests/test_official/000077500000000000000000000000001460724040100214505ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/test_official/__init__.py000066400000000000000000000000001460724040100235470ustar00rootroot00000000000000python-telegram-bot-21.1.1/tests/test_official/arg_type_checker.py000066400000000000000000000222721460724040100253250ustar00rootroot00000000000000#!/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 functions which confirm that the parameters of our methods and classes match the official API. It also checks if the type annotations are correct and if the parameters are required or not.""" import inspect import logging import re from datetime import datetime from types import FunctionType from typing import Any, Sequence from telegram._utils.defaultvalue import DefaultValue from telegram._utils.types import FileInput, ODVInput from telegram.ext import Defaults from tests.test_official.exceptions import ParamTypeCheckingExceptions as PTCE from tests.test_official.exceptions import ignored_param_requirements from tests.test_official.helpers import ( _extract_words, _get_params_base, _unionizer, cached_type_hints, resolve_forward_refs_in_type, wrap_with_none, ) from tests.test_official.scraper import TelegramParameter ARRAY_OF_PATTERN = r"Array of(?: Array of)? ([\w\,\s]*)" # In order to evaluate the type annotation, we need to first have a mapping of the types # specified in the official API to our types. The keys are types in the column of official API. TYPE_MAPPING: dict[str, set[Any]] = { "Integer or String": {int | str}, "Integer": {int}, "String": {str}, r"Boolean|True": {bool}, r"Float(?: number)?": {float}, # Distinguishing 1D and 2D Sequences and finding the inner type is done later. ARRAY_OF_PATTERN: {Sequence}, r"InputFile(?: or String)?": {resolve_forward_refs_in_type(FileInput)}, } ALL_DEFAULTS = inspect.getmembers(Defaults, lambda x: isinstance(x, property)) DATETIME_REGEX = re.compile( r"""([_]+|\b) # check for word boundary or underscore date # check for "date" [^\w]*\b # optionally check for a word after 'date' """, re.VERBOSE, ) log = logging.debug def check_required_param( tg_param: TelegramParameter, param: inspect.Parameter, method_or_obj_name: str ) -> bool: """Checks if the method/class parameter is a required/optional param as per Telegram docs. Returns: :obj:`bool`: The boolean returned represents whether our parameter's requirement (optional or required) is the same as Telegram's or not. """ is_ours_required = param.default is inspect.Parameter.empty # Handle cases where we provide convenience intentionally- if param.name in ignored_param_requirements(method_or_obj_name): return True return tg_param.param_required is is_ours_required def check_defaults_type(ptb_param: inspect.Parameter) -> bool: return DefaultValue.get_value(ptb_param.default) is None def check_param_type( ptb_param: inspect.Parameter, tg_parameter: TelegramParameter, obj: FunctionType | type, ) -> tuple[bool, type]: """This function checks whether the type annotation of the parameter is the same as the one specified in the official API. It also checks for some special cases where we accept more types Args: ptb_param: The parameter object from our methods/classes tg_parameter: The table row corresponding to the parameter from official API. obj: The object (method/class) that we are checking. Returns: :obj:`tuple`: A tuple containing: * :obj:`bool`: The boolean returned represents whether our parameter's type annotation is the same as Telegram's or not. * :obj:`type`: The expected type annotation of the parameter. """ # PRE-PROCESSING: tg_param_type: str = tg_parameter.param_type is_class = inspect.isclass(obj) ptb_annotation = cached_type_hints(obj, is_class).get(ptb_param.name) # Let's check for a match: # In order to evaluate the type annotation, we need to first have a mapping of the types # (see TYPE_MAPPING comment defined at the top level of this module) mapped: set[type] = _get_params_base(tg_param_type, TYPE_MAPPING) # We should have a maximum of one match. assert len(mapped) <= 1, f"More than one match found for {tg_param_type}" # it may be a list of objects, so let's extract them using _extract_words: mapped_type = _unionizer(_extract_words(tg_param_type)) if not mapped else mapped.pop() # If the parameter is not required by TG, `None` should be added to `mapped_type` mapped_type = wrap_with_none(tg_parameter, mapped_type, obj) log( "At the end of PRE-PROCESSING, the values of variables are:\n" "Parameter name: %s\n" "ptb_annotation= %s\n" "mapped_type= %s\n" "tg_param_type= %s\n" "tg_parameter.param_required= %s\n", ptb_param.name, ptb_annotation, mapped_type, tg_param_type, tg_parameter.param_required, ) # CHECKING: # Each branch manipulates the `mapped_type` (except for 4) ) to match the `ptb_annotation`. # 1) HANDLING ARRAY TYPES: # Now let's do the checking, starting with "Array of ..." types. if "Array of " in tg_param_type: # For exceptions just check if they contain the annotation if ptb_param.name in PTCE.ARRAY_OF_EXCEPTIONS: return PTCE.ARRAY_OF_EXCEPTIONS[ptb_param.name] in str(ptb_annotation), Sequence obj_match: re.Match | None = re.search(ARRAY_OF_PATTERN, tg_param_type) if obj_match is None: raise AssertionError(f"Array of {tg_param_type} not found in {ptb_param.name}") obj_str: str = obj_match.group(1) # is obj a regular type like str? array_map: set[type] = _get_params_base(obj_str, TYPE_MAPPING) mapped_type = _unionizer(_extract_words(obj_str)) if not array_map else array_map.pop() if "Array of Array of" in tg_param_type: log("Array of Array of type found in `%s`\n", tg_param_type) mapped_type = Sequence[Sequence[mapped_type]] else: log("Array of type found in `%s`\n", tg_param_type) mapped_type = Sequence[mapped_type] # 2) HANDLING OTHER TYPES: # Special case for send_* methods where we accept more types than the official API: elif ptb_param.name in PTCE.ADDITIONAL_TYPES and obj.__name__.startswith("send"): log("Checking that `%s` has an additional argument!\n", ptb_param.name) mapped_type = mapped_type | PTCE.ADDITIONAL_TYPES[ptb_param.name] # 3) HANDLING DATETIMES: elif ( re.search( DATETIME_REGEX, ptb_param.name, ) or "Unix time" in tg_parameter.param_description ): log("Checking that `%s` is a datetime!\n", ptb_param.name) if ptb_param.name in PTCE.DATETIME_EXCEPTIONS: return True, mapped_type # If it's a class, we only accept datetime as the parameter mapped_type = datetime if is_class else mapped_type | datetime # 4) COMPLEX TYPES: # Some types are too complicated, so we replace our annotation with a simpler type: elif any(ptb_param.name in key for key in PTCE.COMPLEX_TYPES): log("Converting `%s` to a simpler type!\n", ptb_param.name) for (param_name, is_expected_class), exception_type in PTCE.COMPLEX_TYPES.items(): if ptb_param.name == param_name and is_class is is_expected_class: ptb_annotation = wrap_with_none(tg_parameter, exception_type, obj) # 5) HANDLING DEFAULTS PARAMETERS: # Classes whose parameters are all ODVInput should be converted and checked. elif obj.__name__ in PTCE.IGNORED_DEFAULTS_CLASSES: log("Checking that `%s`'s param is ODVInput:\n", obj.__name__) mapped_type = ODVInput[mapped_type] elif not ( # Defaults checking should not be done for: # 1. Parameters that have name conflict with `Defaults.name` is_class and obj.__name__ in ("ReplyParameters", "Message", "ExternalReplyInfo") and ptb_param.name in PTCE.IGNORED_DEFAULTS_PARAM_NAMES ): # Now let's check if the parameter is a Defaults parameter, it should be for name, _ in ALL_DEFAULTS: if name == ptb_param.name or "parse_mode" in ptb_param.name: log("Checking that `%s` is a Defaults parameter!\n", ptb_param.name) mapped_type = ODVInput[mapped_type] break # RESULTS:- mapped_type = wrap_with_none(tg_parameter, mapped_type, obj) mapped_type = resolve_forward_refs_in_type(mapped_type) log( "At RESULTS, we are comparing:\nptb_annotation= %s\nmapped_type= %s\n", ptb_annotation, mapped_type, ) return mapped_type == ptb_annotation, mapped_type python-telegram-bot-21.1.1/tests/test_official/exceptions.py000066400000000000000000000156501460724040100242120ustar00rootroot00000000000000#!/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 exceptions to our API compared to the official API.""" from telegram import Animation, Audio, Document, PhotoSize, Sticker, Video, VideoNote, Voice from tests.test_official.helpers import _get_params_base IGNORED_OBJECTS = ("ResponseParameters",) GLOBALLY_IGNORED_PARAMETERS = { "self", "read_timeout", "write_timeout", "connect_timeout", "pool_timeout", "bot", "api_kwargs", } class ParamTypeCheckingExceptions: # Types for certain parameters accepted by PTB but not in the official API ADDITIONAL_TYPES = { "photo": PhotoSize, "video": Video, "video_note": VideoNote, "audio": Audio, "document": Document, "animation": Animation, "voice": Voice, "sticker": Sticker, } # Exceptions to the "Array of" types, where we accept more types than the official API # key: parameter name, value: type which must be present in the annotation ARRAY_OF_EXCEPTIONS = { "results": "InlineQueryResult", # + Callable "commands": "BotCommand", # + tuple[str, str] "keyboard": "KeyboardButton", # + sequence[sequence[str]] "reaction": "ReactionType", # + str # TODO: Deprecated and will be corrected (and removed) in next major PTB version: "file_hashes": "List[str]", } # Special cases for other parameters that accept more types than the official API, and are # too complex to compare/predict with official API: COMPLEX_TYPES = ( { # (param_name, is_class (i.e appears in a class?)): reduced form of annotation ("correct_option_id", False): int, # actual: Literal ("file_id", False): str, # actual: Union[str, objs_with_file_id_attr] ("invite_link", False): str, # actual: Union[str, ChatInviteLink] ("provider_data", False): str, # actual: Union[str, obj] ("callback_data", True): str, # actual: Union[str, obj] ("media", True): str, # actual: Union[str, InputMedia*, FileInput] ( "data", True, ): str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress] } ) # param names ignored in the param type checking in classes for the `tg.Defaults` case. IGNORED_DEFAULTS_PARAM_NAMES = { "quote", "link_preview_options", } # These classes' params are all ODVInput, so we ignore them in the defaults type checking. IGNORED_DEFAULTS_CLASSES = {"LinkPreviewOptions"} # TODO: Remove this in v22 when it becomes a datetime (also remove from arg_type_checker.py) DATETIME_EXCEPTIONS = { "file_date", } # Arguments *added* to the official API PTB_EXTRA_PARAMS = { "send_contact": {"contact"}, "send_location": {"location"}, "(send_message|edit_message_text)": { # convenience parameters "disable_web_page_preview", }, r"(send|copy)_\w+": { # convenience parameters "reply_to_message_id", "allow_sending_without_reply", }, "edit_message_live_location": {"location"}, "send_venue": {"venue"}, "answer_inline_query": {"current_offset"}, "send_media_group": {"caption", "parse_mode", "caption_entities"}, "send_(animation|audio|document|photo|video(_note)?|voice)": {"filename"}, "InlineQueryResult": {"id", "type"}, # attributes common to all subclasses "ChatMember": {"user", "status"}, # attributes common to all subclasses "BotCommandScope": {"type"}, # attributes common to all subclasses "MenuButton": {"type"}, # attributes common to all subclasses "PassportFile": {"credentials"}, "EncryptedPassportElement": {"credentials"}, "PassportElementError": {"source", "type", "message"}, "InputMedia": {"caption", "caption_entities", "media", "media_type", "parse_mode"}, "InputMedia(Animation|Audio|Document|Photo|Video|VideoNote|Voice)": {"filename"}, "InputFile": {"attach", "filename", "obj"}, "MaybeInaccessibleMessage": {"date", "message_id", "chat"}, # attributes common to all subcls "ChatBoostSource": {"source"}, # attributes common to all subclasses "MessageOrigin": {"type", "date"}, # attributes common to all subclasses "ReactionType": {"type"}, # attributes common to all subclasses "InputTextMessageContent": {"disable_web_page_preview"}, # convenience arg, here for bw compat } def ptb_extra_params(object_name: str) -> set[str]: return _get_params_base(object_name, PTB_EXTRA_PARAMS) # Arguments *removed* from the official API # Mostly due to the value being fixed anyway PTB_IGNORED_PARAMS = { r"InlineQueryResult\w+": {"type"}, r"ChatMember\w+": {"status"}, r"PassportElementError\w+": {"source"}, "ForceReply": {"force_reply"}, "ReplyKeyboardRemove": {"remove_keyboard"}, r"BotCommandScope\w+": {"type"}, r"MenuButton\w+": {"type"}, r"InputMedia\w+": {"type"}, "InaccessibleMessage": {"date"}, r"MessageOrigin\w+": {"type"}, r"ChatBoostSource\w+": {"source"}, r"ReactionType\w+": {"type"}, } def ptb_ignored_params(object_name: str) -> set[str]: return _get_params_base(object_name, PTB_IGNORED_PARAMS) IGNORED_PARAM_REQUIREMENTS = { # Ignore these since there's convenience params in them (eg. Venue) # <---- "send_location": {"latitude", "longitude"}, "edit_message_live_location": {"latitude", "longitude"}, "send_venue": {"latitude", "longitude", "title", "address"}, "send_contact": {"phone_number", "first_name"}, # ----> } def ignored_param_requirements(object_name: str) -> set[str]: return _get_params_base(object_name, IGNORED_PARAM_REQUIREMENTS) # Arguments that are optional arguments for now for backwards compatibility BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = { "create_new_sticker_set": {"sticker_format"}, # removed by bot api 7.2 "StickerSet": {"is_animated", "is_video"}, # removed by bot api 7.2 "UsersShared": {"user_ids", "users"}, # removed/added by bot api 7.2 } def backwards_compat_kwargs(object_name: str) -> set[str]: return _get_params_base(object_name, BACKWARDS_COMPAT_KWARGS) IGNORED_PARAM_REQUIREMENTS.update(BACKWARDS_COMPAT_KWARGS) python-telegram-bot-21.1.1/tests/test_official/helpers.py000066400000000000000000000103611460724040100234650ustar00rootroot00000000000000#!/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 helper functions for the official API tests used in the other modules.""" import functools import re from typing import TYPE_CHECKING, Any, Sequence, _eval_type, get_type_hints from bs4 import PageElement, Tag import telegram import telegram._utils.defaultvalue import telegram._utils.types if TYPE_CHECKING: from tests.test_official.scraper import TelegramParameter tg_objects = vars(telegram) tg_objects.update(vars(telegram._utils.types)) tg_objects.update(vars(telegram._utils.defaultvalue)) def _get_params_base(object_name: str, search_dict: dict[str, set[Any]]) -> set[Any]: """Helper function for the *_params functions below. Given an object name and a search dict, goes through the keys of the search dict and checks if the object name matches any of the regexes (keys). The union of all the sets (values) of the matching regexes is returned. `object_name` may be a CamelCase or snake_case name. """ out = set() for regex, params in search_dict.items(): if re.fullmatch(regex, object_name): out.update(params) # also check the snake_case version snake_case_name = re.sub(r"(? set[str]: """Extracts all words from a string, removing all punctuation and words like 'and' & 'or'.""" return set(re.sub(r"[^\w\s]", "", text).split()) - {"and", "or"} def _unionizer(annotation: Sequence[Any] | set[Any]) -> Any: """Returns a union of all the types in the annotation. Also imports objects from lib.""" union = None for t in annotation: if isinstance(t, str): # we have to import objects from lib t = getattr(telegram, t) # noqa: PLW2901 union = t if union is None else union | t return union def find_next_sibling_until(tag: Tag, name: str, until: Tag) -> PageElement | None: for sibling in tag.next_siblings: if sibling is until: return None if sibling.name == name: return sibling return None def is_pascal_case(s): "PascalCase. Starts with a capital letter and has no spaces. Useful for identifying classes." return bool(re.match(r"^[A-Z][a-zA-Z\d]*$", s)) def is_parameter_required_by_tg(field: str) -> bool: if field in {"Required", "Yes"}: return True return field.split(".", 1)[0] != "Optional" # splits the sentence and extracts first word def wrap_with_none(tg_parameter: "TelegramParameter", mapped_type: Any, obj: object) -> type: """Adds `None` to type annotation if the parameter isn't required. Respects ignored params.""" # have to import here to avoid circular imports from tests.test_official.exceptions import ignored_param_requirements if tg_parameter.param_name in ignored_param_requirements(obj.__name__): return mapped_type | type(None) return mapped_type | type(None) if not tg_parameter.param_required else mapped_type @functools.cache def cached_type_hints(obj: Any, is_class: bool) -> dict[str, Any]: """Returns type hints of a class, method, or function, with forward refs evaluated.""" return get_type_hints(obj.__init__ if is_class else obj, localns=tg_objects) @functools.cache def resolve_forward_refs_in_type(obj: type) -> type: """Resolves forward references in a type hint.""" return _eval_type(obj, localns=tg_objects, globalns=None) python-telegram-bot-21.1.1/tests/test_official/scraper.py000066400000000000000000000112771460724040100234710ustar00rootroot00000000000000#!/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 functions which are used to scrape the official Bot API documentation.""" import asyncio from dataclasses import dataclass from typing import Literal, overload import httpx from bs4 import BeautifulSoup, Tag from tests.test_official.exceptions import IGNORED_OBJECTS from tests.test_official.helpers import ( find_next_sibling_until, is_parameter_required_by_tg, is_pascal_case, ) @dataclass(slots=True, frozen=True) class TelegramParameter: """Represents the scraped Telegram parameter. Contains all relevant attributes needed for comparison. Relevant for both TelegramMethod and TelegramClass.""" param_name: str param_type: str param_required: bool param_description: str @dataclass(slots=True, frozen=True) class TelegramClass: """Represents the scraped Telegram class. Contains all relevant attributes needed for comparison.""" class_name: str class_parameters: list[TelegramParameter] # class_description: str @dataclass(slots=True, frozen=True) class TelegramMethod: """Represents the scraped Telegram method. Contains all relevant attributes needed for comparison.""" method_name: str method_parameters: list[TelegramParameter] # method_description: str @dataclass(slots=True, frozen=False) class Scraper: request: httpx.Response | None = None soup: BeautifulSoup | None = None async def make_request(self) -> None: async with httpx.AsyncClient() as client: self.request = await client.get("https://core.telegram.org/bots/api", timeout=10) self.soup = BeautifulSoup(self.request.text, "html.parser") @overload def parse_docs( self, doc_type: Literal["method"] ) -> tuple[list[TelegramMethod], list[str]]: ... @overload def parse_docs(self, doc_type: Literal["class"]) -> tuple[list[TelegramClass], list[str]]: ... def parse_docs(self, doc_type): argvalues = [] names: list[str] = [] if self.request is None: asyncio.run(self.make_request()) for unparsed in self.soup.select("h4 > a.anchor"): if "-" not in unparsed["name"]: h4: Tag | None = unparsed.parent name = h4.text if h4 is None: raise AssertionError("h4 is None") if doc_type == "method" and name[0].lower() == name[0]: params = parse_table_for_params(h4) obj = TelegramMethod(method_name=name, method_parameters=params) argvalues.append(obj) names.append(name) elif doc_type == "class" and is_pascal_case(name) and name not in IGNORED_OBJECTS: params = parse_table_for_params(h4) obj = TelegramClass(class_name=name, class_parameters=params) argvalues.append(obj) names.append(name) return argvalues, names def collect_methods(self) -> tuple[list[TelegramMethod], list[str]]: return self.parse_docs("method") def collect_classes(self) -> tuple[list[TelegramClass], list[str]]: return self.parse_docs("class") def parse_table_for_params(h4: Tag) -> list[TelegramParameter]: """Parses the Telegram doc table and outputs a list of TelegramParameter objects.""" table = find_next_sibling_until(h4, "table", h4.find_next_sibling("h4")) if not table: return [] params = [] for tr in table.find_all("tr")[1:]: fields = [] for td in tr.find_all("td"): param = td.text fields.append(param) param_name = fields[0] param_type = fields[1] param_required = is_parameter_required_by_tg(fields[2]) param_desc = fields[-1] # since length can be 2 or 3, but desc is always the last params.append(TelegramParameter(param_name, param_type, param_required, param_desc)) return params python-telegram-bot-21.1.1/tests/test_official/test_official.py000066400000000000000000000172711460724040100246450ustar00rootroot00000000000000#!/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 typing import TYPE_CHECKING import pytest import telegram from tests.auxil.envvars import RUN_TEST_OFFICIAL from tests.test_official.arg_type_checker import ( check_defaults_type, check_param_type, check_required_param, ) from tests.test_official.exceptions import ( GLOBALLY_IGNORED_PARAMETERS, backwards_compat_kwargs, ptb_extra_params, ptb_ignored_params, ) from tests.test_official.scraper import Scraper, TelegramClass, TelegramMethod if TYPE_CHECKING: from types import FunctionType # Will skip all tests in this file if the env var is False pytestmark = pytest.mark.skipif(not RUN_TEST_OFFICIAL, reason="test_official is not enabled") methods, method_ids, classes, class_ids = [], [], [], [] # not needed (just for completeness) if RUN_TEST_OFFICIAL: scraper = Scraper() methods, method_ids = scraper.collect_methods() classes, class_ids = scraper.collect_classes() @pytest.mark.parametrize("tg_method", argvalues=methods, ids=method_ids) def test_check_method(tg_method: TelegramMethod) -> None: """This function checks for the following things compared to the official API docs: - Method existence - Parameter existence - Parameter requirement correctness - Parameter type annotation existence - Parameter type annotation correctness - Parameter default value correctness - No unexpected parameters - Extra parameters should be keyword only """ ptb_method: FunctionType | None = getattr(telegram.Bot, tg_method.method_name, None) assert ptb_method, f"Method {tg_method.method_name} not found in telegram.Bot" # Check arguments based on source sig = inspect.signature(ptb_method, follow_wrapped=True) checked = [] for tg_parameter in tg_method.method_parameters: # Check if parameter is present in our method ptb_param = sig.parameters.get(tg_parameter.param_name) assert ( ptb_param is not None ), f"Parameter {tg_parameter.param_name} not found in {ptb_method.__name__}" # Now check if the parameter is required or not assert check_required_param( tg_parameter, ptb_param, ptb_method.__name__ ), f"Param {ptb_param.name!r} of {ptb_method.__name__!r} requirement mismatch" # Check if type annotation is present assert ( ptb_param.annotation is not inspect.Parameter.empty ), f"Param {ptb_param.name!r} of {ptb_method.__name__!r} should have a type annotation!" # Check if type annotation is correct correct_type_hint, expected_type_hint = check_param_type( ptb_param, tg_parameter, ptb_method, ) assert correct_type_hint, ( f"Type hint of param {ptb_param.name!r} of {ptb_method.__name__!r} should be " f"{expected_type_hint!r} or something else!" ) # Now we will check that we don't pass default values if the parameter is not required. if ptb_param.default is not inspect.Parameter.empty: # If there is a default argument... default_arg_none = check_defaults_type(ptb_param) # check if it's None assert ( default_arg_none ), f"Param {ptb_param.name!r} of {ptb_method.__name__!r} should be `None`" checked.append(tg_parameter.param_name) expected_additional_args = GLOBALLY_IGNORED_PARAMETERS.copy() expected_additional_args |= ptb_extra_params(tg_method.method_name) expected_additional_args |= backwards_compat_kwargs(tg_method.method_name) unexpected_args = (sig.parameters.keys() ^ checked) - expected_additional_args assert ( unexpected_args == set() ), f"In {ptb_method.__qualname__}, unexpected args were found: {unexpected_args}." kw_or_positional_args = [ p.name for p in sig.parameters.values() if p.kind != inspect.Parameter.KEYWORD_ONLY ] non_kw_only_args = set(kw_or_positional_args).difference(checked).difference(["self"]) non_kw_only_args -= backwards_compat_kwargs(tg_method.method_name) assert non_kw_only_args == set(), ( f"In {ptb_method.__qualname__}, extra args should be keyword only (compared to " f"{tg_method.method_name} in API)" ) @pytest.mark.parametrize("tg_class", argvalues=classes, ids=class_ids) def test_check_object(tg_class: TelegramClass) -> None: """This function checks for the following things compared to the official API docs: - Class existence - Parameter existence - Parameter requirement correctness - Parameter type annotation existence - Parameter type annotation correctness - Parameter default value correctness - No unexpected parameters """ obj = getattr(telegram, tg_class.class_name) # Check arguments based on source. Makes sure to only check __init__'s signature & nothing else sig = inspect.signature(obj.__init__, follow_wrapped=True) checked = set() fields_removed_by_ptb = ptb_ignored_params(tg_class.class_name) for tg_parameter in tg_class.class_parameters: field: str = tg_parameter.param_name if field in fields_removed_by_ptb: continue if field == "from": field = "from_user" ptb_param = sig.parameters.get(field) assert ptb_param is not None, f"Attribute {field} not found in {obj.__name__}" # Now check if the parameter is required or not assert check_required_param( tg_parameter, ptb_param, obj.__name__ ), f"Param {ptb_param.name!r} of {obj.__name__!r} requirement mismatch" # Check if type annotation is present assert ( ptb_param.annotation is not inspect.Parameter.empty ), f"Param {ptb_param.name!r} of {obj.__name__!r} should have a type annotation" # Check if type annotation is correct correct_type_hint, expected_type_hint = check_param_type(ptb_param, tg_parameter, obj) assert correct_type_hint, ( f"Type hint of param {ptb_param.name!r} of {obj.__name__!r} should be " f"{expected_type_hint!r} or something else!" ) # Now we will check that we don't pass default values if the parameter is not required. if ptb_param.default is not inspect.Parameter.empty: # If there is a default argument... default_arg_none = check_defaults_type(ptb_param) # check if its None assert ( default_arg_none ), f"Param {ptb_param.name!r} of {obj.__name__!r} should be `None`" checked.add(field) expected_additional_args = GLOBALLY_IGNORED_PARAMETERS.copy() expected_additional_args |= ptb_extra_params(tg_class.class_name) expected_additional_args |= backwards_compat_kwargs(tg_class.class_name) unexpected_args = (sig.parameters.keys() ^ checked) - expected_additional_args assert ( unexpected_args == set() ), f"In {tg_class.class_name}, unexpected args were found: {unexpected_args}." python-telegram-bot-21.1.1/tests/test_poll.py000066400000000000000000000275731460724040100212320ustar00rootroot00000000000000#!/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, 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 poll_option(): out = PollOption(text=TestPollOptionBase.text, voter_count=TestPollOptionBase.voter_count) out._unfreeze() return out class TestPollOptionBase: text = "test option" voter_count = 3 class TestPollOptionWithoutRequest(TestPollOptionBase): 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_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 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( TestPollAnswerBase.poll_id, TestPollAnswerBase.option_ids, TestPollAnswerBase.user, TestPollAnswerBase.voter_chat, ) class TestPollAnswerBase: poll_id = "id" option_ids = [2] user = User(1, "", False) voter_chat = Chat(1, "") class TestPollAnswerWithoutRequest(TestPollAnswerBase): 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( TestPollBase.id_, TestPollBase.question, TestPollBase.options, TestPollBase.total_voter_count, TestPollBase.is_closed, TestPollBase.is_anonymous, TestPollBase.type, TestPollBase.allows_multiple_answers, explanation=TestPollBase.explanation, explanation_entities=TestPollBase.explanation_entities, open_period=TestPollBase.open_period, close_date=TestPollBase.close_date, ) poll._unfreeze() return poll class TestPollBase: id_ = "id" question = "Test?" 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) class TestPollWithoutRequest(TestPollBase): 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), } 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) 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), } 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) 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_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_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"} python-telegram-bot-21.1.1/tests/test_pollhandler.py000066400000000000000000000072111460724040100225530ustar00rootroot00000000000000#!/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 pytest from telegram import ( Bot, CallbackQuery, Chat, ChosenInlineResult, Message, Poll, PollOption, PreCheckoutQuery, ShippingQuery, Update, User, ) from telegram.ext import CallbackContext, JobQueue, PollHandler from tests.auxil.slots import mro_slots message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") params = [ {"message": message}, {"edited_message": message}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, {"channel_post": message}, {"edited_channel_post": message}, {"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 = ( "message", "edited_message", "callback_query", "channel_post", "edited_channel_post", "chosen_inline_result", "shipping_query", "pre_checkout_query", "callback_query_without_message", ) @pytest.fixture(scope="class", params=params, ids=ids) def false_update(request): return Update(update_id=2, **request.param) @pytest.fixture() def poll(bot): return Update( 0, poll=Poll( 1, "question", [PollOption("1", 0), PollOption("2", 0)], 0, False, False, Poll.REGULAR, True, ), ) class TestPollHandler: test_flag = False def test_slot_behaviour(self): inst = PollHandler(self.callback) 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" @pytest.fixture(autouse=True) def _reset(self): self.test_flag = False async def callback(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, asyncio.Queue) and isinstance(context.job_queue, JobQueue) and context.user_data is None and context.chat_data is None and isinstance(context.bot_data, dict) and isinstance(update.poll, Poll) ) def test_other_update_types(self, false_update): handler = PollHandler(self.callback) assert not handler.check_update(false_update) async def test_context(self, app, poll): handler = PollHandler(self.callback) app.add_handler(handler) async with app: await app.process_update(poll) assert self.test_flag python-telegram-bot-21.1.1/tests/test_proximityalerttriggered.py000066400000000000000000000073001460724040100252370ustar00rootroot00000000000000#!/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( TestProximityAlertTriggeredBase.traveler, TestProximityAlertTriggeredBase.watcher, TestProximityAlertTriggeredBase.distance, ) class TestProximityAlertTriggeredBase: traveler = User(1, "foo", False) watcher = User(2, "bar", False) distance = 42 class TestProximityAlertTriggeredWithoutRequest(TestProximityAlertTriggeredBase): 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) python-telegram-bot-21.1.1/tests/test_reaction.py000066400000000000000000000225361460724040100220620ustar00rootroot00000000000000#!/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, ) 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 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, ], 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 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 else: 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) python-telegram-bot-21.1.1/tests/test_reply.py000066400000000000000000000234211460724040100214030ustar00rootroot00000000000000#!/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, ReplyParameters, TextQuote, User, ) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") def external_reply_info(): return ExternalReplyInfo( origin=TestExternalReplyInfoBase.origin, chat=TestExternalReplyInfoBase.chat, message_id=TestExternalReplyInfoBase.message_id, link_preview_options=TestExternalReplyInfoBase.link_preview_options, giveaway=TestExternalReplyInfoBase.giveaway, ) class TestExternalReplyInfoBase: 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, ) class TestExternalReplyInfoWithoutRequest(TestExternalReplyInfoBase): 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(), } 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 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() 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=TestTextQuoteBase.text, position=TestTextQuoteBase.position, entities=TestTextQuoteBase.entities, is_manual=TestTextQuoteBase.is_manual, ) class TestTextQuoteBase: text = "text" position = 1 entities = [ MessageEntity(MessageEntity.MENTION, 1, 2), MessageEntity(MessageEntity.EMAIL, 3, 4), ] is_manual = True class TestTextQuoteWithoutRequest(TestTextQuoteBase): 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=TestReplyParametersBase.message_id, chat_id=TestReplyParametersBase.chat_id, allow_sending_without_reply=TestReplyParametersBase.allow_sending_without_reply, quote=TestReplyParametersBase.quote, quote_parse_mode=TestReplyParametersBase.quote_parse_mode, quote_entities=TestReplyParametersBase.quote_entities, quote_position=TestReplyParametersBase.quote_position, ) class TestReplyParametersBase: 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(TestReplyParametersBase): 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) python-telegram-bot-21.1.1/tests/test_replykeyboardmarkup.py000066400000000000000000000160331460724040100243450ustar00rootroot00000000000000#!/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( TestReplyKeyboardMarkupBase.keyboard, resize_keyboard=TestReplyKeyboardMarkupBase.resize_keyboard, one_time_keyboard=TestReplyKeyboardMarkupBase.one_time_keyboard, selective=TestReplyKeyboardMarkupBase.selective, is_persistent=TestReplyKeyboardMarkupBase.is_persistent, ) class TestReplyKeyboardMarkupBase: keyboard = [[KeyboardButton("button1"), KeyboardButton("button2")]] resize_keyboard = True one_time_keyboard = True selective = True is_persistent = True class TestReplyKeyboardMarkupWithoutRequest(TestReplyKeyboardMarkupBase): 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(TestReplyKeyboardMarkupBase): 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" python-telegram-bot-21.1.1/tests/test_replykeyboardremove.py000066400000000000000000000045001460724040100243370ustar00rootroot00000000000000#!/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=TestReplyKeyboardRemoveBase.selective) class TestReplyKeyboardRemoveBase: remove_keyboard = True selective = True class TestReplyKeyboardRemoveWithoutRequest(TestReplyKeyboardRemoveBase): 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(TestReplyKeyboardRemoveBase): 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" python-telegram-bot-21.1.1/tests/test_sentwebappmessage.py000066400000000000000000000045631460724040100237730ustar00rootroot00000000000000#!/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=TestSentWebAppMessageBase.inline_message_id) class TestSentWebAppMessageBase: inline_message_id = "123" class TestSentWebAppMessageWithoutRequest(TestSentWebAppMessageBase): 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) python-telegram-bot-21.1.1/tests/test_shared.py000066400000000000000000000205661460724040100215250ustar00rootroot00000000000000#!/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 telegram.warnings import PTBDeprecationWarning from tests.auxil.slots import mro_slots @pytest.fixture(scope="class") def users_shared(): return UsersShared(TestUsersSharedBase.request_id, users=TestUsersSharedBase.users) class TestUsersSharedBase: request_id = 789 user_ids = (101112, 101113) users = (SharedUser(101112, "user1"), SharedUser(101113, "user2")) class TestUsersSharedWithoutRequest(TestUsersSharedBase): 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 users_shared.user_ids == tuple(self.user_ids) assert UsersShared.de_json({}, bot) is None def test_users_is_required_argument(self): with pytest.raises(TypeError, match="`users` is a required argument"): UsersShared(self.request_id, user_ids=self.user_ids) def test_user_ids_deprecation_warning(self): with pytest.warns( PTBDeprecationWarning, match="'user_ids' was renamed to 'users' in Bot API 7.2" ): users_shared = UsersShared(self.request_id, user_ids=self.user_ids, users=self.users) with pytest.warns( PTBDeprecationWarning, match="renamed the attribute 'user_ids' to 'users'" ): users_shared.user_ids 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( TestChatSharedBase.request_id, TestChatSharedBase.chat_id, ) class TestChatSharedBase: request_id = 131415 chat_id = 161718 class TestChatSharedWithoutRequest(TestChatSharedBase): 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( TestSharedUserBase.user_id, TestSharedUserBase.first_name, last_name=TestSharedUserBase.last_name, username=TestSharedUserBase.username, photo=TestSharedUserBase.photo, ) class TestSharedUserBase: 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(TestSharedUserBase): 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) python-telegram-bot-21.1.1/tests/test_slots.py000066400000000000000000000045341460724040100214200ustar00rootroot00000000000000#!/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__] python-telegram-bot-21.1.1/tests/test_story.py000066400000000000000000000043571460724040100214370ustar00rootroot00000000000000#!/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(TestStoryBase.chat, TestStoryBase.id) class TestStoryBase: chat = Chat(1, "") id = 0 class TestStoryWithoutRequest(TestStoryBase): 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) python-telegram-bot-21.1.1/tests/test_switchinlinequerychosenchat.py000066400000000000000000000072671460724040100261100ustar00rootroot00000000000000#!/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=TestSwitchInlineQueryChosenChatBase.query, allow_user_chats=TestSwitchInlineQueryChosenChatBase.allow_user_chats, allow_bot_chats=TestSwitchInlineQueryChosenChatBase.allow_bot_chats, allow_channel_chats=TestSwitchInlineQueryChosenChatBase.allow_channel_chats, allow_group_chats=TestSwitchInlineQueryChosenChatBase.allow_group_chats, ) class TestSwitchInlineQueryChosenChatBase: query = "query" allow_user_chats = True allow_bot_chats = True allow_channel_chats = False allow_group_chats = True class TestSwitchInlineQueryChosenChat(TestSwitchInlineQueryChosenChatBase): 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) python-telegram-bot-21.1.1/tests/test_telegramobject.py000066400000000000000000000542541460724040100232470ustar00rootroot00000000000000#!/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_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 assert chat.api_kwargs == { "all_members_are_administrators": True, "something": "Manually inserted", } 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") python-telegram-bot-21.1.1/tests/test_update.py000066400000000000000000000321651460724040100215370ustar00rootroot00000000000000#!/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=TestUpdateBase.update_id, **request.param) class TestUpdateBase: update_id = 868573637 class TestUpdateWithoutRequest(TestUpdateBase): 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__ python-telegram-bot-21.1.1/tests/test_user.py000066400000000000000000001014131460724040100212240ustar00rootroot00000000000000#!/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": TestUserBase.id_, "is_bot": TestUserBase.is_bot, "first_name": TestUserBase.first_name, "last_name": TestUserBase.last_name, "username": TestUserBase.username, "language_code": TestUserBase.language_code, "can_join_groups": TestUserBase.can_join_groups, "can_read_all_group_messages": TestUserBase.can_read_all_group_messages, "supports_inline_queries": TestUserBase.supports_inline_queries, "is_premium": TestUserBase.is_premium, "added_to_attachment_menu": TestUserBase.added_to_attachment_menu, "can_connect_to_business": TestUserBase.can_connect_to_business, } @pytest.fixture() def user(bot): user = User( id=TestUserBase.id_, first_name=TestUserBase.first_name, is_bot=TestUserBase.is_bot, last_name=TestUserBase.last_name, username=TestUserBase.username, language_code=TestUserBase.language_code, can_join_groups=TestUserBase.can_join_groups, can_read_all_group_messages=TestUserBase.can_read_all_group_messages, supports_inline_queries=TestUserBase.supports_inline_queries, is_premium=TestUserBase.is_premium, added_to_attachment_menu=TestUserBase.added_to_attachment_menu, can_connect_to_business=TestUserBase.can_connect_to_business, ) user.set_bot(bot) user._unfreeze() return user class TestUserBase: 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 class TestUserWithoutRequest(TestUserBase): 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 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 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.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.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)) python-telegram-bot-21.1.1/tests/test_userprofilephotos.py000066400000000000000000000054771460724040100240570ustar00rootroot00000000000000#!/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 TestUserProfilePhotosBase: 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(TestUserProfilePhotosBase): 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) python-telegram-bot-21.1.1/tests/test_version.py000066400000000000000000000064551460724040100217450ustar00rootroot00000000000000#!/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) python-telegram-bot-21.1.1/tests/test_videochat.py000066400000000000000000000166431460724040100222260ustar00rootroot00000000000000#!/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) python-telegram-bot-21.1.1/tests/test_warnings.py000066400000000000000000000064531460724040100221060ustar00rootroot00000000000000#!/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()), ], ) 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("test message 3", stacklevel=1, category=PTBDeprecationWarning) expected_file = Path(__file__) assert len(recwarn) == 3 assert recwarn[2].category is PTBDeprecationWarning assert str(recwarn[2].message) == "test message 3" assert Path(recwarn[2].filename) == expected_file, "incorrect stacklevel!" python-telegram-bot-21.1.1/tests/test_webappdata.py000066400000000000000000000046421460724040100223640ustar00rootroot00000000000000#!/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=TestWebAppDataBase.data, button_text=TestWebAppDataBase.button_text) class TestWebAppDataBase: data = "data" button_text = "button_text" class TestWebAppDataWithoutRequest(TestWebAppDataBase): 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) python-telegram-bot-21.1.1/tests/test_webappinfo.py000066400000000000000000000042011460724040100223750ustar00rootroot00000000000000#!/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=TestWebAppInfoBase.url) class TestWebAppInfoBase: url = "https://www.example.com" class TestWebAppInfoWithoutRequest(TestWebAppInfoBase): 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) python-telegram-bot-21.1.1/tests/test_webhookinfo.py000066400000000000000000000170701460724040100225650ustar00rootroot00000000000000#!/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=TestWebhookInfoBase.url, has_custom_certificate=TestWebhookInfoBase.has_custom_certificate, pending_update_count=TestWebhookInfoBase.pending_update_count, ip_address=TestWebhookInfoBase.ip_address, last_error_date=TestWebhookInfoBase.last_error_date, max_connections=TestWebhookInfoBase.max_connections, allowed_updates=TestWebhookInfoBase.allowed_updates, last_synchronization_error_date=TestWebhookInfoBase.last_synchronization_error_date, ) class TestWebhookInfoBase: 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(TestWebhookInfoBase): 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) python-telegram-bot-21.1.1/tests/test_writeaccessallowed.py000066400000000000000000000037141460724040100241370ustar00rootroot00000000000000#!/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)