pax_global_header00006660000000000000000000000064141765632440014526gustar00rootroot0000000000000052 comment=92cb6f3ae8d5c3e49b9019a9348d4408135ffc95 python-telegram-bot-13.11/000077500000000000000000000000001417656324400154345ustar00rootroot00000000000000python-telegram-bot-13.11/.deepsource.toml000066400000000000000000000005221417656324400205440ustar00rootroot00000000000000version = 1 test_patterns = ["tests/**"] exclude_patterns = [ "tests/**", "docs/**", "telegram/vendor/**", "setup.py", "setup-raw.py" ] [[analyzers]] name = "python" enabled = true [analyzers.meta] runtime_version = "3.x.x" max_line_length = 99 skip_doc_coverage = ["module", "magic", "init", "nonpublic"] python-telegram-bot-13.11/.github/000077500000000000000000000000001417656324400167745ustar00rootroot00000000000000python-telegram-bot-13.11/.github/CONTRIBUTING.rst000066400000000000000000000244421417656324400214430ustar00rootroot00000000000000How 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 --recursive $ 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.txt -r requirements-dev.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`_ (use `@admins` to mention the maintainers), 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. - 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.helpers.typing``. - Document your code. This project uses `sphinx`_ to generate static HTML docs. To build them, first make sure you 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:: version``, ``.. versionchanged:: version`` or ``.. deprecated:: 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. - 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 the `Black`_ coder 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. - Don’t break backward compatibility. - Add yourself to the AUTHORS.rst_ file in an alphabetical fashion. - Before making a commit ensure that all automated tests still pass: .. code-block:: $ pytest -v To run ``test_official`` (particularly useful if you made API changes), run .. code-block:: $ export TEST_OFFICIAL=true prior to running the tests. - If you want run style & type checks before committing run .. code-block:: $ 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 an email. 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 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 - If after merging you see local modified files in ``telegram/vendor/`` directory, that you didn't actually touch, that means you need to update submodules with this command: .. code-block:: bash $ git submodule update --init --recursive - 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``! 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. Properly defining optional arguments #################################### It's always good to not initialize optional arguments at class creation, instead use ``**kwargs`` to get them. It's well known Telegram API can change without notice, in that case if a new argument is added it won't break the API classes. For example: .. code-block:: python # GOOD def __init__(self, id, name, last_name=None, **kwargs): self.last_name = last_name # BAD def __init__(self, id, name, last_name=None): self.last_name = last_name .. _`Code of Conduct`: https://www.python.org/psf/codeofconduct/ .. _`issue tracker`: https://github.com/python-telegram-bot/python-telegram-bot/issues .. _`Telegram group`: https://telegram.me/pythontelegrambotgroup .. _`PEP 8 Style Guide`: https://www.python.org/dev/peps/pep-0008/ .. _`sphinx`: http://sphinx-doc.org .. _`Google Python Style Guide`: http://google.github.io/styleguide/pyguide.html .. _`Google Python Style Docstrings`: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html .. _AUTHORS.rst: ../AUTHORS.rst .. _`MyPy`: https://mypy.readthedocs.io/en/stable/index.html .. _`here`: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html .. _`Black`: https://black.readthedocs.io/en/stable/index.html .. _`popular editors`: https://black.readthedocs.io/en/stable/editor_integration.html .. _`RTD build`: https://python-telegram-bot.readthedocs.io/en/doc-fixes python-telegram-bot-13.11/.github/ISSUE_TEMPLATE/000077500000000000000000000000001417656324400211575ustar00rootroot00000000000000python-telegram-bot-13.11/.github/ISSUE_TEMPLATE/bug-report.yml000066400000000000000000000040071417656324400237710ustar00rootroot00000000000000name: 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-13.11/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000006211417656324400231460ustar00rootroot00000000000000blank_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-13.11/.github/ISSUE_TEMPLATE/feature-request.yml000066400000000000000000000025251417656324400250270ustar00rootroot00000000000000name: 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-13.11/.github/ISSUE_TEMPLATE/question.yml000066400000000000000000000044661417656324400235630ustar00rootroot00000000000000name: 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://git.io/JURJO). 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://git.io/JG3rk) 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-13.11/.github/pull_request_template.md000066400000000000000000000036131417656324400237400ustar00rootroot00000000000000 ### Checklist for PRs - [ ] Added `.. versionadded:: version`, `.. versionchanged:: version` or `.. deprecated:: version` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes) - [ ] Created new or adapted existing unit tests - [ ] Added myself alphabetically to `AUTHORS.rst` (optional) - [ ] Added new classes & modules to the docs ### If the PR contains API changes (otherwise, you can delete this passage) * New classes: - [ ] Added `self._id_attrs` and corresponding documentation - [ ] `__init__` accepts `**_kwargs` * Added new shortcuts: - [ ] In `Chat` & `User` for all methods that accept `chat/user_id` - [ ] In `Message` for all methods that accept `chat_id` and `message_id` - [ ] For new `Message` shortcuts: Added `quote` argument if methods accepts `reply_to_message_id` - [ ] In `CallbackQuery` for all methods that accept either `chat_id` and `message_id` or `inline_message_id` * If relevant: - [ ] Added new constants at `telegram.constants` and shortcuts to them as class variables - [ ] Added new handlers for new update types - [ ] Added new filters for new message (sub)types - [ ] Added or updated documentation for the changed class(es) and/or method(s) - [ ] 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` - [ ] Added logic for arbitrary callback data in `tg.ext.Bot` for new methods that either accept a `reply_markup` in some form or have a return type that is/contains `telegram.Message` python-telegram-bot-13.11/.github/stale.yml000066400000000000000000000013051417656324400206260ustar00rootroot00000000000000# Number of days of inactivity before an issue becomes stale daysUntilStale: 3 # Number of days of inactivity before a stale issue is closed daysUntilClose: 2 # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) onlyLabels: question # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking as stale. Set to `false` to disable markComment: false # Comment to post when closing a stale issue. Set to `false` to disable closeComment: > 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-13.11/.github/workflows/000077500000000000000000000000001417656324400210315ustar00rootroot00000000000000python-telegram-bot-13.11/.github/workflows/example_notifier.yml000066400000000000000000000007561417656324400251160ustar00rootroot00000000000000name: Warning maintainers on: pull_request: paths: examples/** jobs: job: runs-on: ubuntu-latest name: about example change steps: - name: running the check uses: Poolitzer/notifier-action@master with: notify-message: Hey there. Relax, I am just a little warning for the maintainers to release directly after merging your PR, otherwise we have broken examples and people might get confused :) repo-token: ${{ secrets.GITHUB_TOKEN }}python-telegram-bot-13.11/.github/workflows/lock.yml000066400000000000000000000006021417656324400225020ustar00rootroot00000000000000name: 'Lock Closed Threads' on: schedule: - cron: '8 4 * * *' - cron: '42 17 * * *' jobs: lock: runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v2.0.1 with: github-token: ${{ github.token }} issue-lock-inactive-days: '7' issue-lock-reason: '' pr-lock-inactive-days: '7' pr-lock-reason: '' python-telegram-bot-13.11/.github/workflows/pre-commit_dependencies_notifier.yml000066400000000000000000000012321417656324400302330ustar00rootroot00000000000000name: Warning maintainers on: pull_request: paths: - requirements.txt - requirements-dev.txt - .pre-commit-config.yaml 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 (dev) requirements or the pre-commit hooks. I'm just a friendly reminder to keep the pre-commit hook versions in sync with the dev requirements and the additional dependencies for the hooks in sync with the requirements :) repo-token: ${{ secrets.GITHUB_TOKEN }} python-telegram-bot-13.11/.github/workflows/readme_notifier.yml000066400000000000000000000007471417656324400247200ustar00rootroot00000000000000name: Warning maintainers on: pull_request: paths: - README.rst - README_RAW.rst 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-13.11/.github/workflows/test.yml000066400000000000000000000173641417656324400225460ustar00rootroot00000000000000name: GitHub Actions on: pull_request: branches: - master push: branches: - master jobs: pytest: name: pytest runs-on: ${{matrix.os}} strategy: matrix: python-version: [3.6, 3.7, 3.8, 3.9] os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: False steps: - uses: actions/checkout@v2 - name: Initialize vendored libs run: git submodule update --init --recursive - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip python -W ignore -m pip install -U codecov pytest-cov python -W ignore -m pip install -r requirements.txt python -W ignore -m pip install -r requirements-dev.txt - name: Test with pytest # We run 3 different suites here # 1. Test just utils.helpers.py without pytz being installed # 2. Test just test_no_passport.py without passport dependencies being installed # 3. Test everything else # The first & second one are achieved by mocking the corresponding import # See test_helpers.py & test_no_passport.py for details run: | pytest -v --cov -k test_no_passport.py no_passport_exit=$? export TEST_NO_PASSPORT='false' pytest -v --cov --cov-append -k test_helpers.py no_pytz_exit=$? export TEST_NO_PYTZ='false' pytest -v --cov --cov-append full_exit=$? special_exit=$(( no_pytz_exit > no_passport_exit ? no_pytz_exit : no_passport_exit )) global_exit=$(( special_exit > full_exit ? special_exit : full_exit )) exit ${global_exit} env: JOB_INDEX: ${{ strategy.job-index }} BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMi43IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzkwOTgzOTk3IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzI3X2JvdCJ9LCB7InRva2VuIjogIjY3MTQ2ODg4NjpBQUdQR2ZjaVJJQlVORmU4MjR1SVZkcTdKZTNfWW5BVE5HdyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpHWXdPVGxrTXpNeE4yWTIiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ0NjAyMjUyMiIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNF9ib3QifSwgeyJ0b2tlbiI6ICI2MjkzMjY1Mzg6QUFGUnJaSnJCN29CM211ekdzR0pYVXZHRTVDUXpNNUNVNG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNbU01WVdKaFl6a3hNMlUxIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjUiLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDE0OTY5MTc3NTAiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy42IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzMzODcxNDYxIiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM2X2JvdCJ9LCB7InRva2VuIjogIjY5NTEwNDA4ODpBQUhmenlsSU9qU0lJUy1lT25JMjB5MkUyMEhvZEhzZnotMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk9HUTFNRGd3WmpJd1pqRmwiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ3ODI5MzcxNCIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zN19ib3QifSwgeyJ0b2tlbiI6ICI2OTE0MjM1NTQ6QUFGOFdrakNaYm5IcVBfaTZHaFRZaXJGRWxackdhWU9oWDAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZamM1TlRoaU1tUXlNV1ZoIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEzNjM5MzI1NzMiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMy41IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDA3ODM2NjA1IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19weXB5XzM1X2JvdCJ9LCB7InRva2VuIjogIjY5MDA5MTM0NzpBQUZMbVI1cEFCNVljcGVfbU9oN3pNNEpGQk9oMHozVDBUbyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpEaGxOekU1TURrd1lXSmkiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjc5NjAwMDI2IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2FwcHZleW9yX2NweXRob25fMzRfYm90In0sIHsidG9rZW4iOiAiNjk0MzA4MDUyOkFBRUIyX3NvbkNrNTVMWTlCRzlBTy1IOGp4aVBTNTVvb0JBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WW1aaVlXWm1NakpoWkdNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gQXBwVmV5b3IgdXNpbmcgQ1B5dGhvbiAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEyOTMwNzkxNjUiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QifSwgeyJ0b2tlbiI6ICIxMDU1Mzk3NDcxOkFBRzE4bkJfUzJXQXd1SjNnN29oS0JWZ1hYY2VNbklPeVNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpBd056QXpZalZpTkdOayIsICJuYW1lIjogIlBUQiB0ZXN0cyBbMF0iLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDExODU1MDk2MzYiLCAidXNlcm5hbWUiOiAicHRiXzBfYm90In0sIHsidG9rZW4iOiAiMTA0NzMyNjc3MTpBQUY4bk90ODFGcFg4bGJidno4VWV3UVF2UmZUYkZmQnZ1SSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllUVTFOVEk0WkdSallqbGkiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzFdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDg0Nzk3NjEyIiwgInVzZXJuYW1lIjogInB0Yl8xX2JvdCJ9LCB7InRva2VuIjogIjk3MTk5Mjc0NTpBQUdPa09hVzBOSGpnSXY1LTlqUWJPajR2R3FkaFNGLVV1cyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk5XWmtNV1ZoWWpsallqVTUiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzJdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDAyMjU1MDcwIiwgInVzZXJuYW1lIjogInB0Yl8yX2JvdCJ9XQ== TEST_NO_PYTZ : "true" TEST_NO_PASSPORT: "true" TEST_BUILD: "true" shell: bash --noprofile --norc {0} - name: Submit coverage uses: codecov/codecov-action@v1 with: env_vars: OS,PYTHON name: ${{ matrix.os }}-${{ matrix.python-version }} fail_ci_if_error: true test_official: name: test-official runs-on: ${{matrix.os}} strategy: matrix: python-version: [3.7] os: [ubuntu-latest] fail-fast: False steps: - uses: actions/checkout@v2 - name: Initialize vendored libs run: git submodule update --init --recursive - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip python -W ignore -m pip install -r requirements.txt python -W ignore -m pip install -r requirements-dev.txt - name: Compare to official api run: | pytest -v tests/test_official.py exit $? env: TEST_OFFICIAL: "true" shell: bash --noprofile --norc {0} test_pre_commit: name: test-pre-commit runs-on: ${{matrix.os}} strategy: matrix: python-version: [3.7] os: [ubuntu-latest] fail-fast: False steps: - uses: actions/checkout@v2 - name: Initialize vendored libs run: git submodule update --init --recursive - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -W ignore -m pip install --upgrade pip python -W ignore -m pip install -r requirements.txt python -W ignore -m pip install -r requirements-dev.txt - name: Run pre-commit tests run: pre-commit run --all-files python-telegram-bot-13.11/.gitignore000066400000000000000000000021001417656324400174150ustar00rootroot00000000000000# 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 # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ .idea/ # Sublime Text 2 *.sublime* # 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-13.11/.gitmodules000066400000000000000000000002221417656324400176050ustar00rootroot00000000000000[submodule "telegram/vendor/urllib3"] path = telegram/vendor/ptb_urllib3 url = https://github.com/python-telegram-bot/urllib3.git branch = ptb python-telegram-bot-13.11/.pre-commit-config.yaml000066400000000000000000000031261417656324400217170ustar00rootroot00000000000000# Make sure that # * the revs specified here match requirements-dev.txt # * the additional_dependencies here match requirements.txt repos: - repo: https://github.com/psf/black rev: 20.8b1 hooks: - id: black args: - --diff - --check - repo: https://gitlab.com/pycqa/flake8 rev: 3.9.2 hooks: - id: flake8 - repo: https://github.com/PyCQA/pylint rev: v2.8.3 hooks: - id: pylint files: ^(telegram|examples)/.*\.py$ args: - --rcfile=setup.cfg additional_dependencies: - certifi - tornado>=6.1 - APScheduler==3.6.3 - cachetools==4.2.2 - . # this basically does `pip install -e .` - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.812 hooks: - id: mypy name: mypy-ptb files: ^telegram/.*\.py$ additional_dependencies: - certifi - tornado>=6.1 - APScheduler==3.6.3 - cachetools==4.2.2 - . # this basically does `pip install -e .` - id: mypy name: mypy-examples files: ^examples/.*\.py$ args: - --no-strict-optional - --follow-imports=silent additional_dependencies: - certifi - tornado>=6.1 - APScheduler==3.6.3 - cachetools==4.2.2 - . # this basically does `pip install -e .` - repo: https://github.com/asottile/pyupgrade rev: v2.19.1 hooks: - id: pyupgrade files: ^(telegram|examples|tests)/.*\.py$ args: - --py36-plus python-telegram-bot-13.11/.readthedocs.yml000066400000000000000000000010351417656324400205210ustar00rootroot00000000000000# .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: version: 3 install: - method: pip path: . - requirements: docs/requirements-docs.txt python-telegram-bot-13.11/AUTHORS.rst000066400000000000000000000120011417656324400173050ustar00rootroot00000000000000Credits ======= ``python-telegram-bot`` was originally created by `Leandro Toledo `_. The current development team includes - `Hinrich Mahler `_ (maintainer) - `Poolitzer `_ (community liaison) - `Shivam `_ - `Harshil `_ Emeritus maintainers include `Jannes Höke `_ (`@jh0ker `_ on Telegram), `Noam Meltzer `_, `Pieter Schutz `_ and `Jasmin Bom `_. Vendored packages ----------------- We're vendoring urllib3 as part of ``python-telegram-bot`` which is distributed under the MIT license. For more info, full credits & license terms, the sources can be found here: `https://github.com/python-telegram-bot/urllib3`. Contributors ------------ The following wonderful people contributed directly or indirectly to this project: - `Abshar `_ - `Alateas `_ - `Ales Dokshanin `_ - `Ambro17 `_ - `Andrej Zhilenkov `_ - `Anton Tagunov `_ - `Avanatiker `_ - `Balduro `_ - `Bibo-Joshi `_ - `bimmlerd `_ - `d-qoi `_ - `daimajia `_ - `Daniel Reed `_ - `D David Livingston `_ - `DonalDuck004 `_ - `Eana Hufwe `_ - `Ehsan Online `_ - `Eli Gao `_ - `Emilio Molinari `_ - `ErgoZ Riftbit Vaper `_ - `Eugene Lisitsky `_ - `Eugenio Panadero `_ - `Evan Haberecht `_ - `Evgeny Denisov `_ - `evgfilim1 `_ - `franciscod `_ - `gamgi `_ - `Gauthamram Ravichandran `_ - `Harshil `_ - `Hugo Damer `_ - `ihoru `_ - `Jasmin Bom `_ - `JASON0916 `_ - `jeffffc `_ - `Jelle Besseling `_ - `jh0ker `_ - `jlmadurga `_ - `John Yong `_ - `Joscha Götzer `_ - `jossalgon `_ - `JRoot3D `_ - `Kirill Vasin `_ - `Kjwon15 `_ - `Li-aung Yip `_ - `Loo Zheng Yuan `_ - `LRezende `_ - `macrojames `_ - `Matheus Lemos `_ - `Michael Elovskikh `_ - `Mischa Krüger `_ - `naveenvhegde `_ - `neurrone `_ - `NikitaPirate `_ - `Nikolai Krivenko `_ - `njittam `_ - `Noam Meltzer `_ - `Oleg Shlyazhko `_ - `Oleg Sushchenko `_ - `Or Bin `_ - `overquota `_ - `Paradox `_ - `Patrick Hofmann `_ - `Paul Larsen `_ - `Pieter Schutz `_ - `Poolitzer `_ - `Pranjalya Tiwari `_ - `Rahiel Kasim `_ - `Riko Naka `_ - `Rizlas `_ - `Sahil Sharma `_ - `Sascha `_ - `Shelomentsev D `_ - `Simon Schürrle `_ - `sooyhwang `_ - `syntx `_ - `thodnev `_ - `Timur Kushukov `_ - `Trainer Jono `_ - `Valentijn `_ - `voider1 `_ - `Vorobjev Simon `_ - `Wagner Macedo `_ - `wjt `_ - `zeroone2numeral2 `_ - `zeshuaro `_ Please add yourself here alphabetically when you submit your first pull request. python-telegram-bot-13.11/CHANGES.rst000066400000000000000000002364701417656324400172520ustar00rootroot00000000000000========= Changelog ========= 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 (`#2881`_) .. _`#2881`: https://github.com/python-telegram-bot/python-telegram-bot/pull/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 (`#2835`_) **Minor Changes & Doc fixes:** - Update Copyright to 2022 (`#2836`_) - Update Documentation of ``BotCommand`` (`#2820`_) .. _`#2835`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2835 .. _`#2836`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2836 .. _`#2820`: https://github.com/python-telegram-bot/python-telegram-bot/pull/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 (`#2809`_) **Minor Changes** - Adjust Automated Locking of Inactive Issues (`#2775`_) .. _`#2809`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2809 .. _`#2775`: https://github.com/python-telegram-bot/python-telegram-bot/pull/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 (`#2771`_) .. _`#2771`: https://github.com/python-telegram-bot/python-telegram-bot/pull/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 (`#2767`_) **Minor changes, CI improvements, Doc fixes and Type hinting:** - Create Issue Template Forms (`#2689`_) - Fix ``camelCase`` Functions in ``ExtBot`` (`#2659`_) - Fix Empty Captions not Being Passed by ``Bot.copy_message`` (`#2651`_) - Fix Setting Thumbs When Uploading A Single File (`#2583`_) - Fix Bug in ``BasePersistence.insert``/``replace_bot`` for Objects with ``__dict__`` not in ``__slots__`` (`#2603`_) .. _`#2767`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2767 .. _`#2689`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2689 .. _`#2659`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2659 .. _`#2651`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2651 .. _`#2583`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2583 .. _`#2603`: https://github.com/python-telegram-bot/python-telegram-bot/pull/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 (`#2572`_) **Bug Fixes:** - Fix Bug in ``BasePersistence.insert/replace_bot`` for Objects with ``__dict__`` in their slots (`#2561`_) - Remove Incorrect Warning About ``Defaults`` and ``ExtBot`` (`#2553`_) **Minor changes, CI improvements, Doc fixes and Type hinting:** - Type Hinting Fixes (`#2552`_) - Doc Fixes (`#2551`_) - Improve Deprecation Warning for ``__slots__`` (`#2574`_) - Stabilize CI (`#2575`_) - Fix Coverage Configuration (`#2571`_) - Better Exception-Handling for ``BasePersistence.replace/insert_bot`` (`#2564`_) - Remove Deprecated ``pass_args`` from Deeplinking Example (`#2550`_) .. _`#2572`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2572 .. _`#2561`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2561 .. _`#2553`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2553 .. _`#2552`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2552 .. _`#2551`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2551 .. _`#2574`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2574 .. _`#2575`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2575 .. _`#2571`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2571 .. _`#2564`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2564 .. _`#2550`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2550 Version 13.6 ============ *Released 2021-06-06* New Features: - Arbitrary ``callback_data`` (`#1844`_) - Add ``ContextTypes`` & ``BasePersistence.refresh_user/chat/bot_data`` (`#2262`_) - Add ``Filters.attachment`` (`#2528`_) - Add ``pattern`` Argument to ``ChosenInlineResultHandler`` (`#2517`_) Major Changes: - Add ``slots`` (`#2345`_) Minor changes, CI improvements, Doc fixes and Type hinting: - Doc Fixes (`#2495`_, `#2510`_) - Add ``max_connections`` Parameter to ``Updater.start_webhook`` (`#2547`_) - Fix for ``Promise.done_callback`` (`#2544`_) - Improve Code Quality (`#2536`_, `#2454`_) - Increase Test Coverage of ``CallbackQueryHandler`` (`#2520`_) - Stabilize CI (`#2522`_, `#2537`_, `#2541`_) - Fix ``send_phone_number_to_provider`` argument for ``Bot.send_invoice`` (`#2527`_) - Handle Classes as Input for ``BasePersistence.replace/insert_bot`` (`#2523`_) - Bump Tornado Version and Remove Workaround from `#2067`_ (`#2494`_) .. _`#1844`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1844 .. _`#2262`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2262 .. _`#2528`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2528 .. _`#2517`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2517 .. _`#2345`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2345 .. _`#2495`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2495 .. _`#2547`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2547 .. _`#2544`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2544 .. _`#2536`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2536 .. _`#2454`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2454 .. _`#2520`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2520 .. _`#2522`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2522 .. _`#2537`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2537 .. _`#2541`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2541 .. _`#2527`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2527 .. _`#2523`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2523 .. _`#2067`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2067 .. _`#2494`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2494 .. _`#2510`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2510 Version 13.5 ============ *Released 2021-04-30* **Major Changes:** - Full support of Bot API 5.2 (`#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`` (`#2460`_) **New Features:** - Convenience Utilities & Example for Handling ``ChatMemberUpdated`` (`#2490`_) - ``Filters.forwarded_from`` (`#2446`_) **Minor changes, CI improvements, Doc fixes and Type hinting:** - Improve Timeouts in ``ConversationHandler`` (`#2417`_) - Stabilize CI (`#2480`_) - Doc Fixes (`#2437`_) - Improve Type Hints of Data Filters (`#2456`_) - Add Two ``UserWarnings`` (`#2464`_) - Improve Code Quality (`#2450`_) - Update Fallback Test-Bots (`#2451`_) - Improve Examples (`#2441`_, `#2448`_) .. _`#2489`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2489 .. _`#2460`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2460 .. _`#2490`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2490 .. _`#2446`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2446 .. _`#2417`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2417 .. _`#2480`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2480 .. _`#2437`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2437 .. _`#2456`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2456 .. _`#2464`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2464 .. _`#2450`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2450 .. _`#2451`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2451 .. _`#2441`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2441 .. _`#2448`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2448 Version 13.4.1 ============== *Released 2021-03-14* **Hot fix release:** - Fixed a bug in ``setup.py`` (`#2431`_) .. _`#2431`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2431 Version 13.4 ============ *Released 2021-03-14* **Major Changes:** - Full support of Bot API 5.1 (`#2424`_) **Minor changes, CI improvements, doc fixes and type hinting:** - Improve ``Updater.set_webhook`` (`#2419`_) - Doc Fixes (`#2404`_) - Type Hinting Fixes (`#2425`_) - Update ``pre-commit`` Settings (`#2415`_) - Fix Logging for Vendored ``urllib3`` (`#2427`_) - Stabilize Tests (`#2409`_) .. _`#2424`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2424 .. _`#2419`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2419 .. _`#2404`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2404 .. _`#2425`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2425 .. _`#2415`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2415 .. _`#2427`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2427 .. _`#2409`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2409 Version 13.3 ============ *Released 2021-02-19* **Major Changes:** - Make ``cryptography`` Dependency Optional & Refactor Some Tests (`#2386`_, `#2370`_) - Deprecate ``MessageQueue`` (`#2393`_) **Bug Fixes:** - Refactor ``Defaults`` Integration (`#2363`_) - Add Missing ``telegram.SecureValue`` to init and Docs (`#2398`_) **Minor changes:** - Doc Fixes (`#2359`_) .. _`#2386`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2386 .. _`#2370`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2370 .. _`#2393`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2393 .. _`#2363`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2363 .. _`#2398`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2398 .. _`#2359`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2359 Version 13.2 ============ *Released 2021-02-02* **Major Changes:** - Introduce ``python-telegram-bot-raw`` (`#2324`_) - Explicit Signatures for Shortcuts (`#2240`_) **New Features:** - Add Missing Shortcuts to ``Message`` (`#2330`_) - Rich Comparison for ``Bot`` (`#2320`_) - Add ``run_async`` Parameter to ``ConversationHandler`` (`#2292`_) - Add New Shortcuts to ``Chat`` (`#2291`_) - Add New Constant ``MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH`` (`#2282`_) - Allow Passing Custom Filename For All Media (`#2249`_) - Handle Bytes as File Input (`#2233`_) **Bug Fixes:** - Fix Escaping in Nested Entities in ``Message`` Properties (`#2312`_) - Adjust Calling of ``Dispatcher.update_persistence`` (`#2285`_) - Add ``quote`` kwarg to ``Message.reply_copy`` (`#2232`_) - ``ConversationHandler``: Docs & ``edited_channel_post`` behavior (`#2339`_) **Minor changes, CI improvements, doc fixes and type hinting:** - Doc Fixes (`#2253`_, `#2225`_) - Reduce Usage of ``typing.Any`` (`#2321`_) - Extend Deeplinking Example (`#2335`_) - Add pyupgrade to pre-commit Hooks (`#2301`_) - Add PR Template (`#2299`_) - Drop Nightly Tests & Update Badges (`#2323`_) - Update Copyright (`#2289`_, `#2287`_) - Change Order of Class DocStrings (`#2256`_) - Add macOS to Test Matrix (`#2266`_) - Start Using Versioning Directives in Docs (`#2252`_) - Improve Annotations & Docs of Handlers (`#2243`_) .. _`#2324`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2324 .. _`#2240`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2240 .. _`#2330`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2330 .. _`#2320`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2320 .. _`#2292`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2292 .. _`#2291`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2291 .. _`#2282`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2282 .. _`#2249`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2249 .. _`#2233`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2233 .. _`#2312`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2312 .. _`#2285`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2285 .. _`#2232`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2232 .. _`#2339`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2339 .. _`#2253`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2253 .. _`#2225`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2225 .. _`#2321`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2321 .. _`#2335`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2335 .. _`#2301`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2301 .. _`#2299`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2299 .. _`#2323`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2323 .. _`#2289`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2289 .. _`#2287`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2287 .. _`#2256`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2256 .. _`#2266`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2266 .. _`#2252`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2252 .. _`#2243`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2243 Version 13.1 ============ *Released 2020-11-29* **Major Changes:** - Full support of Bot API 5.0 (`#2181`_, `#2186`_, `#2190`_, `#2189`_, `#2183`_, `#2184`_, `#2188`_, `#2185`_, `#2192`_, `#2196`_, `#2193`_, `#2223`_, `#2199`_, `#2187`_, `#2147`_, `#2205`_) **New Features:** - Add ``Defaults.run_async`` (`#2210`_) - Improve and Expand ``CallbackQuery`` Shortcuts (`#2172`_) - Add XOR Filters and make ``Filters.name`` a Property (`#2179`_) - Add ``Filters.document.file_extension`` (`#2169`_) - Add ``Filters.caption_regex`` (`#2163`_) - Add ``Filters.chat_type`` (`#2128`_) - Handle Non-Binary File Input (`#2202`_) **Bug Fixes:** - Improve Handling of Custom Objects in ``BasePersistence.insert``/``replace_bot`` (`#2151`_) - Fix bugs in ``replace/insert_bot`` (`#2218`_) **Minor changes, CI improvements, doc fixes and type hinting:** - Improve Type hinting (`#2204`_, `#2118`_, `#2167`_, `#2136`_) - Doc Fixes & Extensions (`#2201`_, `#2161`_) - Use F-Strings Where Possible (`#2222`_) - Rename kwargs to _kwargs where possible (`#2182`_) - Comply with PEP561 (`#2168`_) - Improve Code Quality (`#2131`_) - Switch Code Formatting to Black (`#2122`_, `#2159`_, `#2158`_) - Update Wheel Settings (`#2142`_) - Update ``timerbot.py`` to ``v13.0`` (`#2149`_) - Overhaul Constants (`#2137`_) - Add Python 3.9 to Test Matrix (`#2132`_) - Switch Codecov to ``GitHub`` Action (`#2127`_) - Specify Required pytz Version (`#2121`_) .. _`#2181`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2181 .. _`#2186`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2186 .. _`#2190`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2190 .. _`#2189`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2189 .. _`#2183`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2183 .. _`#2184`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2184 .. _`#2188`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2188 .. _`#2185`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2185 .. _`#2192`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2192 .. _`#2196`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2196 .. _`#2193`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2193 .. _`#2223`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2223 .. _`#2199`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2199 .. _`#2187`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2187 .. _`#2147`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2147 .. _`#2205`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2205 .. _`#2210`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2210 .. _`#2172`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2172 .. _`#2179`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2179 .. _`#2169`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2169 .. _`#2163`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2163 .. _`#2128`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2128 .. _`#2202`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2202 .. _`#2151`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2151 .. _`#2218`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2218 .. _`#2204`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2204 .. _`#2118`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2118 .. _`#2167`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2167 .. _`#2136`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2136 .. _`#2201`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2201 .. _`#2161`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2161 .. _`#2222`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2222 .. _`#2182`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2182 .. _`#2168`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2168 .. _`#2131`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2131 .. _`#2122`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2122 .. _`#2159`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2159 .. _`#2158`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2158 .. _`#2142`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2142 .. _`#2149`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2149 .. _`#2137`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2137 .. _`#2132`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2132 .. _`#2127`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2127 .. _`#2121`: https://github.com/python-telegram-bot/python-telegram-bot/pull/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 (`#2050`_) - Refactor Handling of Message VS Update Filters (`#2032`_) - Deprecate ``Message.default_quote`` (`#1965`_) - Refactor persistence of Bot instances (`#1994`_) - Refactor ``JobQueue`` (`#1981`_) - Refactor handling of kwargs in Bot methods (`#1924`_) - Refactor ``Dispatcher.run_async``, deprecating the ``@run_async`` decorator (`#2051`_) **New Features:** - Type Hinting (`#1920`_) - Automatic Pagination for ``answer_inline_query`` (`#2072`_) - ``Defaults.tzinfo`` (`#2042`_) - Extend rich comparison of objects (`#1724`_) - Add ``Filters.via_bot`` (`#2009`_) - Add missing shortcuts (`#2043`_) - Allow ``DispatcherHandlerStop`` in ``ConversationHandler`` (`#2059`_) - Make Errors picklable (`#2106`_) **Minor changes, CI improvements, doc fixes or bug fixes:** - Fix Webhook not working on Windows with Python 3.8+ (`#2067`_) - Fix setting thumbs with ``send_media_group`` (`#2093`_) - Make ``MessageHandler`` filter for ``Filters.update`` first (`#2085`_) - Fix ``PicklePersistence.flush()`` with only ``bot_data`` (`#2017`_) - Add test for clean argument of ``Updater.start_polling/webhook`` (`#2002`_) - Doc fixes, refinements and additions (`#2005`_, `#2008`_, `#2089`_, `#2094`_, `#2090`_) - CI fixes (`#2018`_, `#2061`_) - Refine ``pollbot.py`` example (`#2047`_) - Refine Filters in examples (`#2027`_) - Rename ``echobot`` examples (`#2025`_) - Use Lock-Bot to lock old threads (`#2048`_, `#2052`_, `#2049`_, `#2053`_) .. _`#2050`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2050 .. _`#2032`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2032 .. _`#1965`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1965 .. _`#1994`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1994 .. _`#1981`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1981 .. _`#1924`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1924 .. _`#2051`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2051 .. _`#1920`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1920 .. _`#2072`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2072 .. _`#2042`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2042 .. _`#1724`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1724 .. _`#2009`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2009 .. _`#2043`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2043 .. _`#2059`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2059 .. _`#2106`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2106 .. _`#2067`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2067 .. _`#2093`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2093 .. _`#2085`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2085 .. _`#2017`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2017 .. _`#2002`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2002 .. _`#2005`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2005 .. _`#2008`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2008 .. _`#2089`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2089 .. _`#2094`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2094 .. _`#2090`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2090 .. _`#2018`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2018 .. _`#2061`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2061 .. _`#2047`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2047 .. _`#2027`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2027 .. _`#2025`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2025 .. _`#2048`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2048 .. _`#2052`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2052 .. _`#2049`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2049 .. _`#2053`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2053 Version 12.8 ============ *Released 2020-06-22* **Major Changes:** - Remove Python 2 support (`#1715`_) - Bot API 4.9 support (`#1980`_) - IDs/Usernames of ``Filters.user`` and ``Filters.chat`` can now be updated (`#1757`_) **Minor changes, CI improvements, doc fixes or bug fixes:** - Update contribution guide and stale bot (`#1937`_) - Remove ``NullHandlers`` (`#1913`_) - Improve and expand examples (`#1943`_, `#1995`_, `#1983`_, `#1997`_) - Doc fixes (`#1940`_, `#1962`_) - Add ``User.send_poll()`` shortcut (`#1968`_) - Ignore private attributes en ``TelegramObject.to_dict()`` (`#1989`_) - Stabilize CI (`#2000`_) .. _`#1937`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1937 .. _`#1913`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1913 .. _`#1943`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1943 .. _`#1757`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1757 .. _`#1940`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1940 .. _`#1962`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1962 .. _`#1968`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1968 .. _`#1989`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1989 .. _`#1995`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1995 .. _`#1983`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1983 .. _`#1715`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1715 .. _`#2000`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2000 .. _`#1997`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1997 .. _`#1980`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1980 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. (`#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`` (`#1621`_) **New Features:** - New method ``run_monthly`` for the ``JobQueue`` (`#1705`_) - ``Job.next_t`` now gives the datetime of the jobs next execution (`#1685`_) **Minor changes, CI improvements, doc fixes or bug fixes:** - Stabalize CI (`#1919`_, `#1931`_) - Use ABCs ``@abstractmethod`` instead of raising ``NotImplementedError`` for ``Handler``, ``BasePersistence`` and ``BaseFilter`` (`#1905`_) - Doc fixes (`#1914`_, `#1902`_, `#1910`_) .. _`#1902`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1902 .. _`#1685`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1685 .. _`#1910`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1910 .. _`#1914`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1914 .. _`#1931`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1931 .. _`#1905`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1905 .. _`#1919`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1919 .. _`#1621`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1621 .. _`#1705`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1705 .. _`#1917`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1917 Version 12.6.1 ============== *Released 2020-04-11* **Bug fixes:** - Fix serialization of ``reply_markup`` in media messages (`#1889`_) .. _`#1889`: https://github.com/python-telegram-bot/python-telegram-bot/pull/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. (`#1858`_) **Minor changes, CI improvements or bug fixes:** - Add tests for ``swtich_inline_query(_current_chat)`` with empty string (`#1635`_) - Doc fixes (`#1854`_, `#1874`_, `#1884`_) - Update issue templates (`#1880`_) - Favor concrete types over "Iterable" (`#1882`_) - Pass last valid ``CallbackContext`` to ``TIMEOUT`` handlers of ``ConversationHandler`` (`#1826`_) - Tweak handling of persistence and update persistence after job calls (`#1827`_) - Use checkout@v2 for GitHub actions (`#1887`_) .. _`#1858`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1858 .. _`#1635`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1635 .. _`#1854`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1854 .. _`#1874`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1874 .. _`#1884`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1884 .. _`#1880`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1880 .. _`#1882`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1882 .. _`#1826`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1826 .. _`#1827`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1827 .. _`#1887`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1887 Version 12.5.1 ============== *Released 2020-03-30* **Minor changes, doc fixes or bug fixes:** - Add missing docs for `PollHandler` and `PollAnswerHandler` (`#1853`_) - Fix wording in `Filters` docs (`#1855`_) - Reorder tests to make them more stable (`#1835`_) - Make `ConversationHandler` attributes immutable (`#1756`_) - Make `PrefixHandler` attributes `command` and `prefix` editable (`#1636`_) - Fix UTC as default `tzinfo` for `Job` (`#1696`_) .. _`#1853`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1853 .. _`#1855`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1855 .. _`#1835`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1835 .. _`#1756`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1756 .. _`#1636`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1636 .. _`#1696`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1696 Version 12.5 ============ *Released 2020-03-29* **New Features:** - `Bot.link` gives the `t.me` link of the bot (`#1770`_) **Major Changes:** - Bot API 4.5 and 4.6 support. (`#1508`_, `#1723`_) **Minor changes, CI improvements or bug fixes:** - Remove legacy CI files (`#1783`_, `#1791`_) - Update pre-commit config file (`#1787`_) - Remove builtin names (`#1792`_) - CI improvements (`#1808`_, `#1848`_) - Support Python 3.8 (`#1614`_, `#1824`_) - Use stale bot for auto closing stale issues (`#1820`_, `#1829`_, `#1840`_) - Doc fixes (`#1778`_, `#1818`_) - Fix typo in `edit_message_media` (`#1779`_) - In examples, answer CallbackQueries and use `edit_message_text` shortcut (`#1721`_) - Revert accidental change in vendored urllib3 (`#1775`_) .. _`#1783`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1783 .. _`#1787`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1787 .. _`#1792`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1792 .. _`#1791`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1791 .. _`#1808`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1808 .. _`#1614`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1614 .. _`#1770`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1770 .. _`#1824`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1824 .. _`#1820`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1820 .. _`#1829`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1829 .. _`#1840`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1840 .. _`#1778`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1778 .. _`#1779`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1779 .. _`#1721`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1721 .. _`#1775`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1775 .. _`#1848`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1848 .. _`#1818`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1818 .. _`#1508`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1508 .. _`#1723`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1723 Version 12.4.2 ============== *Released 2020-02-10* **Bug Fixes** - Pass correct parse_mode to InlineResults if bot.defaults is None (`#1763`_) - Make sure PP can read files that dont have bot_data (`#1760`_) .. _`#1763`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1763 .. _`#1760`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1760 Version 12.4.1 ============== *Released 2020-02-08* This is a quick release for `#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`_. (`#1490`_) - Store data in ``CallbackContext.bot_data`` to access it in every callback. Also persists. (`#1325`_) - ``Filters.poll`` allows only messages containing a poll (`#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 (`#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 (`#1744`_). **Minor changes, CI improvements or bug fixes:** - Add ``disptacher`` argument to ``Updater`` to allow passing a customized ``Dispatcher`` (`#1484`_) - Add missing names for ``Filters`` (`#1632`_) - Documentation fixes (`#1624`_, `#1647`_, `#1669`_, `#1703`_, `#1718`_, `#1734`_, `#1740`_, `#1642`_, `#1739`_, `#1746`_) - CI improvements (`#1716`_, `#1731`_, `#1738`_, `#1748`_, `#1749`_, `#1750`_, `#1752`_) - Fix spelling issue for ``encode_conversations_to_json`` (`#1661`_) - Remove double assignement of ``Dispatcher.job_queue`` (`#1698`_) - Expose dispatcher as property for ``CallbackContext`` (`#1684`_) - Fix ``None`` check in ``JobQueue._put()`` (`#1707`_) - Log datetimes correctly in ``JobQueue`` (`#1714`_) - Fix false ``Message.link`` creation for private groups (`#1741`_) - Add option ``--with-upstream-urllib3`` to `setup.py` to allow using non-vendored version (`#1725`_) - Fix persistence for nested ``ConversationHandlers`` (`#1679`_) - Improve handling of non-decodable server responses (`#1623`_) - Fix download for files without ``file_path`` (`#1591`_) - test_webhook_invalid_posts is now considered flaky and retried on failure (`#1758`_) .. _`wiki page for the new defaults`: https://github.com/python-telegram-bot/python-telegram-bot/wiki/Adding-defaults-to-your-bot .. _`#1744`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1744 .. _`#1752`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1752 .. _`#1750`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1750 .. _`#1591`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1591 .. _`#1490`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1490 .. _`#1749`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1749 .. _`#1623`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1623 .. _`#1748`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1748 .. _`#1679`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1679 .. _`#1711`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1711 .. _`#1325`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1325 .. _`#1746`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1746 .. _`#1725`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1725 .. _`#1739`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1739 .. _`#1741`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1741 .. _`#1642`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1642 .. _`#1738`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1738 .. _`#1740`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1740 .. _`#1734`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1734 .. _`#1680`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1680 .. _`#1718`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1718 .. _`#1714`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1714 .. _`#1707`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1707 .. _`#1731`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1731 .. _`#1673`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1673 .. _`#1684`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1684 .. _`#1703`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1703 .. _`#1698`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1698 .. _`#1669`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1669 .. _`#1661`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1661 .. _`#1647`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1647 .. _`#1632`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1632 .. _`#1624`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1624 .. _`#1716`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1716 .. _`#1484`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1484 .. _`#1758`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1484 Version 12.3.0 ============== *Released 2020-01-11* **New features:** - `Filters.caption` allows only messages with caption (`#1631`_). - Filter for exact messages/captions with new capability of `Filters.text` and `Filters.caption`. Especially useful in combination with ReplyKeyboardMarkup. (`#1631`_). **Major changes:** - Fix inconsistent handling of naive datetimes (`#1506`_). **Minor changes, CI improvements or bug fixes:** - Documentation fixes (`#1558`_, `#1569`_, `#1579`_, `#1572`_, `#1566`_, `#1577`_, `#1656`_). - Add mutex protection on `ConversationHandler` (`#1533`_). - Add `MAX_PHOTOSIZE_UPLOAD` constant (`#1560`_). - Add args and kwargs to `Message.forward()` (`#1574`_). - Transfer to GitHub Actions CI (`#1555`_, `#1556`_, `#1605`_, `#1606`_, `#1607`_, `#1612`_, `#1615`_, `#1645`_). - Fix deprecation warning with Py3.8 by vendored urllib3 (`#1618`_). - Simplify assignements for optional arguments (`#1600`_) - Allow private groups for `Message.link` (`#1619`_). - Fix wrong signature call for `ConversationHandler.TIMEOUT` handlers (`#1653`_). .. _`#1631`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1631 .. _`#1506`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1506 .. _`#1558`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1558 .. _`#1569`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1569 .. _`#1579`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1579 .. _`#1572`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1572 .. _`#1566`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1566 .. _`#1577`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1577 .. _`#1533`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1533 .. _`#1560`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1560 .. _`#1574`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1574 .. _`#1555`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1555 .. _`#1556`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1556 .. _`#1605`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1605 .. _`#1606`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1606 .. _`#1607`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1607 .. _`#1612`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1612 .. _`#1615`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1615 .. _`#1618`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1618 .. _`#1600`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1600 .. _`#1619`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1619 .. _`#1653`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1653 .. _`#1656`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1656 .. _`#1645`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1645 Version 12.2.0 ============== *Released 2019-10-14* **New features:** - Nested ConversationHandlers (`#1512`_). **Minor changes, CI improvments or bug fixes:** - Fix CI failures due to non-backward compat attrs depndency (`#1540`_). - travis.yaml: TEST_OFFICIAL removed from allowed_failures. - Fix typos in examples (`#1537`_). - Fix Bot.to_dict to use proper first_name (`#1525`_). - Refactor ``test_commandhandler.py`` (`#1408`_). - Add Python 3.8 (RC version) to Travis testing matrix (`#1543`_). - test_bot.py: Add to_dict test (`#1544`_). - Flake config moved into setup.cfg (`#1546`_). .. _`#1512`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1512 .. _`#1540`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1540 .. _`#1537`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1537 .. _`#1525`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1525 .. _`#1408`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1408 .. _`#1543`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1543 .. _`#1544`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1544 .. _`#1546`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1546 Version 12.1.1 ============== *Released 2019-09-18* **Hot fix release** Fixed regression in the vendored urllib3 (`#1517`_). .. _`#1517`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1517 Version 12.1.0 ================ *Released 2019-09-13* **Major changes:** - Bot API 4.4 support (`#1464`_, `#1510`_) - Add `get_file` method to `Animation` & `ChatPhoto`. Add, `get_small_file` & `get_big_file` methods to `ChatPhoto` (`#1489`_) - Tools for deep linking (`#1049`_) **Minor changes and/or bug fixes:** - Documentation fixes (`#1500`_, `#1499`_) - Improved examples (`#1502`_) .. _`#1464`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1464 .. _`#1502`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1502 .. _`#1499`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1499 .. _`#1500`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1500 .. _`#1049`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1049 .. _`#1489`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1489 .. _`#1510`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1510 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 (`#1485`_) - Return UTC from from_timestamp() (`#1485`_) **See the wiki page at https://git.io/fxJuV for a detailed guide on how to migrate from version 11 to version 12.** Context based callbacks (`#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. (`#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 (`#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 (`#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 (`#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 (`#1344`_) - Replace with ``WAITING`` constant and behavior from states (`#1344`_) - Only emit one warning for multiple CallbackQueryHandlers in a ConversationHandler (`#1319`_) - Use warnings.warn for ConversationHandler warnings (`#1343`_) - Fix unresolvable promises (`#1270`_) Bug fixes & improvements ------------------------ - Handlers should be faster due to deduped logic. - Avoid compiling compiled regex in regex filter. (`#1314`_) - Add missing ``left_chat_member`` to Message.MESSAGE_TYPES (`#1336`_) - Make custom timeouts actually work properly (`#1330`_) - Add convenience classmethods (from_button, from_row and from_column) to InlineKeyboardMarkup - Small typo fix in setup.py (`#1306`_) - Add Conflict error (HTTP error code 409) (`#1154`_) - Change MAX_CAPTION_LENGTH to 1024 (`#1262`_) - Remove some unnecessary clauses (`#1247`_, `#1239`_) - Allow filenames without dots in them when sending files (`#1228`_) - Fix uploading files with unicode filenames (`#1214`_) - Replace http.server with Tornado (`#1191`_) - Allow SOCKSConnection to parse username and password from URL (`#1211`_) - Fix for arguments in passport/data.py (`#1213`_) - Improve message entity parsing by adding text_mention (`#1206`_) - Documentation fixes (`#1348`_, `#1397`_, `#1436`_) - Merged filters short-circuit (`#1350`_) - Fix webhook listen with tornado (`#1383`_) - Call task_done() on update queue after update processing finished (`#1428`_) - Fix send_location() - latitude may be 0 (`#1437`_) - Make MessageEntity objects comparable (`#1465`_) - Add prefix to thread names (`#1358`_) Buf fixes since v12.0.0b1 ------------------------- - Fix setting bot on ShippingQuery (`#1355`_) - Fix _trigger_timeout() missing 1 required positional argument: 'job' (`#1367`_) - Add missing message.text check in PrefixHandler check_update (`#1375`_) - Make updates persist even on DispatcherHandlerStop (`#1463`_) - Dispatcher force updating persistence object's chat data attribute(`#1462`_) .. _`#1100`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1100 .. _`#1283`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1283 .. _`#1017`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1017 .. _`#1325`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1325 .. _`#1301`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1301 .. _`#1312`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1312 .. _`#1324`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1324 .. _`#1114`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1114 .. _`#1221`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1221 .. _`#1314`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1314 .. _`#1336`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1336 .. _`#1330`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1330 .. _`#1306`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1306 .. _`#1154`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1154 .. _`#1262`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1262 .. _`#1247`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1247 .. _`#1239`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1239 .. _`#1228`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1228 .. _`#1214`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1214 .. _`#1191`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1191 .. _`#1211`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1211 .. _`#1213`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1213 .. _`#1206`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1206 .. _`#1344`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1344 .. _`#1319`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1319 .. _`#1343`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1343 .. _`#1270`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1270 .. _`#1348`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1348 .. _`#1350`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1350 .. _`#1383`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1383 .. _`#1397`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1397 .. _`#1428`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1428 .. _`#1436`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1436 .. _`#1437`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1437 .. _`#1465`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1465 .. _`#1358`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1358 .. _`#1355`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1355 .. _`#1367`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1367 .. _`#1375`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1375 .. _`#1463`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1463 .. _`#1462`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1462 .. _`#1483`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1483 .. _`#1485`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1485 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: (`#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 .. _`#1198`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1198 Version 11.0.0 ============== *Released 2018-08-29* Fully support Bot API version 4.0! (also some bugfixes :)) Telegram Passport (`#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 (`#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. (`#1170`_) - Add vCard support by adding vcard field to Contact, InlineQueryResultContact, InputContactMessageContent, and Bot.send_contact. (`#1166`_) - Support new message entities: CASHTAG and PHONE_NUMBER. (`#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. (`#1172`_) Non Bot API 4.0 changes: - Minor integer comparison fix (`#1147`_) - Fix Filters.regex failing on non-text message (`#1158`_) - Fix ProcessLookupError if process finishes before we kill it (`#1126`_) - Add t.me links for User, Chat and Message if available and update User.mention_* (`#1092`_) - Fix mention_markdown/html on py2 (`#1112`_) .. _`#1092`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1092 .. _`#1112`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1112 .. _`#1126`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1126 .. _`#1147`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1147 .. _`#1158`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1158 .. _`#1166`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1166 .. _`#1170`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1170 .. _`#1174`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1174 .. _`#1172`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1172 .. _`#1179`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1179 .. _`#1184`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1184 .. _`our telegram passport wiki page`: https://git.io/fAvYd Version 10.1.0 ============== *Released 2018-05-02* Fixes changing previous behaviour: - Add urllib3 fix for socks5h support (`#1085`_) - Fix send_sticker() timeout=20 (`#1088`_) Fixes: - Add a caption_entity filter for filtering caption entities (`#1068`_) - Inputfile encode filenames (`#1086`_) - InputFile: Fix proper naming of file when reading from subprocess.PIPE (`#1079`_) - Remove pytest-catchlog from requirements (`#1099`_) - Documentation fixes (`#1061`_, `#1078`_, `#1081`_, `#1096`_) .. _`#1061`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1061 .. _`#1068`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1068 .. _`#1078`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1078 .. _`#1079`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1079 .. _`#1081`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1081 .. _`#1085`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1085 .. _`#1086`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1086 .. _`#1088`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1088 .. _`#1096`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1096 .. _`#1099`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1099 Version 10.0.2 ============== *Released 2018-04-17* Important fix: - Handle utf8 decoding errors (`#1076`_) New features: - Added Filter.regex (`#1028`_) - Filters for Category and file types (`#1046`_) - Added video note filter (`#1067`_) Fixes: - Fix in telegram.Message (`#1042`_) - Make chat_id a positional argument inside shortcut methods of Chat and User classes (`#1050`_) - Make Bot.full_name return a unicode object. (`#1063`_) - CommandHandler faster check (`#1074`_) - Correct documentation of Dispatcher.add_handler (`#1071`_) - Various small fixes to documentation. .. _`#1028`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1028 .. _`#1042`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1042 .. _`#1046`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1046 .. _`#1050`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1050 .. _`#1067`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1067 .. _`#1063`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1063 .. _`#1074`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1074 .. _`#1076`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1076 .. _`#1071`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1071 Version 10.0.1 ============== *Released 2018-03-05* Fixes: - Fix conversationhandler timeout (PR `#1032`_) - Add missing docs utils (PR `#912`_) .. _`#1032`: https://github.com/python-telegram-bot/python-telegram-bot/pull/826 .. _`#912`: https://github.com/python-telegram-bot/python-telegram-bot/pull/826 Version 10.0.0 ============== *Released 2018-03-02* Non backward compatabile changes and changed defaults - JobQueue: Remove deprecated prevent_autostart & put() (PR `#1012`_) - Bot, Updater: Remove deprecated network_delay (PR `#1012`_) - Remove deprecated Message.new_chat_member (PR `#1012`_) - Retry bootstrap phase indefinitely (by default) on network errors (PR `#1018`_) New Features - Support v3.6 API (PR `#1006`_) - User.full_name convinience property (PR `#949`_) - Add `send_phone_number_to_provider` and `send_email_to_provider` arguments to send_invoice (PR `#986`_) - Bot: Add shortcut methods reply_{markdown,html} (PR `#827`_) - Bot: Add shortcut method reply_media_group (PR `#994`_) - Added utils.helpers.effective_message_type (PR `#826`_) - Bot.get_file now allows passing a file in addition to file_id (PR `#963`_) - Add .get_file() to Audio, Document, PhotoSize, Sticker, Video, VideoNote and Voice (PR `#963`_) - Add .send_*() methods to User and Chat (PR `#963`_) - Get jobs by name (PR `#1011`_) - Add Message caption html/markdown methods (PR `#1013`_) - File.download_as_bytearray - new method to get a d/led file as bytearray (PR `#1019`_) - File.download(): Now returns a meaningful return value (PR `#1019`_) - Added conversation timeout in ConversationHandler (PR `#895`_) Changes - Store bot in PreCheckoutQuery (PR `#953`_) - Updater: Issue INFO log upon received signal (PR `#951`_) - JobQueue: Thread safety fixes (PR `#977`_) - WebhookHandler: Fix exception thrown during error handling (PR `#985`_) - Explicitly check update.effective_chat in ConversationHandler.check_update (PR `#959`_) - Updater: Better handling of timeouts during get_updates (PR `#1007`_) - Remove unnecessary to_dict() (PR `#834`_) - CommandHandler - ignore strings in entities and "/" followed by whitespace (PR `#1020`_) - Documentation & style fixes (PR `#942`_, PR `#956`_, PR `#962`_, PR `#980`_, PR `#983`_) .. _`#826`: https://github.com/python-telegram-bot/python-telegram-bot/pull/826 .. _`#827`: https://github.com/python-telegram-bot/python-telegram-bot/pull/827 .. _`#834`: https://github.com/python-telegram-bot/python-telegram-bot/pull/834 .. _`#895`: https://github.com/python-telegram-bot/python-telegram-bot/pull/895 .. _`#942`: https://github.com/python-telegram-bot/python-telegram-bot/pull/942 .. _`#949`: https://github.com/python-telegram-bot/python-telegram-bot/pull/949 .. _`#951`: https://github.com/python-telegram-bot/python-telegram-bot/pull/951 .. _`#956`: https://github.com/python-telegram-bot/python-telegram-bot/pull/956 .. _`#953`: https://github.com/python-telegram-bot/python-telegram-bot/pull/953 .. _`#962`: https://github.com/python-telegram-bot/python-telegram-bot/pull/962 .. _`#959`: https://github.com/python-telegram-bot/python-telegram-bot/pull/959 .. _`#963`: https://github.com/python-telegram-bot/python-telegram-bot/pull/963 .. _`#977`: https://github.com/python-telegram-bot/python-telegram-bot/pull/977 .. _`#980`: https://github.com/python-telegram-bot/python-telegram-bot/pull/980 .. _`#983`: https://github.com/python-telegram-bot/python-telegram-bot/pull/983 .. _`#985`: https://github.com/python-telegram-bot/python-telegram-bot/pull/985 .. _`#986`: https://github.com/python-telegram-bot/python-telegram-bot/pull/986 .. _`#994`: https://github.com/python-telegram-bot/python-telegram-bot/pull/994 .. _`#1006`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1006 .. _`#1007`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1007 .. _`#1011`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1011 .. _`#1012`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1012 .. _`#1013`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1013 .. _`#1018`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1018 .. _`#1019`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1019 .. _`#1020`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1020 Version 9.0.0 ============= *Released 2017-12-08* Breaking changes (possibly) - Drop support for python 3.3 (PR `#930`_) New Features - Support Bot API 3.5 (PR `#920`_) Changes - Fix race condition in dispatcher start/stop (`#887`_) - Log error trace if there is no error handler registered (`#694`_) - Update examples with consistent string formatting (`#870`_) - Various changes and improvements to the docs. .. _`#920`: https://github.com/python-telegram-bot/python-telegram-bot/pull/920 .. _`#930`: https://github.com/python-telegram-bot/python-telegram-bot/pull/930 .. _`#887`: https://github.com/python-telegram-bot/python-telegram-bot/pull/887 .. _`#694`: https://github.com/python-telegram-bot/python-telegram-bot/pull/694 .. _`#870`: https://github.com/python-telegram-bot/python-telegram-bot/pull/870 Version 8.1.1 ============= *Released 2017-10-15* - Fix Commandhandler crashing on single character messages (PR `#873`_). .. _`#873`: https://github.com/python-telegram-bot/python-telegram-bot/pull/871 Version 8.1.0 ============= *Released 2017-10-14* New features - Support Bot API 3.4 (PR `#865`_). Changes - MessageHandler & RegexHandler now consider channel_updates. - Fix command not recognized if it is directly followed by a newline (PR `#869`_). - Removed Bot._message_wrapper (PR `#822`_). - Unitests are now also running on AppVeyor (Windows VM). - Various unitest improvements. - Documentation fixes. .. _`#822`: https://github.com/python-telegram-bot/python-telegram-bot/pull/822 .. _`#865`: https://github.com/python-telegram-bot/python-telegram-bot/pull/865 .. _`#869`: https://github.com/python-telegram-bot/python-telegram-bot/pull/869 Version 8.0.0 ============= *Released 2017-09-01* New features - Fully support Bot Api 3.3 (PR `#806`_). - DispatcherHandlerStop (`see docs`_). - Regression fix for text_html & text_markdown (PR `#777`_). - Added effective_attachment to message (PR `#766`_). Non backward compatible changes - Removed Botan support from the library (PR `#776`_). - Fully support Bot Api 3.3 (PR `#806`_). - Remove de_json() (PR `#789`_). Changes - Sane defaults for tcp socket options on linux (PR `#754`_). - Add RESTRICTED as constant to ChatMember (PR `#761`_). - Add rich comparison to CallbackQuery (PR `#764`_). - Fix get_game_high_scores (PR `#771`_). - Warn on small con_pool_size during custom initalization of Updater (PR `#793`_). - Catch exceptions in error handlerfor errors that happen during polling (PR `#810`_). - For testing we switched to pytest (PR `#788`_). - Lots of small improvements to our tests and documentation. .. _`see docs`: http://python-telegram-bot.readthedocs.io/en/stable/telegram.ext.dispatcher.html#telegram.ext.Dispatcher.add_handler .. _`#777`: https://github.com/python-telegram-bot/python-telegram-bot/pull/777 .. _`#806`: https://github.com/python-telegram-bot/python-telegram-bot/pull/806 .. _`#766`: https://github.com/python-telegram-bot/python-telegram-bot/pull/766 .. _`#776`: https://github.com/python-telegram-bot/python-telegram-bot/pull/776 .. _`#789`: https://github.com/python-telegram-bot/python-telegram-bot/pull/789 .. _`#754`: https://github.com/python-telegram-bot/python-telegram-bot/pull/754 .. _`#761`: https://github.com/python-telegram-bot/python-telegram-bot/pull/761 .. _`#764`: https://github.com/python-telegram-bot/python-telegram-bot/pull/764 .. _`#771`: https://github.com/python-telegram-bot/python-telegram-bot/pull/771 .. _`#788`: https://github.com/python-telegram-bot/python-telegram-bot/pull/788 .. _`#793`: https://github.com/python-telegram-bot/python-telegram-bot/pull/793 .. _`#810`: https://github.com/python-telegram-bot/python-telegram-bot/pull/810 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`` - https://github.com/python-telegram-bot/python-telegram-bot/pull/484 - Download files into file-like objects - https://github.com/python-telegram-bot/python-telegram-bot/pull/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`` - https://github.com/python-telegram-bot/python-telegram-bot/pull/507 - Add support for Socks5 proxy - https://github.com/python-telegram-bot/python-telegram-bot/pull/518 - Add support for filters in ``CommandHandler`` - https://github.com/python-telegram-bot/python-telegram-bot/pull/536 - Add the ability to invert (not) filters - https://github.com/python-telegram-bot/python-telegram-bot/pull/552 - Add ``Filters.group`` and ``Filters.private`` - Compatibility with GAE via ``urllib3.contrib`` package - https://github.com/python-telegram-bot/python-telegram-bot/pull/583 - Add equality rich comparision operators to telegram objects - https://github.com/python-telegram-bot/python-telegram-bot/pull/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`` - https://github.com/python-telegram-bot/python-telegram-bot/pull/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-13.11/CODE_OF_CONDUCT.md000066400000000000000000000064151417656324400202410ustar00rootroot00000000000000# 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][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ python-telegram-bot-13.11/LICENSE000066400000000000000000000772461417656324400164610ustar00rootroot00000000000000 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-13.11/LICENSE.dual000066400000000000000000001164001417656324400173670ustar00rootroot00000000000000 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-13.11/LICENSE.lesser000066400000000000000000000167431417656324400177500ustar00rootroot00000000000000 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-13.11/MANIFEST.in000066400000000000000000000001321417656324400171660ustar00rootroot00000000000000include LICENSE LICENSE.lesser Makefile requirements.txt README_RAW.rst telegram/py.typed python-telegram-bot-13.11/README.rst000066400000000000000000000227251417656324400171330ustar00rootroot00000000000000.. Make sure to apply any changes to this file to README_RAW.rst as well! .. image:: https://github.com/python-telegram-bot/logos/blob/master/logo-text/png/ptb-logo-text_768.png?raw=true :align: center :target: https://python-telegram-bot.org :alt: python-telegram-bot Logo 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 `_. .. 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-5.7-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://python-telegram-bot.readthedocs.io/en/stable/?badge=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/workflows/GitHub%20Actions/badge.svg :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://codecov.io/gh/python-telegram-bot/python-telegram-bot :alt: Code coverage .. image:: http://isitmaintained.com/badge/resolution/python-telegram-bot/python-telegram-bot.svg :target: http://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://www.codacy.com/app/python-telegram-bot/python-telegram-bot?utm_source=github.com&utm_medium=referral&utm_content=python-telegram-bot/python-telegram-bot&utm_campaign=Badge_Grade :alt: Code quality: Codacy .. image:: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot.svg/?label=active+issues :target: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot/?ref=repository-badge :alt: Code quality: DeepSource .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black .. image:: https://img.shields.io/badge/Telegram-Group-blue.svg?logo=telegram :target: https://telegram.me/pythontelegrambotgroup :alt: Telegram Group ================= Table of contents ================= - `Introduction`_ - `Telegram API support`_ - `Installing`_ - `Getting started`_ #. `Learning by example`_ #. `Logging`_ #. `Documentation`_ - `Getting help`_ - `Contributing`_ - `License`_ ============ Introduction ============ This library provides a pure Python interface for the `Telegram Bot API `_. It's compatible with Python versions 3.6.8+. PTB might also work on `PyPy `_, though there have been a lot of issues before. Hence, PyPy is not officially supported. 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 **5.7** are supported. ========== Installing ========== You can install or upgrade python-telegram-bot with: .. code:: shell $ pip install python-telegram-bot --upgrade Or you can install from source with: .. code:: shell $ git clone https://github.com/python-telegram-bot/python-telegram-bot --recursive $ cd python-telegram-bot $ python setup.py install In case you have a previously cloned local repository already, you should initialize the added urllib3 submodule before installing with: .. code:: shell $ git submodule update --init --recursive --------------------- Optional Dependencies --------------------- PTB can be installed with optional dependencies: * ``pip install python-telegram-bot[passport]`` installs the `cryptography `_ library. Use this, if you want to use Telegram Passport related functionality. * ``pip install python-telegram-bot[ujson]`` installs the `ujson `_ library. It will then be used for JSON de- & encoding, which can bring speed up compared to the standard `json `_ library. * ``pip install python-telegram-bot[socks]`` installs the `PySocks `_ library. Use this, if you want to work behind a Socks5 server. =============== Getting started =============== Our Wiki contains a lot of resources to get you started with ``python-telegram-bot``: - `Introduction to the API `_ - Tutorial: `Your first Bot `_ Other references: - `Telegram API documentation `_ - `python-telegram-bot documentation `_ ------------------- Learning by example ------------------- We believe that the best way to learn this package is by example. Here are some examples for you to review. 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. Best of all, the code for these examples are released to the public domain, so you can start by grabbing the code and building on top of it. Visit `this page `_ to discover the official examples or look at the examples on the `wiki `_ to see other bots the community has built. ------- Logging ------- This library uses the ``logging`` module. To set up logging to standard output, put: .. code:: python import logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') at the beginning of your script. You can also use logs in your application by calling ``logging.getLogger()`` and setting the log level you want: .. code:: python logger = logging.getLogger() logger.setLevel(logging.INFO) If you want DEBUG logs instead: .. code:: python logger.setLevel(logging.DEBUG) ============= Documentation ============= ``python-telegram-bot``'s documentation lives at `readthedocs.io `_. ============ Getting help ============ You can get help in several ways: 1. We have a vibrant community of developers helping each other in our `Telegram group `_. Join us! 2. Report bugs, request new features or ask questions by `creating an issue `_ or `a discussion `_. 3. Our `Wiki pages `_ offer a growing amount of resources. 4. You can even ask for help on Stack Overflow using the `python-telegram-bot tag `_. ============ Contributing ============ Contributions of all sizes are welcome. Please review our `contribution guidelines `_ to get started. You can also help by `reporting bugs `_. ======== 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-13.11/README_RAW.rst000066400000000000000000000207761417656324400176500ustar00rootroot00000000000000.. 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 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 `_. .. 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-5.7-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://python-telegram-bot.readthedocs.io/ :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/workflows/GitHub%20Actions/badge.svg :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://codecov.io/gh/python-telegram-bot/python-telegram-bot :alt: Code coverage .. image:: http://isitmaintained.com/badge/resolution/python-telegram-bot/python-telegram-bot.svg :target: http://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://www.codacy.com/app/python-telegram-bot/python-telegram-bot?utm_source=github.com&utm_medium=referral&utm_content=python-telegram-bot/python-telegram-bot&utm_campaign=Badge_Grade :alt: Code quality: Codacy .. image:: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot.svg/?label=active+issues :target: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot/?ref=repository-badge :alt: Code quality: DeepSource .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black .. image:: https://img.shields.io/badge/Telegram-Group-blue.svg?logo=telegram :target: https://telegram.me/pythontelegrambotgroup :alt: Telegram Group ================= Table of contents ================= - `Introduction`_ - `Telegram API support`_ - `Installing`_ - `Getting started`_ #. `Logging`_ #. `Documentation`_ - `Getting help`_ - `Contributing`_ - `License`_ ============ Introduction ============ This library provides a pure Python, lightweight interface for the `Telegram Bot API `_. It's compatible with Python versions 3.6.8+. PTB-Raw might also work on `PyPy `_, though there have been a lot of issues before. Hence, PyPy is not officially supported. ``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. Please consult the PTB resources. ---- 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 **5.7** are supported. ========== Installing ========== You can install or upgrade python-telegram-bot-raw with: .. code:: shell $ pip install python-telegram-bot-raw --upgrade Or you can install from source with: .. code:: shell $ git clone https://github.com/python-telegram-bot/python-telegram-bot --recursive $ cd python-telegram-bot $ python setup-raw.py install In case you have a previously cloned local repository already, you should initialize the added urllib3 submodule before installing with: .. code:: shell $ git submodule update --init --recursive ---- 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`. --------------------- Optional Dependencies --------------------- PTB can be installed with optional dependencies: * ``pip install python-telegram-bot-raw[passport]`` installs the `cryptography `_ library. Use this, if you want to use Telegram Passport related functionality. * ``pip install python-telegram-bot-raw[ujson]`` installs the `ujson `_ library. It will then be used for JSON de- & encoding, which can bring speed up compared to the standard `json `_ library. =============== Getting started =============== Our Wiki contains an `Introduction to the API `_. Other references are: - the `Telegram API documentation `_ - the `python-telegram-bot documentation `_ ------- Logging ------- This library uses the ``logging`` module. To set up logging to standard output, put: .. code:: python import logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') at the beginning of your script. You can also use logs in your application by calling ``logging.getLogger()`` and setting the log level you want: .. code:: python logger = logging.getLogger() logger.setLevel(logging.INFO) If you want DEBUG logs instead: .. code:: python logger.setLevel(logging.DEBUG) ============= Documentation ============= ``python-telegram-bot``'s documentation lives at `readthedocs.io `_, which includes the relevant documentation for ``python-telegram-bot-raw``. ============ Getting help ============ You can get help in several ways: 1. We have a vibrant community of developers helping each other in our `Telegram group `_. Join us! 2. Report bugs, request new features or ask questions by `creating an issue `_ or `a discussion `_. 3. Our `Wiki pages `_ offer a growing amount of resources. 4. You can even ask for help on Stack Overflow using the `python-telegram-bot tag `_. ============ Contributing ============ Contributions of all sizes are welcome. Please review our `contribution guidelines `_ to get started. You can also help by `reporting bugs `_. ======== 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-13.11/codecov.yml000066400000000000000000000004031417656324400175760ustar00rootroot00000000000000comment: 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-13.11/contrib/000077500000000000000000000000001417656324400170745ustar00rootroot00000000000000python-telegram-bot-13.11/contrib/build-debian.sh000077500000000000000000000001261417656324400217510ustar00rootroot00000000000000#!/bin/bash cp -R contrib/debian . debuild -us -uc debian/rules clean rm -rf debian python-telegram-bot-13.11/contrib/debian/000077500000000000000000000000001417656324400203165ustar00rootroot00000000000000python-telegram-bot-13.11/contrib/debian/changelog000066400000000000000000000002461417656324400221720ustar00rootroot00000000000000telegram (12.0.0b1) unstable; urgency=medium * Debian packaging; * Initial Release. -- Marco Marinello Thu, 22 Aug 2019 20:36:47 +0200 python-telegram-bot-13.11/contrib/debian/compat000066400000000000000000000000031417656324400215150ustar00rootroot0000000000000011 python-telegram-bot-13.11/contrib/debian/control000066400000000000000000000017521417656324400217260ustar00rootroot00000000000000Source: 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-13.11/contrib/debian/copyright000066400000000000000000000021521417656324400222510ustar00rootroot00000000000000Format: 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-13.11/contrib/debian/install000066400000000000000000000000601417656324400217030ustar00rootroot00000000000000AUTHORS.rst /usr/share/doc/python3-telegram-bot python-telegram-bot-13.11/contrib/debian/rules000077500000000000000000000011171417656324400213760ustar00rootroot00000000000000#!/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-13.11/contrib/debian/source/000077500000000000000000000000001417656324400216165ustar00rootroot00000000000000python-telegram-bot-13.11/contrib/debian/source/format000066400000000000000000000000151417656324400230250ustar00rootroot000000000000003.0 (native) python-telegram-bot-13.11/contrib/debian/source/options000066400000000000000000000000521417656324400232310ustar00rootroot00000000000000extend-diff-ignore = "^[^/]*[.]egg-info/" python-telegram-bot-13.11/docs/000077500000000000000000000000001417656324400163645ustar00rootroot00000000000000python-telegram-bot-13.11/docs/Makefile000066400000000000000000000164451417656324400200360ustar00rootroot00000000000000# 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 # 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." 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-13.11/docs/requirements-docs.txt000066400000000000000000000004101417656324400225710ustar00rootroot00000000000000sphinx==3.5.4 sphinx-pypi-upload # When bumping this, make sure to rebuild the dark-mode CSS # More instructions at source/_static/dark.css # Ofc once https://github.com/readthedocs/sphinx_rtd_theme/issues/224 is closed, we should use that sphinx_rtd_theme==0.5.2 python-telegram-bot-13.11/docs/source/000077500000000000000000000000001417656324400176645ustar00rootroot00000000000000python-telegram-bot-13.11/docs/source/_static/000077500000000000000000000000001417656324400213125ustar00rootroot00000000000000python-telegram-bot-13.11/docs/source/_static/.placeholder000066400000000000000000000000001417656324400235630ustar00rootroot00000000000000python-telegram-bot-13.11/docs/source/_static/dark.css000066400000000000000000003712761417656324400227650ustar00rootroot00000000000000@media (prefers-color-scheme: dark) { /* Generated by https://darkreader.org Instructions: Install the extension on a Chromium-based browser Then do this to export the CSS: https://git.io/JOM6t and drop it here Some color values where manually changed - just search for "/*" in this file and insert them in the new css */ /* User-Agent Style */ html { background-color: #181a1b !important; } html, body, input, textarea, select, button { background-color: #181a1b; } html, body, input, textarea, select, button { border-color: #736b5e; color: #e8e6e3; } a { color: #3391ff; } table { border-color: #545b5e; } ::placeholder { color: #b2aba1; } input:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill { background-color: #555b00 !important; color: #e8e6e3 !important; } ::-webkit-scrollbar { background-color: #202324; color: #aba499; } ::-webkit-scrollbar-thumb { background-color: #454a4d; } ::-webkit-scrollbar-thumb:hover { background-color: #575e62; } ::-webkit-scrollbar-thumb:active { background-color: #484e51; } ::-webkit-scrollbar-corner { background-color: #181a1b; } ::selection { background-color: #004daa !important; color: #e8e6e3 !important; } ::-moz-selection { background-color: #004daa !important; color: #e8e6e3 !important; } /* Invert Style */ .jfk-bubble.gtx-bubble, .captcheck_answer_label > input + img, span#closed_text > img[src^="https://www.gstatic.com/images/branding/googlelogo"], span[data-href^="https://www.hcaptcha.com/"] > #icon, #bit-notification-bar-iframe, embed[type="application/pdf"] { filter: invert(100%) hue-rotate(180deg) contrast(90%) !important; } /* Variables Style */ :root { --darkreader-neutral-background: #131516; --darkreader-neutral-text: #d8d4cf; --darkreader-selection-background: #004daa; --darkreader-selection-text: #e8e6e3; } /* Modified CSS */ a:active, a:hover { outline-color: initial; } abbr[title] { border-bottom-color: initial; } ins { background-image: initial; background-color: rgb(112, 112, 0); text-decoration-color: initial; } ins, mark { color: rgb(232, 230, 227); } mark { background-image: initial; background-color: rgb(204, 204, 0); } dl, ol, ul { list-style-image: none; } li { list-style-image: initial; } img { border-color: initial; } .chromeframe { background-image: initial; background-color: rgb(53, 57, 59); color: rgb(232, 230, 227); } .ir { border-color: initial; background-color: transparent; } .visuallyhidden { border-color: initial; } .fa-border { border-color: rgb(53, 57, 59); } .fa-inverse { color: rgb(232, 230, 227); } .sr-only { border-color: initial; } .fa::before, .icon::before, .rst-content .admonition-title::before, .rst-content .code-block-caption .headerlink::before, .rst-content code.download span:first-child::before, .rst-content dl dt .headerlink::before, .rst-content h1 .headerlink::before, .rst-content h2 .headerlink::before, .rst-content h3 .headerlink::before, .rst-content h4 .headerlink::before, .rst-content h5 .headerlink::before, .rst-content h6 .headerlink::before, .rst-content p.caption .headerlink::before, .rst-content table > caption .headerlink::before, .rst-content tt.download span:first-child::before, .wy-dropdown .caret::before, .wy-inline-validate.wy-inline-validate-danger .wy-input-context::before, .wy-inline-validate.wy-inline-validate-info .wy-input-context::before, .wy-inline-validate.wy-inline-validate-success .wy-input-context::before, .wy-inline-validate.wy-inline-validate-warning .wy-input-context::before, .wy-menu-vertical li.current > a span.toctree-expand::before, .wy-menu-vertical li.on a span.toctree-expand::before, .wy-menu-vertical li span.toctree-expand::before { text-decoration-color: inherit; } .rst-content .code-block-caption a .headerlink, .rst-content a .admonition-title, .rst-content code.download a span:first-child, .rst-content dl dt a .headerlink, .rst-content h1 a .headerlink, .rst-content h2 a .headerlink, .rst-content h3 a .headerlink, .rst-content h4 a .headerlink, .rst-content h5 a .headerlink, .rst-content h6 a .headerlink, .rst-content p.caption a .headerlink, .rst-content table > caption a .headerlink, .rst-content tt.download a span:first-child, .wy-menu-vertical li.current > a span.toctree-expand, .wy-menu-vertical li.on a span.toctree-expand, .wy-menu-vertical li a span.toctree-expand, a .fa, a .icon, a .rst-content .admonition-title, a .rst-content .code-block-caption .headerlink, a .rst-content code.download span:first-child, a .rst-content dl dt .headerlink, a .rst-content h1 .headerlink, a .rst-content h2 .headerlink, a .rst-content h3 .headerlink, a .rst-content h4 .headerlink, a .rst-content h5 .headerlink, a .rst-content h6 .headerlink, a .rst-content p.caption .headerlink, a .rst-content table > caption .headerlink, a .rst-content tt.download span:first-child, a .wy-menu-vertical li span.toctree-expand { text-decoration-color: inherit; } .rst-content .admonition, .rst-content .admonition-todo, .rst-content .attention, .rst-content .caution, .rst-content .danger, .rst-content .error, .rst-content .hint, .rst-content .important, .rst-content .note, .rst-content .seealso, .rst-content .tip, .rst-content .warning, .wy-alert { background-image: initial; background-color: rgb(32, 35, 36); } .rst-content .admonition-title, .wy-alert-title { color: rgb(232, 230, 227); background-image: initial; background-color: rgb(29, 91, 131); } .rst-content .danger, .rst-content .error, .rst-content .wy-alert-danger.admonition, .rst-content .wy-alert-danger.admonition-todo, .rst-content .wy-alert-danger.attention, .rst-content .wy-alert-danger.caution, .rst-content .wy-alert-danger.hint, .rst-content .wy-alert-danger.important, .rst-content .wy-alert-danger.note, .rst-content .wy-alert-danger.seealso, .rst-content .wy-alert-danger.tip, .rst-content .wy-alert-danger.warning, .wy-alert.wy-alert-danger { background-image: initial; background-color: rgb(52, 12, 8); } .rst-content .danger .admonition-title, .rst-content .danger .wy-alert-title, .rst-content .error .admonition-title, .rst-content .error .wy-alert-title, .rst-content .wy-alert-danger.admonition-todo .admonition-title, .rst-content .wy-alert-danger.admonition-todo .wy-alert-title, .rst-content .wy-alert-danger.admonition .admonition-title, .rst-content .wy-alert-danger.admonition .wy-alert-title, .rst-content .wy-alert-danger.attention .admonition-title, .rst-content .wy-alert-danger.attention .wy-alert-title, .rst-content .wy-alert-danger.caution .admonition-title, .rst-content .wy-alert-danger.caution .wy-alert-title, .rst-content .wy-alert-danger.hint .admonition-title, .rst-content .wy-alert-danger.hint .wy-alert-title, .rst-content .wy-alert-danger.important .admonition-title, .rst-content .wy-alert-danger.important .wy-alert-title, .rst-content .wy-alert-danger.note .admonition-title, .rst-content .wy-alert-danger.note .wy-alert-title, .rst-content .wy-alert-danger.seealso .admonition-title, .rst-content .wy-alert-danger.seealso .wy-alert-title, .rst-content .wy-alert-danger.tip .admonition-title, .rst-content .wy-alert-danger.tip .wy-alert-title, .rst-content .wy-alert-danger.warning .admonition-title, .rst-content .wy-alert-danger.warning .wy-alert-title, .rst-content .wy-alert.wy-alert-danger .admonition-title, .wy-alert.wy-alert-danger .rst-content .admonition-title, .wy-alert.wy-alert-danger .wy-alert-title { background-image: initial; background-color: rgb(108, 22, 13); } .rst-content .admonition-todo, .rst-content .attention, .rst-content .caution, .rst-content .warning, .rst-content .wy-alert-warning.admonition, .rst-content .wy-alert-warning.danger, .rst-content .wy-alert-warning.error, .rst-content .wy-alert-warning.hint, .rst-content .wy-alert-warning.important, .rst-content .wy-alert-warning.note, .rst-content .wy-alert-warning.seealso, .rst-content .wy-alert-warning.tip, .wy-alert.wy-alert-warning { background-image: initial; /* Manually overridden */ background-color: rgb(37, 33, 30); /*background-color: rgb(82, 53, 0);*/ } .rst-content .admonition-todo .admonition-title, .rst-content .admonition-todo .wy-alert-title, .rst-content .attention .admonition-title, .rst-content .attention .wy-alert-title, .rst-content .caution .admonition-title, .rst-content .caution .wy-alert-title, .rst-content .warning .admonition-title, .rst-content .warning .wy-alert-title, .rst-content .wy-alert-warning.admonition .admonition-title, .rst-content .wy-alert-warning.admonition .wy-alert-title, .rst-content .wy-alert-warning.danger .admonition-title, .rst-content .wy-alert-warning.danger .wy-alert-title, .rst-content .wy-alert-warning.error .admonition-title, .rst-content .wy-alert-warning.error .wy-alert-title, .rst-content .wy-alert-warning.hint .admonition-title, .rst-content .wy-alert-warning.hint .wy-alert-title, .rst-content .wy-alert-warning.important .admonition-title, .rst-content .wy-alert-warning.important .wy-alert-title, .rst-content .wy-alert-warning.note .admonition-title, .rst-content .wy-alert-warning.note .wy-alert-title, .rst-content .wy-alert-warning.seealso .admonition-title, .rst-content .wy-alert-warning.seealso .wy-alert-title, .rst-content .wy-alert-warning.tip .admonition-title, .rst-content .wy-alert-warning.tip .wy-alert-title, .rst-content .wy-alert.wy-alert-warning .admonition-title, .wy-alert.wy-alert-warning .rst-content .admonition-title, .wy-alert.wy-alert-warning .wy-alert-title { background-image: initial; background-color: rgb(123, 65, 14); } .rst-content .note, .rst-content .seealso, .rst-content .wy-alert-info.admonition, .rst-content .wy-alert-info.admonition-todo, .rst-content .wy-alert-info.attention, .rst-content .wy-alert-info.caution, .rst-content .wy-alert-info.danger, .rst-content .wy-alert-info.error, .rst-content .wy-alert-info.hint, .rst-content .wy-alert-info.important, .rst-content .wy-alert-info.tip, .rst-content .wy-alert-info.warning, .wy-alert.wy-alert-info { background-image: initial; background-color: rgb(32, 35, 36); } .rst-content .note .admonition-title, .rst-content .note .wy-alert-title, .rst-content .seealso .admonition-title, .rst-content .seealso .wy-alert-title, .rst-content .wy-alert-info.admonition-todo .admonition-title, .rst-content .wy-alert-info.admonition-todo .wy-alert-title, .rst-content .wy-alert-info.admonition .admonition-title, .rst-content .wy-alert-info.admonition .wy-alert-title, .rst-content .wy-alert-info.attention .admonition-title, .rst-content .wy-alert-info.attention .wy-alert-title, .rst-content .wy-alert-info.caution .admonition-title, .rst-content .wy-alert-info.caution .wy-alert-title, .rst-content .wy-alert-info.danger .admonition-title, .rst-content .wy-alert-info.danger .wy-alert-title, .rst-content .wy-alert-info.error .admonition-title, .rst-content .wy-alert-info.error .wy-alert-title, .rst-content .wy-alert-info.hint .admonition-title, .rst-content .wy-alert-info.hint .wy-alert-title, .rst-content .wy-alert-info.important .admonition-title, .rst-content .wy-alert-info.important .wy-alert-title, .rst-content .wy-alert-info.tip .admonition-title, .rst-content .wy-alert-info.tip .wy-alert-title, .rst-content .wy-alert-info.warning .admonition-title, .rst-content .wy-alert-info.warning .wy-alert-title, .rst-content .wy-alert.wy-alert-info .admonition-title, .wy-alert.wy-alert-info .rst-content .admonition-title, .wy-alert.wy-alert-info .wy-alert-title { background-image: initial; background-color: rgb(29, 91, 131); } .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .wy-alert-success.admonition, .rst-content .wy-alert-success.admonition-todo, .rst-content .wy-alert-success.attention, .rst-content .wy-alert-success.caution, .rst-content .wy-alert-success.danger, .rst-content .wy-alert-success.error, .rst-content .wy-alert-success.note, .rst-content .wy-alert-success.seealso, .rst-content .wy-alert-success.warning, .wy-alert.wy-alert-success { background-image: initial; background-color: rgb(9, 66, 58); } .rst-content .hint .admonition-title, .rst-content .hint .wy-alert-title, .rst-content .important .admonition-title, .rst-content .important .wy-alert-title, .rst-content .tip .admonition-title, .rst-content .tip .wy-alert-title, .rst-content .wy-alert-success.admonition-todo .admonition-title, .rst-content .wy-alert-success.admonition-todo .wy-alert-title, .rst-content .wy-alert-success.admonition .admonition-title, .rst-content .wy-alert-success.admonition .wy-alert-title, .rst-content .wy-alert-success.attention .admonition-title, .rst-content .wy-alert-success.attention .wy-alert-title, .rst-content .wy-alert-success.caution .admonition-title, .rst-content .wy-alert-success.caution .wy-alert-title, .rst-content .wy-alert-success.danger .admonition-title, .rst-content .wy-alert-success.danger .wy-alert-title, .rst-content .wy-alert-success.error .admonition-title, .rst-content .wy-alert-success.error .wy-alert-title, .rst-content .wy-alert-success.note .admonition-title, .rst-content .wy-alert-success.note .wy-alert-title, .rst-content .wy-alert-success.seealso .admonition-title, .rst-content .wy-alert-success.seealso .wy-alert-title, .rst-content .wy-alert-success.warning .admonition-title, .rst-content .wy-alert-success.warning .wy-alert-title, .rst-content .wy-alert.wy-alert-success .admonition-title, .wy-alert.wy-alert-success .rst-content .admonition-title, .wy-alert.wy-alert-success .wy-alert-title { background-image: initial; background-color: rgb(21, 150, 125); } .rst-content .wy-alert-neutral.admonition, .rst-content .wy-alert-neutral.admonition-todo, .rst-content .wy-alert-neutral.attention, .rst-content .wy-alert-neutral.caution, .rst-content .wy-alert-neutral.danger, .rst-content .wy-alert-neutral.error, .rst-content .wy-alert-neutral.hint, .rst-content .wy-alert-neutral.important, .rst-content .wy-alert-neutral.note, .rst-content .wy-alert-neutral.seealso, .rst-content .wy-alert-neutral.tip, .rst-content .wy-alert-neutral.warning, .wy-alert.wy-alert-neutral { background-image: initial; background-color: rgb(27, 36, 36); } .rst-content .wy-alert-neutral.admonition-todo .admonition-title, .rst-content .wy-alert-neutral.admonition-todo .wy-alert-title, .rst-content .wy-alert-neutral.admonition .admonition-title, .rst-content .wy-alert-neutral.admonition .wy-alert-title, .rst-content .wy-alert-neutral.attention .admonition-title, .rst-content .wy-alert-neutral.attention .wy-alert-title, .rst-content .wy-alert-neutral.caution .admonition-title, .rst-content .wy-alert-neutral.caution .wy-alert-title, .rst-content .wy-alert-neutral.danger .admonition-title, .rst-content .wy-alert-neutral.danger .wy-alert-title, .rst-content .wy-alert-neutral.error .admonition-title, .rst-content .wy-alert-neutral.error .wy-alert-title, .rst-content .wy-alert-neutral.hint .admonition-title, .rst-content .wy-alert-neutral.hint .wy-alert-title, .rst-content .wy-alert-neutral.important .admonition-title, .rst-content .wy-alert-neutral.important .wy-alert-title, .rst-content .wy-alert-neutral.note .admonition-title, .rst-content .wy-alert-neutral.note .wy-alert-title, .rst-content .wy-alert-neutral.seealso .admonition-title, .rst-content .wy-alert-neutral.seealso .wy-alert-title, .rst-content .wy-alert-neutral.tip .admonition-title, .rst-content .wy-alert-neutral.tip .wy-alert-title, .rst-content .wy-alert-neutral.warning .admonition-title, .rst-content .wy-alert-neutral.warning .wy-alert-title, .rst-content .wy-alert.wy-alert-neutral .admonition-title, .wy-alert.wy-alert-neutral .rst-content .admonition-title, .wy-alert.wy-alert-neutral .wy-alert-title { color: rgb(192, 186, 178); background-image: initial; background-color: rgb(40, 43, 45); } .rst-content .wy-alert-neutral.admonition-todo a, .rst-content .wy-alert-neutral.admonition a, .rst-content .wy-alert-neutral.attention a, .rst-content .wy-alert-neutral.caution a, .rst-content .wy-alert-neutral.danger a, .rst-content .wy-alert-neutral.error a, .rst-content .wy-alert-neutral.hint a, .rst-content .wy-alert-neutral.important a, .rst-content .wy-alert-neutral.note a, .rst-content .wy-alert-neutral.seealso a, .rst-content .wy-alert-neutral.tip a, .rst-content .wy-alert-neutral.warning a, .wy-alert.wy-alert-neutral a { color: rgb(84, 164, 217); } .wy-tray-container li { background-image: initial; background-color: transparent; color: rgb(232, 230, 227); box-shadow: rgba(0, 0, 0, 0.1) 0px 5px 5px 0px; } .wy-tray-container li.wy-tray-item-success { background-image: initial; background-color: rgb(31, 139, 77); } .wy-tray-container li.wy-tray-item-info { background-image: initial; background-color: rgb(33, 102, 148); } .wy-tray-container li.wy-tray-item-warning { background-image: initial; background-color: rgb(178, 94, 20); } .wy-tray-container li.wy-tray-item-danger { background-image: initial; background-color: rgb(162, 33, 20); } .btn { color: rgb(232, 230, 227); border-color: rgba(140, 130, 115, 0.1); background-color: rgb(31, 139, 77); text-decoration-color: initial; box-shadow: rgba(24, 26, 27, 0.5) 0px 1px 2px -1px inset, rgba(0, 0, 0, 0.1) 0px -2px 0px 0px inset; } .btn-hover { background-image: initial; background-color: rgb(37, 114, 165); color: rgb(232, 230, 227); } .btn:hover { background-image: initial; background-color: rgb(35, 156, 86); color: rgb(232, 230, 227); } .btn:focus { background-image: initial; background-color: rgb(35, 156, 86); outline-color: initial; } .btn:active { box-shadow: rgba(0, 0, 0, 0.05) 0px -1px 0px 0px inset, rgba(0, 0, 0, 0.1) 0px 2px 0px 0px inset; } .btn:visited { color: rgb(232, 230, 227); } .btn-disabled, .btn-disabled:active, .btn-disabled:focus, .btn-disabled:hover, .btn:disabled { background-image: none; box-shadow: none; } .btn-info { background-color: rgb(33, 102, 148) !important; } .btn-info:hover { background-color: rgb(37, 114, 165) !important; } .btn-neutral { background-color: rgb(27, 36, 36) !important; color: rgb(192, 186, 178) !important; } .btn-neutral:hover { color: rgb(192, 186, 178); background-color: rgb(34, 44, 44) !important; } .btn-neutral:visited { color: rgb(192, 186, 178) !important; } .btn-success { background-color: rgb(31, 139, 77) !important; } .btn-success:hover { background-color: rgb(27, 122, 68) !important; } .btn-danger { background-color: rgb(162, 33, 20) !important; } .btn-danger:hover { background-color: rgb(149, 30, 18) !important; } .btn-warning { background-color: rgb(178, 94, 20) !important; } .btn-warning:hover { background-color: rgb(165, 87, 18) !important; } .btn-invert { background-color: rgb(26, 28, 29); } .btn-invert:hover { background-color: rgb(35, 38, 40) !important; } .btn-link { color: rgb(84, 164, 217); box-shadow: none; background-color: transparent !important; border-color: transparent !important; } .btn-link:active, .btn-link:hover { box-shadow: none; background-color: transparent !important; color: rgb(79, 162, 216) !important; } .btn-link:visited { color: rgb(164, 103, 188); } .wy-dropdown-menu { background-image: initial; background-color: rgb(26, 28, 29); border-color: rgb(60, 65, 67); box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 2px 0px; } .wy-dropdown-menu > dd > a { color: rgb(192, 186, 178); } .wy-dropdown-menu > dd > a:hover { background-image: initial; background-color: rgb(33, 102, 148); color: rgb(232, 230, 227); } .wy-dropdown-menu > dd.divider { border-top-color: rgb(60, 65, 67); } .wy-dropdown-menu > dd.call-to-action { background-image: initial; background-color: rgb(40, 43, 45); } .wy-dropdown-menu > dd.call-to-action:hover { background-image: initial; background-color: rgb(40, 43, 45); } .wy-dropdown-menu > dd.call-to-action .btn { color: rgb(232, 230, 227); } .wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu { background-image: initial; background-color: rgb(26, 28, 29); } .wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover { background-image: initial; background-color: rgb(33, 102, 148); color: rgb(232, 230, 227); } .wy-dropdown-arrow::before { border-bottom-color: rgb(51, 55, 57); border-left-color: transparent; border-right-color: transparent; } fieldset, legend { border-color: initial; } label { color: rgb(200, 195, 188); } .wy-control-group.wy-control-group-required > label::after { color: rgb(233, 88, 73); } .wy-form-message-inline { color: rgb(168, 160, 149); } .wy-form-message { color: rgb(168, 160, 149); } input[type="color"], input[type="date"], input[type="datetime-local"], input[type="datetime"], input[type="email"], input[type="month"], input[type="number"], input[type="password"], input[type="search"], input[type="tel"], input[type="text"], input[type="time"], input[type="url"], input[type="week"] { border-color: rgb(62, 68, 70); box-shadow: rgb(43, 47, 49) 0px 1px 3px inset; } input[type="color"]:focus, input[type="date"]:focus, input[type="datetime-local"]:focus, input[type="datetime"]:focus, input[type="email"]:focus, input[type="month"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="time"]:focus, input[type="url"]:focus, input[type="week"]:focus { outline-color: initial; border-color: rgb(123, 114, 101); } input.no-focus:focus { border-color: rgb(62, 68, 70) !important; } input[type="checkbox"]:focus, input[type="file"]:focus, input[type="radio"]:focus { outline-color: rgb(13, 113, 167); } input[type="color"][disabled], input[type="date"][disabled], input[type="datetime-local"][disabled], input[type="datetime"][disabled], input[type="email"][disabled], input[type="month"][disabled], input[type="number"][disabled], input[type="password"][disabled], input[type="search"][disabled], input[type="tel"][disabled], input[type="text"][disabled], input[type="time"][disabled], input[type="url"][disabled], input[type="week"][disabled] { background-color: rgb(27, 29, 30); } input:focus:invalid, select:focus:invalid, textarea:focus:invalid { color: rgb(233, 88, 73); border-color: rgb(149, 31, 18); } input:focus:invalid:focus, select:focus:invalid:focus, textarea:focus:invalid:focus { border-color: rgb(149, 31, 18); } input[type="checkbox"]:focus:invalid:focus, input[type="file"]:focus:invalid:focus, input[type="radio"]:focus:invalid:focus { outline-color: rgb(149, 31, 18); } select, textarea { border-color: rgb(62, 68, 70); box-shadow: rgb(43, 47, 49) 0px 1px 3px inset; } select { border-color: rgb(62, 68, 70); background-color: rgb(24, 26, 27); } select:focus, textarea:focus { outline-color: initial; } input[readonly], select[disabled], select[readonly], textarea[disabled], textarea[readonly] { background-color: rgb(27, 29, 30); } .wy-checkbox, .wy-radio { color: rgb(192, 186, 178); } .wy-input-prefix .wy-input-context, .wy-input-suffix .wy-input-context { background-color: rgb(27, 36, 36); border-color: rgb(62, 68, 70); color: rgb(168, 160, 149); } .wy-input-suffix .wy-input-context { border-left-color: initial; } .wy-input-prefix .wy-input-context { border-right-color: initial; } .wy-switch::before { background-image: initial; background-color: rgb(53, 57, 59); } .wy-switch::after { background-image: initial; background-color: rgb(82, 88, 92); } .wy-switch span { color: rgb(200, 195, 188); } .wy-switch.active::before { background-image: initial; background-color: rgb(24, 106, 58); } .wy-switch.active::after { background-image: initial; background-color: rgb(31, 139, 77); } .wy-control-group.wy-control-group-error .wy-form-message, .wy-control-group.wy-control-group-error > label { color: rgb(233, 88, 73); } .wy-control-group.wy-control-group-error input[type="color"], .wy-control-group.wy-control-group-error input[type="date"], .wy-control-group.wy-control-group-error input[type="datetime-local"], .wy-control-group.wy-control-group-error input[type="datetime"], .wy-control-group.wy-control-group-error input[type="email"], .wy-control-group.wy-control-group-error input[type="month"], .wy-control-group.wy-control-group-error input[type="number"], .wy-control-group.wy-control-group-error input[type="password"], .wy-control-group.wy-control-group-error input[type="search"], .wy-control-group.wy-control-group-error input[type="tel"], .wy-control-group.wy-control-group-error input[type="text"], .wy-control-group.wy-control-group-error input[type="time"], .wy-control-group.wy-control-group-error input[type="url"], .wy-control-group.wy-control-group-error input[type="week"], .wy-control-group.wy-control-group-error textarea { border-color: rgb(149, 31, 18); } .wy-inline-validate.wy-inline-validate-success .wy-input-context { color: rgb(92, 218, 145); } .wy-inline-validate.wy-inline-validate-danger .wy-input-context { color: rgb(233, 88, 73); } .wy-inline-validate.wy-inline-validate-warning .wy-input-context { color: rgb(232, 138, 54); } .wy-inline-validate.wy-inline-validate-info .wy-input-context { color: rgb(84, 164, 217); } .rst-content table.docutils caption, .rst-content table.field-list caption, .wy-table caption { color: rgb(232, 230, 227); } .rst-content table.docutils thead, .rst-content table.field-list thead, .wy-table thead { color: rgb(232, 230, 227); } .rst-content table.docutils thead th, .rst-content table.field-list thead th, .wy-table thead th { border-bottom-color: rgb(56, 61, 63); } .rst-content table.docutils td, .rst-content table.field-list td, .wy-table td { background-color: transparent; } .wy-table-secondary { color: rgb(152, 143, 129); } .wy-table-tertiary { color: rgb(152, 143, 129); } .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td, .wy-table-backed, .wy-table-odd td, .wy-table-striped tr:nth-child(2n-1) td { background-color: rgb(27, 36, 36); } .rst-content table.docutils, .wy-table-bordered-all { border-color: rgb(56, 61, 63); } .rst-content table.docutils td, .wy-table-bordered-all td { border-bottom-color: rgb(56, 61, 63); border-left-color: rgb(56, 61, 63); } .wy-table-bordered { border-color: rgb(56, 61, 63); } .wy-table-bordered-rows td { border-bottom-color: rgb(56, 61, 63); } .wy-table-horizontal td, .wy-table-horizontal th { border-bottom-color: rgb(56, 61, 63); } a { color: rgb(84, 164, 217); text-decoration-color: initial; } a:hover { color: rgb(68, 156, 214); } a:visited { color: rgb(164, 103, 188); } body { color: rgb(192, 186, 178); background-image: initial; background-color: rgb(33, 35, 37); } .wy-text-strike { text-decoration-color: initial; } .wy-text-warning { color: rgb(232, 138, 54) !important; } a.wy-text-warning:hover { color: rgb(236, 157, 87) !important; } .wy-text-info { color: rgb(84, 164, 217) !important; } a.wy-text-info:hover { color: rgb(79, 162, 216) !important; } .wy-text-success { color: rgb(92, 218, 145) !important; } a.wy-text-success:hover { color: rgb(73, 214, 133) !important; } .wy-text-danger { color: rgb(233, 88, 73) !important; } a.wy-text-danger:hover { color: rgb(237, 118, 104) !important; } .wy-text-neutral { color: rgb(192, 186, 178) !important; } a.wy-text-neutral:hover { color: rgb(176, 169, 159) !important; } hr { border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-top-color: rgb(56, 61, 63); } .rst-content code, .rst-content tt, code { background-image: initial; background-color: rgb(24, 26, 27); border-color: rgb(56, 61, 63); color: rgb(233, 88, 73); } .rst-content .section ul, .rst-content .toctree-wrapper ul, .wy-plain-list-disc, article ul { list-style-image: initial; } .rst-content .section ul li, .rst-content .toctree-wrapper ul li, .wy-plain-list-disc li, article ul li { list-style-image: initial; } .rst-content .section ul li li, .rst-content .toctree-wrapper ul li li, .wy-plain-list-disc li li, article ul li li { list-style-image: initial; } .rst-content .section ul li li li, .rst-content .toctree-wrapper ul li li li, .wy-plain-list-disc li li li, article ul li li li { list-style-image: initial; } .rst-content .section ul li ol li, .rst-content .toctree-wrapper ul li ol li, .wy-plain-list-disc li ol li, article ul li ol li { list-style-image: initial; } .rst-content .section ol, .rst-content ol.arabic, .wy-plain-list-decimal, article ol { list-style-image: initial; } .rst-content .section ol li, .rst-content ol.arabic li, .wy-plain-list-decimal li, article ol li { list-style-image: initial; } .rst-content .section ol li ul li, .rst-content ol.arabic li ul li, .wy-plain-list-decimal li ul li, article ol li ul li { list-style-image: initial; } .rst-content .wy-breadcrumbs li tt, .wy-breadcrumbs li .rst-content tt, .wy-breadcrumbs li code { border-color: initial; background-image: none; background-color: initial; } .rst-content .wy-breadcrumbs li tt.literal, .wy-breadcrumbs li .rst-content tt.literal, .wy-breadcrumbs li code.literal { color: rgb(192, 186, 178); } .wy-breadcrumbs-extra { color: rgb(184, 178, 169); } .wy-menu a:hover { text-decoration-color: initial; } .wy-menu-horiz li:hover { background-image: initial; background-color: rgba(24, 26, 27, 0.1); } .wy-menu-horiz li.divide-left { border-left-color: rgb(119, 110, 98); } .wy-menu-horiz li.divide-right { border-right-color: rgb(119, 110, 98); } .wy-menu-vertical header, .wy-menu-vertical p.caption { color: rgb(94, 170, 219); } .wy-menu-vertical li.divide-top { border-top-color: rgb(119, 110, 98); } .wy-menu-vertical li.divide-bottom { border-bottom-color: rgb(119, 110, 98); } .wy-menu-vertical li.current { background-image: initial; background-color: rgb(40, 43, 45); } .wy-menu-vertical li.current a { color: rgb(152, 143, 129); border-right-color: rgb(63, 69, 71); } .wy-menu-vertical li.current a:hover { background-image: initial; background-color: rgb(47, 51, 53); } .rst-content .wy-menu-vertical li tt, .wy-menu-vertical li .rst-content tt, .wy-menu-vertical li code { border-color: initial; background-image: inherit; background-color: inherit; color: inherit; } .wy-menu-vertical li span.toctree-expand { color: rgb(183, 177, 168); } .wy-menu-vertical li.current > a, .wy-menu-vertical li.on a { color: rgb(192, 186, 178); background-image: initial; background-color: rgb(26, 28, 29); border-color: initial; } .wy-menu-vertical li.current > a:hover, .wy-menu-vertical li.on a:hover { background-image: initial; background-color: rgb(26, 28, 29); } .wy-menu-vertical li.current > a:hover span.toctree-expand, .wy-menu-vertical li.on a:hover span.toctree-expand { color: rgb(152, 143, 129); } .wy-menu-vertical li.current > a span.toctree-expand, .wy-menu-vertical li.on a span.toctree-expand { color: rgb(200, 195, 188); } .wy-menu-vertical li.toctree-l1.current > a { border-bottom-color: rgb(63, 69, 71); border-top-color: rgb(63, 69, 71); } .wy-menu-vertical li.toctree-l2 a, .wy-menu-vertical li.toctree-l3 a, .wy-menu-vertical li.toctree-l4 a, .wy-menu-vertical li.toctree-l5 a, .wy-menu-vertical li.toctree-l6 a, .wy-menu-vertical li.toctree-l7 a, .wy-menu-vertical li.toctree-l8 a, .wy-menu-vertical li.toctree-l9 a, .wy-menu-vertical li.toctree-l10 a { color: rgb(192, 186, 178); } .wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l4 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l5 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l6 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l7 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l8 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l9 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l10 a:hover span.toctree-expand { color: rgb(152, 143, 129); } .wy-menu-vertical li.toctree-l2.current > a, .wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { background-image: initial; background-color: rgb(54, 59, 61); } .wy-menu-vertical li.toctree-l2 span.toctree-expand { color: rgb(174, 167, 156); } .wy-menu-vertical li.toctree-l3.current > a, .wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a { background-image: initial; background-color: rgb(61, 66, 69); } .wy-menu-vertical li.toctree-l3 span.toctree-expand { color: rgb(166, 158, 146); } .wy-menu-vertical li ul li a { color: rgb(208, 204, 198); } .wy-menu-vertical a { color: rgb(208, 204, 198); } .wy-menu-vertical a:hover { background-color: rgb(57, 62, 64); } .wy-menu-vertical a:hover span.toctree-expand { color: rgb(208, 204, 198); } .wy-menu-vertical a:active { background-color: rgb(33, 102, 148); color: rgb(232, 230, 227); } .wy-menu-vertical a:active span.toctree-expand { color: rgb(232, 230, 227); } .wy-side-nav-search { background-color: rgb(33, 102, 148); color: rgb(230, 228, 225); } .wy-side-nav-search input[type="text"] { border-color: rgb(35, 111, 160); } .wy-side-nav-search img { background-color: rgb(33, 102, 148); } .wy-side-nav-search .wy-dropdown > a, .wy-side-nav-search > a { color: rgb(230, 228, 225); } .wy-side-nav-search .wy-dropdown > a:hover, .wy-side-nav-search > a:hover { background-image: initial; background-color: rgba(24, 26, 27, 0.1); } .wy-side-nav-search .wy-dropdown > a img.logo, .wy-side-nav-search > a img.logo { background-image: initial; background-color: transparent; } .wy-side-nav-search > div.version { color: rgba(232, 230, 227, 0.3); } .wy-nav .wy-menu-vertical header { color: rgb(84, 164, 217); } .wy-nav .wy-menu-vertical a { color: rgb(184, 178, 169); } .wy-nav .wy-menu-vertical a:hover { background-color: rgb(33, 102, 148); color: rgb(232, 230, 227); } .wy-body-for-nav { background-image: initial; background-color: rgb(26, 28, 29); } .wy-nav-side { color: rgb(169, 161, 150); background-image: initial; background-color: rgb(38, 41, 43); } .wy-nav-top { background-image: initial; background-color: rgb(33, 102, 148); color: rgb(232, 230, 227); } .wy-nav-top a { color: rgb(232, 230, 227); } .wy-nav-top img { background-color: rgb(33, 102, 148); } .wy-nav-content-wrap { background-image: initial; background-color: rgb(26, 28, 29); } .wy-body-mask { background-image: initial; background-color: rgba(0, 0, 0, 0.2); } footer { color: rgb(152, 143, 129); } .rst-content footer span.commit tt, footer span.commit .rst-content tt, footer span.commit code { background-image: none; background-color: initial; border-color: initial; color: rgb(152, 143, 129); } #search-results .search li { border-bottom-color: rgb(56, 61, 63); } #search-results .search li:first-child { border-top-color: rgb(56, 61, 63); } #search-results .context { color: rgb(152, 143, 129); } .wy-body-for-nav { background-image: initial; background-color: rgb(26, 28, 29); } @media screen and (min-width: 1100px) { .wy-nav-content-wrap { background-image: initial; background-color: rgba(0, 0, 0, 0.05); } .wy-nav-content { background-image: initial; background-color: rgb(26, 28, 29); } } .rst-versions { color: rgb(230, 228, 225); background-image: initial; background-color: rgb(23, 24, 25); } .rst-versions a { color: rgb(84, 164, 217); text-decoration-color: initial; } .rst-versions .rst-current-version { background-color: rgb(29, 31, 32); color: rgb(92, 218, 145); } .rst-content .code-block-caption .rst-versions .rst-current-version .headerlink, .rst-content .rst-versions .rst-current-version .admonition-title, .rst-content code.download .rst-versions .rst-current-version span:first-child, .rst-content dl dt .rst-versions .rst-current-version .headerlink, .rst-content h1 .rst-versions .rst-current-version .headerlink, .rst-content h2 .rst-versions .rst-current-version .headerlink, .rst-content h3 .rst-versions .rst-current-version .headerlink, .rst-content h4 .rst-versions .rst-current-version .headerlink, .rst-content h5 .rst-versions .rst-current-version .headerlink, .rst-content h6 .rst-versions .rst-current-version .headerlink, .rst-content p.caption .rst-versions .rst-current-version .headerlink, .rst-content table > caption .rst-versions .rst-current-version .headerlink, .rst-content tt.download .rst-versions .rst-current-version span:first-child, .rst-versions .rst-current-version .fa, .rst-versions .rst-current-version .icon, .rst-versions .rst-current-version .rst-content .admonition-title, .rst-versions .rst-current-version .rst-content .code-block-caption .headerlink, .rst-versions .rst-current-version .rst-content code.download span:first-child, .rst-versions .rst-current-version .rst-content dl dt .headerlink, .rst-versions .rst-current-version .rst-content h1 .headerlink, .rst-versions .rst-current-version .rst-content h2 .headerlink, .rst-versions .rst-current-version .rst-content h3 .headerlink, .rst-versions .rst-current-version .rst-content h4 .headerlink, .rst-versions .rst-current-version .rst-content h5 .headerlink, .rst-versions .rst-current-version .rst-content h6 .headerlink, .rst-versions .rst-current-version .rst-content p.caption .headerlink, .rst-versions .rst-current-version .rst-content table > caption .headerlink, .rst-versions .rst-current-version .rst-content tt.download span:first-child, .rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand, .wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand { color: rgb(230, 228, 225); } .rst-versions .rst-current-version.rst-out-of-date { background-color: rgb(162, 33, 20); color: rgb(232, 230, 227); } .rst-versions .rst-current-version.rst-active-old-version { background-color: rgb(192, 156, 11); color: rgb(232, 230, 227); } .rst-versions .rst-other-versions { color: rgb(152, 143, 129); } .rst-versions .rst-other-versions hr { border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-top-color: rgb(119, 111, 98); } .rst-versions .rst-other-versions dd a { color: rgb(230, 228, 225); } .rst-versions.rst-badge { border-color: initial; } .rst-content abbr[title] { text-decoration-color: initial; } .rst-content.style-external-links a.reference.external::after { color: rgb(184, 178, 169); } .rst-content div[class^="highlight"], .rst-content pre.literal-block { border-color: rgb(56, 61, 63); } .rst-content div[class^="highlight"] div[class^="highlight"], .rst-content pre.literal-block div[class^="highlight"] { border-color: initial; } .rst-content .linenodiv pre { border-right-color: rgb(54, 59, 61); } .rst-content .admonition table { border-color: rgba(140, 130, 115, 0.1); } .rst-content .admonition table td, .rst-content .admonition table th { background-image: initial !important; background-color: transparent !important; border-color: rgba(140, 130, 115, 0.1) !important; } .rst-content .section ol.loweralpha, .rst-content .section ol.loweralpha > li { list-style-image: initial; } .rst-content .section ol.upperalpha, .rst-content .section ol.upperalpha > li { list-style-image: initial; } .rst-content .toc-backref { color: rgb(192, 186, 178); } .rst-content .sidebar { background-image: initial; background-color: rgb(27, 36, 36); border-color: rgb(56, 61, 63); } .rst-content .sidebar .sidebar-title { background-image: initial; background-color: rgb(40, 43, 45); } .rst-content .highlighted { background-image: initial; background-color: rgb(192, 156, 11); box-shadow: rgb(192, 156, 11) 0px 0px 0px 2px; } html.writer-html4 .rst-content table.docutils.citation, html.writer-html4 .rst-content table.docutils.footnote { background-image: none; background-color: initial; border-color: initial; } html.writer-html4 .rst-content table.docutils.citation td, html.writer-html4 .rst-content table.docutils.citation tr, html.writer-html4 .rst-content table.docutils.footnote td, html.writer-html4 .rst-content table.docutils.footnote tr { border-color: initial; background-color: transparent !important; } .rst-content table.docutils.footnote, html.writer-html4 .rst-content table.docutils.citation, html.writer-html5 .rst-content dl.footnote { color: rgb(152, 143, 129); } .rst-content table.docutils.footnote code, .rst-content table.docutils.footnote tt, html.writer-html4 .rst-content table.docutils.citation code, html.writer-html4 .rst-content table.docutils.citation tt, html.writer-html5 .rst-content dl.footnote code, html.writer-html5 .rst-content dl.footnote tt { color: rgb(178, 172, 162); } .rst-content table.docutils th { border-color: rgb(56, 61, 63); } html.writer-html5 .rst-content table.docutils th { border-color: rgb(56, 61, 63); } .rst-content table.field-list, .rst-content table.field-list td { border-color: initial; } .rst-content code, .rst-content tt { color: rgb(232, 230, 227); } .rst-content code.literal, .rst-content tt.literal { /* Manually overridden */ color: rgb(228, 138, 128); /*color: rgb(233, 88, 73);*/ } .rst-content code.xref, .rst-content tt.xref, a .rst-content code, a .rst-content tt { color: rgb(192, 186, 178); } .rst-content a code, .rst-content a tt { color: rgb(84, 164, 217); } html.writer-html4 .rst-content dl:not(.docutils) > dt, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) > dt { background-image: initial; background-color: rgb(32, 35, 36); color: rgb(84, 164, 217); border-top-color: rgb(28, 89, 128); } html.writer-html4 .rst-content dl:not(.docutils) > dt::before, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) > dt::before { color: rgb(109, 178, 223); } html.writer-html4 .rst-content dl:not(.docutils) > dt .headerlink, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) > dt .headerlink { color: rgb(192, 186, 178); } html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list) > dt, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list) > dt { border-top-color: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: rgb(62, 68, 70); background-image: initial; background-color: rgb(32, 35, 37); color: rgb(178, 172, 162); } html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list) > dt .headerlink, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list) > dt .headerlink { color: rgb(192, 186, 178); } html.writer-html4 .rst-content dl:not(.docutils) code.descclassname, html.writer-html4 .rst-content dl:not(.docutils) code.descname, html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname, html.writer-html4 .rst-content dl:not(.docutils) tt.descname, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descclassname, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descclassname, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname { background-color: transparent; border-color: initial; } html.writer-html4 .rst-content dl:not(.docutils) .optional, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .optional { color: rgb(232, 230, 227); } .rst-content .viewcode-back, .rst-content .viewcode-link { color: rgb(92, 218, 145); } .rst-content code.download, .rst-content tt.download { background-image: inherit; background-color: inherit; color: inherit; border-color: inherit; } .rst-content .guilabel { border-color: rgb(27, 84, 122); background-image: initial; background-color: rgb(32, 35, 36); } span[id*="MathJax-Span"] { color: rgb(192, 186, 178); } td.linenos pre { color: rgb(232, 230, 227); background-color: rgb(32, 35, 37); } span.linenos { color: rgb(232, 230, 227); background-color: rgb(32, 35, 37); } td.linenos pre.special { color: rgb(232, 230, 227); background-color: rgb(89, 89, 0); } span.linenos.special { color: rgb(232, 230, 227); background-color: rgb(89, 89, 0); } .highlight .hll { background-color: rgb(82, 82, 0); } .highlight { background-image: initial; /* Manually overridden */ background-color: rgb(44 47 37); /*background-color: rgb(61, 82, 0);*/ } .highlight .c { color: rgb(119, 179, 195); } .highlight .err { border-color: rgb(179, 0, 0); } .highlight .k { color: rgb(126, 255, 163); } .highlight .o { color: rgb(168, 160, 149); } .highlight .ch { color: rgb(119, 179, 195); } .highlight .cm { color: rgb(119, 179, 195); } .highlight .cp { color: rgb(126, 255, 163); } .highlight .cpf { color: rgb(119, 179, 195); } .highlight .c1 { color: rgb(119, 179, 195); } .highlight .cs { color: rgb(119, 179, 195); background-color: rgb(60, 0, 0); } .highlight .gd { color: rgb(255, 92, 92); } .highlight .gr { color: rgb(255, 26, 26); } .highlight .gh { color: rgb(127, 174, 255); } .highlight .gi { color: rgb(92, 255, 92); } .highlight .go { color: rgb(200, 195, 188); } .highlight .gp { color: rgb(246, 147, 68); } .highlight .gu { color: rgb(255, 114, 255); } .highlight .gt { color: rgb(71, 160, 255); } .highlight .kc { color: rgb(126, 255, 163); } .highlight .kd { color: rgb(126, 255, 163); } .highlight .kn { color: rgb(126, 255, 163); } .highlight .kp { color: rgb(126, 255, 163); } .highlight .kr { color: rgb(126, 255, 163); } .highlight .kt { color: rgb(255, 137, 103); } .highlight .m { color: rgb(125, 222, 174); } .highlight .s { color: rgb(123, 166, 202); } .highlight .na { color: rgb(123, 166, 202); } .highlight .nb { color: rgb(126, 255, 163); } .highlight .nc { color: rgb(81, 194, 242); } .highlight .no { color: rgb(103, 177, 215); } .highlight .nd { color: rgb(178, 172, 162); } .highlight .ni { color: rgb(217, 100, 73); } .highlight .ne { color: rgb(126, 255, 163); } .highlight .nf { color: rgb(131, 186, 249); } .highlight .nl { color: rgb(137, 193, 255); } .highlight .nn { color: rgb(81, 194, 242); } .highlight .nt { color: rgb(138, 191, 249); } .highlight .nv { color: rgb(190, 103, 215); } .highlight .ow { color: rgb(126, 255, 163); } .highlight .w { color: rgb(189, 183, 175); } .highlight .mb { color: rgb(125, 222, 174); } .highlight .mf { color: rgb(125, 222, 174); } .highlight .mh { color: rgb(125, 222, 174); } .highlight .mi { color: rgb(125, 222, 174); } .highlight .mo { color: rgb(125, 222, 174); } .highlight .sa { color: rgb(123, 166, 202); } .highlight .sb { color: rgb(123, 166, 202); } .highlight .sc { color: rgb(123, 166, 202); } .highlight .dl { color: rgb(123, 166, 202); } .highlight .sd { color: rgb(123, 166, 202); } .highlight .s2 { color: rgb(123, 166, 202); } .highlight .se { color: rgb(123, 166, 202); } .highlight .sh { color: rgb(123, 166, 202); } .highlight .si { color: rgb(117, 168, 209); } .highlight .sx { color: rgb(246, 147, 68); } .highlight .sr { color: rgb(133, 182, 224); } .highlight .s1 { color: rgb(123, 166, 202); } .highlight .ss { color: rgb(188, 230, 128); } .highlight .bp { color: rgb(126, 255, 163); } .highlight .fm { color: rgb(131, 186, 249); } .highlight .vc { color: rgb(190, 103, 215); } .highlight .vg { color: rgb(190, 103, 215); } .highlight .vi { color: rgb(190, 103, 215); } .highlight .vm { color: rgb(190, 103, 215); } .highlight .il { color: rgb(125, 222, 174); } @media (prefers-color-scheme: dark) { html { background-color: rgb(19, 21, 22) !important; } html, body, input, textarea, select, button { background-color: rgb(19, 21, 22); } html, body, input, textarea, select, button { border-color: rgb(106, 98, 87); color: rgb(216, 212, 207); } a { color: rgb(61, 165, 255); } table { border-color: rgb(111, 103, 91); } ::placeholder { color: rgb(178, 171, 161); } input:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill { background-color: rgb(68, 73, 0) !important; color: rgb(216, 212, 207) !important; } ::-webkit-scrollbar { background-color: rgb(26, 28, 29); color: rgb(173, 166, 156); } ::-webkit-scrollbar-thumb { background-color: rgb(55, 60, 62); } ::-webkit-scrollbar-thumb:hover { background-color: rgb(70, 75, 78); } ::-webkit-scrollbar-thumb:active { background-color: rgb(58, 62, 65); } ::-webkit-scrollbar-corner { background-color: rgb(19, 21, 22); } ::selection { background-color: rgb(0, 62, 136) !important; color: rgb(216, 212, 207) !important; } :root { --darkreader-neutral-background: #131516; --darkreader-text--darkreader-neutral-text: #cdc8c2; --darkreader-selection-background: #004daa; --darkreader-selection-text: #e8e6e3; } a:active, a:hover { outline-color: currentcolor; } abbr[title] { border-bottom-color: currentcolor; } ins { background-color: rgb(90, 90, 0); background-image: none; text-decoration-color: currentcolor; } ins, mark { color: rgb(216, 212, 207); } mark { background-color: rgb(163, 163, 0); background-image: none; } dl, ol, ul { list-style-image: none; } li { list-style-image: none; } img { border-color: currentcolor; } .chromeframe { background-color: rgb(42, 46, 47); background-image: none; color: rgb(216, 212, 207); } .ir { border-color: currentcolor; background-color: transparent; } .visuallyhidden { border-color: currentcolor; } .fa-border { border-color: rgb(122, 113, 100); } .fa-inverse { color: rgb(216, 212, 207); } .sr-only { border-color: currentcolor; } .fa::before, .icon::before, .rst-content .admonition-title::before, .rst-content .code-block-caption .headerlink::before, .rst-content code.download span:first-child::before, .rst-content dl dt .headerlink::before, .rst-content h1 .headerlink::before, .rst-content h2 .headerlink::before, .rst-content h3 .headerlink::before, .rst-content h4 .headerlink::before, .rst-content h5 .headerlink::before, .rst-content h6 .headerlink::before, .rst-content p.caption .headerlink::before, .rst-content table > caption .headerlink::before, .rst-content tt.download span:first-child::before, .wy-dropdown .caret::before, .wy-inline-validate.wy-inline-validate-danger .wy-input-context::before, .wy-inline-validate.wy-inline-validate-info .wy-input-context::before, .wy-inline-validate.wy-inline-validate-success .wy-input-context::before, .wy-inline-validate.wy-inline-validate-warning .wy-input-context::before, .wy-menu-vertical li.current > a span.toctree-expand::before, .wy-menu-vertical li.on a span.toctree-expand::before, .wy-menu-vertical li span.toctree-expand::before { text-decoration-color: inherit; } .rst-content .code-block-caption a .headerlink, .rst-content a .admonition-title, .rst-content code.download a span:first-child, .rst-content dl dt a .headerlink, .rst-content h1 a .headerlink, .rst-content h2 a .headerlink, .rst-content h3 a .headerlink, .rst-content h4 a .headerlink, .rst-content h5 a .headerlink, .rst-content h6 a .headerlink, .rst-content p.caption a .headerlink, .rst-content table > caption a .headerlink, .rst-content tt.download a span:first-child, .wy-menu-vertical li.current > a span.toctree-expand, .wy-menu-vertical li.on a span.toctree-expand, .wy-menu-vertical li a span.toctree-expand, a .fa, a .icon, a .rst-content .admonition-title, a .rst-content .code-block-caption .headerlink, a .rst-content code.download span:first-child, a .rst-content dl dt .headerlink, a .rst-content h1 .headerlink, a .rst-content h2 .headerlink, a .rst-content h3 .headerlink, a .rst-content h4 .headerlink, a .rst-content h5 .headerlink, a .rst-content h6 .headerlink, a .rst-content p.caption .headerlink, a .rst-content table > caption .headerlink, a .rst-content tt.download span:first-child, a .wy-menu-vertical li span.toctree-expand { text-decoration-color: inherit; } .rst-content .admonition, .rst-content .admonition-todo, .rst-content .attention, .rst-content .caution, .rst-content .danger, .rst-content .error, .rst-content .hint, .rst-content .important, .rst-content .note, .rst-content .seealso, .rst-content .tip, .rst-content .warning, .wy-alert { background-color: rgb(26, 28, 29); background-image: none; } .rst-content .admonition-title, .wy-alert-title { color: rgb(216, 212, 207); background-color: rgb(23, 73, 105); background-image: none; } .rst-content .danger, .rst-content .error, .rst-content .wy-alert-danger.admonition, .rst-content .wy-alert-danger.admonition-todo, .rst-content .wy-alert-danger.attention, .rst-content .wy-alert-danger.caution, .rst-content .wy-alert-danger.hint, .rst-content .wy-alert-danger.important, .rst-content .wy-alert-danger.note, .rst-content .wy-alert-danger.seealso, .rst-content .wy-alert-danger.tip, .rst-content .wy-alert-danger.warning, .wy-alert.wy-alert-danger { background-color: rgb(42, 10, 6); background-image: none; } .rst-content .danger .admonition-title, .rst-content .danger .wy-alert-title, .rst-content .error .admonition-title, .rst-content .error .wy-alert-title, .rst-content .wy-alert-danger.admonition-todo .admonition-title, .rst-content .wy-alert-danger.admonition-todo .wy-alert-title, .rst-content .wy-alert-danger.admonition .admonition-title, .rst-content .wy-alert-danger.admonition .wy-alert-title, .rst-content .wy-alert-danger.attention .admonition-title, .rst-content .wy-alert-danger.attention .wy-alert-title, .rst-content .wy-alert-danger.caution .admonition-title, .rst-content .wy-alert-danger.caution .wy-alert-title, .rst-content .wy-alert-danger.hint .admonition-title, .rst-content .wy-alert-danger.hint .wy-alert-title, .rst-content .wy-alert-danger.important .admonition-title, .rst-content .wy-alert-danger.important .wy-alert-title, .rst-content .wy-alert-danger.note .admonition-title, .rst-content .wy-alert-danger.note .wy-alert-title, .rst-content .wy-alert-danger.seealso .admonition-title, .rst-content .wy-alert-danger.seealso .wy-alert-title, .rst-content .wy-alert-danger.tip .admonition-title, .rst-content .wy-alert-danger.tip .wy-alert-title, .rst-content .wy-alert-danger.warning .admonition-title, .rst-content .wy-alert-danger.warning .wy-alert-title, .rst-content .wy-alert.wy-alert-danger .admonition-title, .wy-alert.wy-alert-danger .rst-content .admonition-title, .wy-alert.wy-alert-danger .wy-alert-title { background-color: rgb(86, 18, 10); background-image: none; } .rst-content .admonition-todo, .rst-content .attention, .rst-content .caution, .rst-content .warning, .rst-content .wy-alert-warning.admonition, .rst-content .wy-alert-warning.danger, .rst-content .wy-alert-warning.error, .rst-content .wy-alert-warning.hint, .rst-content .wy-alert-warning.important, .rst-content .wy-alert-warning.note, .rst-content .wy-alert-warning.seealso, .rst-content .wy-alert-warning.tip, .wy-alert.wy-alert-warning { background-color: rgb(25, 27, 28); background-image: none; } .rst-content .admonition-todo .admonition-title, .rst-content .admonition-todo .wy-alert-title, .rst-content .attention .admonition-title, .rst-content .attention .wy-alert-title, .rst-content .caution .admonition-title, .rst-content .caution .wy-alert-title, .rst-content .warning .admonition-title, .rst-content .warning .wy-alert-title, .rst-content .wy-alert-warning.admonition .admonition-title, .rst-content .wy-alert-warning.admonition .wy-alert-title, .rst-content .wy-alert-warning.danger .admonition-title, .rst-content .wy-alert-warning.danger .wy-alert-title, .rst-content .wy-alert-warning.error .admonition-title, .rst-content .wy-alert-warning.error .wy-alert-title, .rst-content .wy-alert-warning.hint .admonition-title, .rst-content .wy-alert-warning.hint .wy-alert-title, .rst-content .wy-alert-warning.important .admonition-title, .rst-content .wy-alert-warning.important .wy-alert-title, .rst-content .wy-alert-warning.note .admonition-title, .rst-content .wy-alert-warning.note .wy-alert-title, .rst-content .wy-alert-warning.seealso .admonition-title, .rst-content .wy-alert-warning.seealso .wy-alert-title, .rst-content .wy-alert-warning.tip .admonition-title, .rst-content .wy-alert-warning.tip .wy-alert-title, .rst-content .wy-alert.wy-alert-warning .admonition-title, .wy-alert.wy-alert-warning .rst-content .admonition-title, .wy-alert.wy-alert-warning .wy-alert-title { background-color: rgb(98, 52, 11); background-image: none; } .rst-content .note, .rst-content .seealso, .rst-content .wy-alert-info.admonition, .rst-content .wy-alert-info.admonition-todo, .rst-content .wy-alert-info.attention, .rst-content .wy-alert-info.caution, .rst-content .wy-alert-info.danger, .rst-content .wy-alert-info.error, .rst-content .wy-alert-info.hint, .rst-content .wy-alert-info.important, .rst-content .wy-alert-info.tip, .rst-content .wy-alert-info.warning, .wy-alert.wy-alert-info { background-color: rgb(26, 28, 29); background-image: none; } .rst-content .note .admonition-title, .rst-content .note .wy-alert-title, .rst-content .seealso .admonition-title, .rst-content .seealso .wy-alert-title, .rst-content .wy-alert-info.admonition-todo .admonition-title, .rst-content .wy-alert-info.admonition-todo .wy-alert-title, .rst-content .wy-alert-info.admonition .admonition-title, .rst-content .wy-alert-info.admonition .wy-alert-title, .rst-content .wy-alert-info.attention .admonition-title, .rst-content .wy-alert-info.attention .wy-alert-title, .rst-content .wy-alert-info.caution .admonition-title, .rst-content .wy-alert-info.caution .wy-alert-title, .rst-content .wy-alert-info.danger .admonition-title, .rst-content .wy-alert-info.danger .wy-alert-title, .rst-content .wy-alert-info.error .admonition-title, .rst-content .wy-alert-info.error .wy-alert-title, .rst-content .wy-alert-info.hint .admonition-title, .rst-content .wy-alert-info.hint .wy-alert-title, .rst-content .wy-alert-info.important .admonition-title, .rst-content .wy-alert-info.important .wy-alert-title, .rst-content .wy-alert-info.tip .admonition-title, .rst-content .wy-alert-info.tip .wy-alert-title, .rst-content .wy-alert-info.warning .admonition-title, .rst-content .wy-alert-info.warning .wy-alert-title, .rst-content .wy-alert.wy-alert-info .admonition-title, .wy-alert.wy-alert-info .rst-content .admonition-title, .wy-alert.wy-alert-info .wy-alert-title { background-color: rgb(23, 73, 105); background-image: none; } .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .wy-alert-success.admonition, .rst-content .wy-alert-success.admonition-todo, .rst-content .wy-alert-success.attention, .rst-content .wy-alert-success.caution, .rst-content .wy-alert-success.danger, .rst-content .wy-alert-success.error, .rst-content .wy-alert-success.note, .rst-content .wy-alert-success.seealso, .rst-content .wy-alert-success.warning, .wy-alert.wy-alert-success { background-color: rgb(7, 53, 46); background-image: none; } .rst-content .hint .admonition-title, .rst-content .hint .wy-alert-title, .rst-content .important .admonition-title, .rst-content .important .wy-alert-title, .rst-content .tip .admonition-title, .rst-content .tip .wy-alert-title, .rst-content .wy-alert-success.admonition-todo .admonition-title, .rst-content .wy-alert-success.admonition-todo .wy-alert-title, .rst-content .wy-alert-success.admonition .admonition-title, .rst-content .wy-alert-success.admonition .wy-alert-title, .rst-content .wy-alert-success.attention .admonition-title, .rst-content .wy-alert-success.attention .wy-alert-title, .rst-content .wy-alert-success.caution .admonition-title, .rst-content .wy-alert-success.caution .wy-alert-title, .rst-content .wy-alert-success.danger .admonition-title, .rst-content .wy-alert-success.danger .wy-alert-title, .rst-content .wy-alert-success.error .admonition-title, .rst-content .wy-alert-success.error .wy-alert-title, .rst-content .wy-alert-success.note .admonition-title, .rst-content .wy-alert-success.note .wy-alert-title, .rst-content .wy-alert-success.seealso .admonition-title, .rst-content .wy-alert-success.seealso .wy-alert-title, .rst-content .wy-alert-success.warning .admonition-title, .rst-content .wy-alert-success.warning .wy-alert-title, .rst-content .wy-alert.wy-alert-success .admonition-title, .wy-alert.wy-alert-success .rst-content .admonition-title, .wy-alert.wy-alert-success .wy-alert-title { background-color: rgb(17, 120, 100); background-image: none; } .rst-content .wy-alert-neutral.admonition, .rst-content .wy-alert-neutral.admonition-todo, .rst-content .wy-alert-neutral.attention, .rst-content .wy-alert-neutral.caution, .rst-content .wy-alert-neutral.danger, .rst-content .wy-alert-neutral.error, .rst-content .wy-alert-neutral.hint, .rst-content .wy-alert-neutral.important, .rst-content .wy-alert-neutral.note, .rst-content .wy-alert-neutral.seealso, .rst-content .wy-alert-neutral.tip, .rst-content .wy-alert-neutral.warning, .wy-alert.wy-alert-neutral { background-color: rgb(22, 29, 29); background-image: none; } .rst-content .wy-alert-neutral.admonition-todo .admonition-title, .rst-content .wy-alert-neutral.admonition-todo .wy-alert-title, .rst-content .wy-alert-neutral.admonition .admonition-title, .rst-content .wy-alert-neutral.admonition .wy-alert-title, .rst-content .wy-alert-neutral.attention .admonition-title, .rst-content .wy-alert-neutral.attention .wy-alert-title, .rst-content .wy-alert-neutral.caution .admonition-title, .rst-content .wy-alert-neutral.caution .wy-alert-title, .rst-content .wy-alert-neutral.danger .admonition-title, .rst-content .wy-alert-neutral.danger .wy-alert-title, .rst-content .wy-alert-neutral.error .admonition-title, .rst-content .wy-alert-neutral.error .wy-alert-title, .rst-content .wy-alert-neutral.hint .admonition-title, .rst-content .wy-alert-neutral.hint .wy-alert-title, .rst-content .wy-alert-neutral.important .admonition-title, .rst-content .wy-alert-neutral.important .wy-alert-title, .rst-content .wy-alert-neutral.note .admonition-title, .rst-content .wy-alert-neutral.note .wy-alert-title, .rst-content .wy-alert-neutral.seealso .admonition-title, .rst-content .wy-alert-neutral.seealso .wy-alert-title, .rst-content .wy-alert-neutral.tip .admonition-title, .rst-content .wy-alert-neutral.tip .wy-alert-title, .rst-content .wy-alert-neutral.warning .admonition-title, .rst-content .wy-alert-neutral.warning .wy-alert-title, .rst-content .wy-alert.wy-alert-neutral .admonition-title, .wy-alert.wy-alert-neutral .rst-content .admonition-title, .wy-alert.wy-alert-neutral .wy-alert-title { color: rgb(188, 182, 173); background-color: rgb(32, 35, 36); background-image: none; } .rst-content .wy-alert-neutral.admonition-todo a, .rst-content .wy-alert-neutral.admonition a, .rst-content .wy-alert-neutral.attention a, .rst-content .wy-alert-neutral.caution a, .rst-content .wy-alert-neutral.danger a, .rst-content .wy-alert-neutral.error a, .rst-content .wy-alert-neutral.hint a, .rst-content .wy-alert-neutral.important a, .rst-content .wy-alert-neutral.note a, .rst-content .wy-alert-neutral.seealso a, .rst-content .wy-alert-neutral.tip a, .rst-content .wy-alert-neutral.warning a, .wy-alert.wy-alert-neutral a { color: rgb(94, 169, 219); } .wy-tray-container li { background-color: transparent; background-image: none; color: rgb(216, 212, 207); box-shadow: rgba(0, 0, 0, 0.1) 0px 5px 5px 0px; } .wy-tray-container li.wy-tray-item-success { background-color: rgb(25, 111, 62); background-image: none; } .wy-tray-container li.wy-tray-item-info { background-color: rgb(26, 82, 118); background-image: none; } .wy-tray-container li.wy-tray-item-warning { background-color: rgb(142, 75, 16); background-image: none; } .wy-tray-container li.wy-tray-item-danger { background-color: rgb(130, 26, 16); background-image: none; } .btn { color: rgb(216, 212, 207); border-color: rgba(84, 91, 95, 0.1); background-color: rgb(25, 111, 62); text-decoration-color: currentcolor; box-shadow: rgba(19, 21, 22, 0.5) 0px 1px 2px -1px inset, rgba(0, 0, 0, 0.1) 0px -2px 0px 0px inset; } .btn-hover { background-color: rgb(30, 91, 132); background-image: none; color: rgb(216, 212, 207); } .btn:hover { background-color: rgb(28, 125, 69); background-image: none; color: rgb(216, 212, 207); } .btn:focus { background-color: rgb(28, 125, 69); background-image: none; outline-color: currentcolor; } .btn:active { box-shadow: rgba(0, 0, 0, 0.05) 0px -1px 0px 0px inset, rgba(0, 0, 0, 0.1) 0px 2px 0px 0px inset; } .btn:visited { color: rgb(216, 212, 207); } .btn-disabled, .btn-disabled:active, .btn-disabled:focus, .btn-disabled:hover, .btn:disabled { background-image: none; box-shadow: none; } .btn-info { background-color: rgb(26, 82, 118) !important; } .btn-info:hover { background-color: rgb(30, 91, 132) !important; } .btn-neutral { background-color: rgb(22, 29, 29) !important; color: rgb(188, 182, 173) !important; } .btn-neutral:hover { color: rgb(188, 182, 173); background-color: rgb(27, 35, 35) !important; } .btn-neutral:visited { color: rgb(188, 182, 173) !important; } .btn-success { background-color: rgb(25, 111, 62) !important; } .btn-success:hover { background-color: rgb(22, 98, 54) !important; } .btn-danger { background-color: rgb(130, 26, 16) !important; } .btn-danger:hover { background-color: rgb(119, 24, 14) !important; } .btn-warning { background-color: rgb(142, 75, 16) !important; } .btn-warning:hover { background-color: rgb(132, 70, 14) !important; } .btn-invert { background-color: rgb(21, 22, 23); } .btn-invert:hover { background-color: rgb(28, 31, 32) !important; } .btn-link { color: rgb(94, 169, 219); box-shadow: none; background-color: transparent !important; border-color: transparent !important; } .btn-link:active, .btn-link:hover { box-shadow: none; background-color: transparent !important; color: rgb(90, 168, 218) !important; } .btn-link:visited { color: rgb(170, 113, 192); } .wy-dropdown-menu { background-color: rgb(21, 22, 23); background-image: none; border-color: rgb(119, 111, 98); box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 2px 0px; } .wy-dropdown-menu > dd > a { color: rgb(188, 182, 173); } .wy-dropdown-menu > dd > a:hover { background-color: rgb(26, 82, 118); background-image: none; color: rgb(216, 212, 207); } .wy-dropdown-menu > dd.divider { border-top-color: rgb(119, 111, 98); } .wy-dropdown-menu > dd.call-to-action { background-color: rgb(32, 35, 36); background-image: none; } .wy-dropdown-menu > dd.call-to-action:hover { background-color: rgb(32, 35, 36); background-image: none; } .wy-dropdown-menu > dd.call-to-action .btn { color: rgb(216, 212, 207); } .wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu { background-color: rgb(21, 22, 23); background-image: none; } .wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover { background-color: rgb(26, 82, 118); background-image: none; color: rgb(216, 212, 207); } .wy-dropdown-arrow::before { border-bottom-color: rgb(122, 113, 100); border-left-color: transparent; border-right-color: transparent; } fieldset, legend { border-color: currentcolor; } label { color: rgb(193, 188, 180); } .wy-control-group.wy-control-group-required > label::after { color: rgb(234, 96, 82); } .wy-form-message-inline { color: rgb(171, 164, 153); } .wy-form-message { color: rgb(171, 164, 153); } input[type="color"], input[type="date"], input[type="datetime-local"], input[type="datetime"], input[type="email"], input[type="month"], input[type="number"], input[type="password"], input[type="search"], input[type="tel"], input[type="text"], input[type="time"], input[type="url"], input[type="week"] { border-color: rgb(118, 110, 97); box-shadow: rgb(35, 38, 39) 0px 1px 3px inset; } input[type="color"]:focus, input[type="date"]:focus, input[type="datetime-local"]:focus, input[type="datetime"]:focus, input[type="email"]:focus, input[type="month"]:focus, input[type="number"]:focus, input[type="password"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="text"]:focus, input[type="time"]:focus, input[type="url"]:focus, input[type="week"]:focus { outline-color: currentcolor; border-color: rgb(103, 96, 85); } input.no-focus:focus { border-color: rgb(118, 110, 97) !important; } input[type="checkbox"]:focus, input[type="file"]:focus, input[type="radio"]:focus { outline-color: rgb(15, 126, 186); } input[type="color"][disabled], input[type="date"][disabled], input[type="datetime-local"][disabled], input[type="datetime"][disabled], input[type="email"][disabled], input[type="month"][disabled], input[type="number"][disabled], input[type="password"][disabled], input[type="search"][disabled], input[type="tel"][disabled], input[type="text"][disabled], input[type="time"][disabled], input[type="url"][disabled], input[type="week"][disabled] { background-color: rgb(21, 23, 24); } input:focus:invalid, select:focus:invalid, textarea:focus:invalid { color: rgb(234, 96, 82); border-color: rgb(183, 38, 22); } input:focus:invalid:focus, select:focus:invalid:focus, textarea:focus:invalid:focus { border-color: rgb(183, 38, 22); } input[type="checkbox"]:focus:invalid:focus, input[type="file"]:focus:invalid:focus, input[type="radio"]:focus:invalid:focus { outline-color: rgb(183, 38, 22); } select, textarea { border-color: rgb(118, 110, 97); box-shadow: rgb(35, 38, 39) 0px 1px 3px inset; } select { border-color: rgb(118, 110, 97); background-color: rgb(19, 21, 22); } select:focus, textarea:focus { outline-color: currentcolor; } input[readonly], select[disabled], select[readonly], textarea[disabled], textarea[readonly] { background-color: rgb(21, 23, 24); } .wy-checkbox, .wy-radio { color: rgb(188, 182, 173); } .wy-input-prefix .wy-input-context, .wy-input-suffix .wy-input-context { background-color: rgb(22, 29, 29); border-color: rgb(118, 110, 97); color: rgb(171, 164, 153); } .wy-input-suffix .wy-input-context { border-left-color: currentcolor; } .wy-input-prefix .wy-input-context { border-right-color: currentcolor; } .wy-switch::before { background-color: rgb(42, 46, 47); background-image: none; } .wy-switch::after { background-color: rgb(66, 71, 74); background-image: none; } .wy-switch span { color: rgb(193, 188, 180); } .wy-switch.active::before { background-color: rgb(19, 85, 46); background-image: none; } .wy-switch.active::after { background-color: rgb(25, 111, 62); background-image: none; } .wy-control-group.wy-control-group-error .wy-form-message, .wy-control-group.wy-control-group-error > label { color: rgb(234, 96, 82); } .wy-control-group.wy-control-group-error input[type="color"], .wy-control-group.wy-control-group-error input[type="date"], .wy-control-group.wy-control-group-error input[type="datetime-local"], .wy-control-group.wy-control-group-error input[type="datetime"], .wy-control-group.wy-control-group-error input[type="email"], .wy-control-group.wy-control-group-error input[type="month"], .wy-control-group.wy-control-group-error input[type="number"], .wy-control-group.wy-control-group-error input[type="password"], .wy-control-group.wy-control-group-error input[type="search"], .wy-control-group.wy-control-group-error input[type="tel"], .wy-control-group.wy-control-group-error input[type="text"], .wy-control-group.wy-control-group-error input[type="time"], .wy-control-group.wy-control-group-error input[type="url"], .wy-control-group.wy-control-group-error input[type="week"], .wy-control-group.wy-control-group-error textarea { border-color: rgb(183, 38, 22); } .wy-inline-validate.wy-inline-validate-success .wy-input-context { color: rgb(99, 220, 150); } .wy-inline-validate.wy-inline-validate-danger .wy-input-context { color: rgb(234, 96, 82); } .wy-inline-validate.wy-inline-validate-warning .wy-input-context { color: rgb(234, 146, 69); } .wy-inline-validate.wy-inline-validate-info .wy-input-context { color: rgb(94, 169, 219); } .rst-content table.docutils caption, .rst-content table.field-list caption, .wy-table caption { color: rgb(216, 212, 207); } .rst-content table.docutils thead, .rst-content table.field-list thead, .wy-table thead { color: rgb(216, 212, 207); } .rst-content table.docutils thead th, .rst-content table.field-list thead th, .wy-table thead th { border-bottom-color: rgb(120, 112, 99); } .rst-content table.docutils td, .rst-content table.field-list td, .wy-table td { background-color: transparent; } .wy-table-secondary { color: rgb(160, 151, 139); } .wy-table-tertiary { color: rgb(160, 151, 139); } .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td, .wy-table-backed, .wy-table-odd td, .wy-table-striped tr:nth-child(2n-1) td { background-color: rgb(22, 29, 29); } .rst-content table.docutils, .wy-table-bordered-all { border-color: rgb(120, 112, 99); } .rst-content table.docutils td, .wy-table-bordered-all td { border-bottom-color: rgb(120, 112, 99); border-left-color: rgb(120, 112, 99); } .wy-table-bordered { border-color: rgb(120, 112, 99); } .wy-table-bordered-rows td { border-bottom-color: rgb(120, 112, 99); } .wy-table-horizontal td, .wy-table-horizontal th { border-bottom-color: rgb(120, 112, 99); } a { color: rgb(94, 169, 219); text-decoration-color: currentcolor; } a:hover { color: rgb(82, 164, 217); } a:visited { color: rgb(170, 113, 192); } body { color: rgb(188, 182, 173); background-color: rgb(26, 29, 30); background-image: none; } .wy-text-strike { text-decoration-color: currentcolor; } .wy-text-warning { color: rgb(234, 146, 69) !important; } a.wy-text-warning:hover { color: rgb(237, 160, 92) !important; } .wy-text-info { color: rgb(94, 169, 219) !important; } a.wy-text-info:hover { color: rgb(90, 168, 218) !important; } .wy-text-success { color: rgb(99, 220, 150) !important; } a.wy-text-success:hover { color: rgb(86, 217, 142) !important; } .wy-text-danger { color: rgb(234, 96, 82) !important; } a.wy-text-danger:hover { color: rgb(237, 118, 104) !important; } .wy-text-neutral { color: rgb(188, 182, 173) !important; } a.wy-text-neutral:hover { color: rgb(177, 170, 160) !important; } hr { border-color: rgb(120, 112, 99) currentcolor currentcolor; } .rst-content code, .rst-content tt, code { background-color: rgb(19, 21, 22); background-image: none; border-color: rgb(120, 112, 99); color: rgb(234, 96, 82); } .rst-content .section ul, .rst-content .toctree-wrapper ul, .wy-plain-list-disc, article ul { list-style-image: none; } .rst-content .section ul li, .rst-content .toctree-wrapper ul li, .wy-plain-list-disc li, article ul li { list-style-image: none; } .rst-content .section ul li li, .rst-content .toctree-wrapper ul li li, .wy-plain-list-disc li li, article ul li li { list-style-image: none; } .rst-content .section ul li li li, .rst-content .toctree-wrapper ul li li li, .wy-plain-list-disc li li li, article ul li li li { list-style-image: none; } .rst-content .section ul li ol li, .rst-content .toctree-wrapper ul li ol li, .wy-plain-list-disc li ol li, article ul li ol li { list-style-image: none; } .rst-content .section ol, .rst-content ol.arabic, .wy-plain-list-decimal, article ol { list-style-image: none; } .rst-content .section ol li, .rst-content ol.arabic li, .wy-plain-list-decimal li, article ol li { list-style-image: none; } .rst-content .section ol li ul li, .rst-content ol.arabic li ul li, .wy-plain-list-decimal li ul li, article ol li ul li { list-style-image: none; } .rst-content .wy-breadcrumbs li tt, .wy-breadcrumbs li .rst-content tt, .wy-breadcrumbs li code { border-color: currentcolor; background-color: rgba(0, 0, 0, 0); background-image: none; } .rst-content .wy-breadcrumbs li tt.literal, .wy-breadcrumbs li .rst-content tt.literal, .wy-breadcrumbs li code.literal { color: rgb(188, 182, 173); } .wy-breadcrumbs-extra { color: rgb(182, 176, 167); } .wy-menu a:hover { text-decoration-color: currentcolor; } .wy-menu-horiz li:hover { background-color: rgba(19, 21, 22, 0.1); background-image: none; } .wy-menu-horiz li.divide-left { border-left-color: rgb(104, 97, 86); } .wy-menu-horiz li.divide-right { border-right-color: rgb(104, 97, 86); } .wy-menu-vertical header, .wy-menu-vertical p.caption { color: rgb(101, 173, 220); } .wy-menu-vertical li.divide-top { border-top-color: rgb(104, 97, 86); } .wy-menu-vertical li.divide-bottom { border-bottom-color: rgb(104, 97, 86); } .wy-menu-vertical li.current { background-color: rgb(32, 35, 36); background-image: none; } .wy-menu-vertical li.current a { color: rgb(160, 151, 139); border-right-color: rgb(118, 110, 97); } .wy-menu-vertical li.current a:hover { background-color: rgb(38, 41, 42); background-image: none; } .rst-content .wy-menu-vertical li tt, .wy-menu-vertical li .rst-content tt, .wy-menu-vertical li code { border-color: currentcolor; background-color: inherit; background-image: inherit; color: inherit; } .wy-menu-vertical li span.toctree-expand { color: rgb(182, 175, 166); } .wy-menu-vertical li.current > a, .wy-menu-vertical li.on a { color: rgb(188, 182, 173); background-color: rgb(21, 22, 23); background-image: none; border-color: currentcolor; } .wy-menu-vertical li.current > a:hover, .wy-menu-vertical li.on a:hover { background-color: rgb(21, 22, 23); background-image: none; } .wy-menu-vertical li.current > a:hover span.toctree-expand, .wy-menu-vertical li.on a:hover span.toctree-expand { color: rgb(160, 151, 139); } .wy-menu-vertical li.current > a span.toctree-expand, .wy-menu-vertical li.on a span.toctree-expand { color: rgb(193, 188, 180); } .wy-menu-vertical li.toctree-l1.current > a { border-bottom-color: rgb(118, 110, 97); border-top-color: rgb(118, 110, 97); } .wy-menu-vertical li.toctree-l2 a, .wy-menu-vertical li.toctree-l3 a, .wy-menu-vertical li.toctree-l4 a, .wy-menu-vertical li.toctree-l5 a, .wy-menu-vertical li.toctree-l6 a, .wy-menu-vertical li.toctree-l7 a, .wy-menu-vertical li.toctree-l8 a, .wy-menu-vertical li.toctree-l9 a, .wy-menu-vertical li.toctree-l10 a { color: rgb(188, 182, 173); } .wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l4 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l5 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l6 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l7 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l8 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l9 a:hover span.toctree-expand, .wy-menu-vertical li.toctree-l10 a:hover span.toctree-expand { color: rgb(160, 151, 139); } .wy-menu-vertical li.toctree-l2.current > a, .wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { background-color: rgb(43, 47, 49); background-image: none; } .wy-menu-vertical li.toctree-l2 span.toctree-expand { color: rgb(175, 168, 158); } .wy-menu-vertical li.toctree-l3.current > a, .wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a { background-color: rgb(49, 53, 55); background-image: none; } .wy-menu-vertical li.toctree-l3 span.toctree-expand { color: rgb(169, 162, 151); } .wy-menu-vertical li ul li a { color: rgb(199, 194, 187); } .wy-menu-vertical a { color: rgb(199, 194, 187); } .wy-menu-vertical a:hover { background-color: rgb(46, 49, 51); } .wy-menu-vertical a:hover span.toctree-expand { color: rgb(199, 194, 187); } .wy-menu-vertical a:active { background-color: rgb(26, 82, 118); color: rgb(216, 212, 207); } .wy-menu-vertical a:active span.toctree-expand { color: rgb(216, 212, 207); } .wy-side-nav-search { background-color: rgb(26, 82, 118); color: rgb(215, 211, 206); } .wy-side-nav-search input[type="text"] { border-color: rgb(35, 112, 161); } .wy-side-nav-search img { background-color: rgb(26, 82, 118); } .wy-side-nav-search .wy-dropdown > a, .wy-side-nav-search > a { color: rgb(215, 211, 206); } .wy-side-nav-search .wy-dropdown > a:hover, .wy-side-nav-search > a:hover { background-color: rgba(19, 21, 22, 0.1); background-image: none; } .wy-side-nav-search .wy-dropdown > a img.logo, .wy-side-nav-search > a img.logo { background-color: transparent; background-image: none; } .wy-side-nav-search > div.version { color: rgba(216, 212, 207, 0.3); } .wy-nav .wy-menu-vertical header { color: rgb(94, 169, 219); } .wy-nav .wy-menu-vertical a { color: rgb(182, 176, 167); } .wy-nav .wy-menu-vertical a:hover { background-color: rgb(26, 82, 118); color: rgb(216, 212, 207); } .wy-body-for-nav { background-color: rgb(21, 22, 23); background-image: none; } .wy-nav-side { color: rgb(172, 164, 154); background-color: rgb(30, 33, 34); background-image: none; } .wy-nav-top { background-color: rgb(26, 82, 118); background-image: none; color: rgb(216, 212, 207); } .wy-nav-top a { color: rgb(216, 212, 207); } .wy-nav-top img { background-color: rgb(26, 82, 118); } .wy-nav-content-wrap { background-color: rgb(21, 22, 23); background-image: none; } .wy-body-mask { background-color: rgba(0, 0, 0, 0.2); background-image: none; } footer { color: rgb(160, 151, 139); } .rst-content footer span.commit tt, footer span.commit .rst-content tt, footer span.commit code { background-color: rgba(0, 0, 0, 0); background-image: none; border-color: currentcolor; color: rgb(160, 151, 139); } #search-results .search li { border-bottom-color: rgb(120, 112, 99); } #search-results .search li:first-child { border-top-color: rgb(120, 112, 99); } #search-results .context { color: rgb(160, 151, 139); } .wy-body-for-nav { background-color: rgb(21, 22, 23); background-image: none; } @media screen and (min-width: 1100px) { .wy-nav-content-wrap { background-color: rgba(0, 0, 0, 0.05); background-image: none; } .wy-nav-content { background-color: rgb(21, 22, 23); background-image: none; } } .rst-versions { color: rgb(215, 211, 206); background-color: rgb(18, 20, 20); background-image: none; } .rst-versions a { color: rgb(94, 169, 219); text-decoration-color: currentcolor; } .rst-versions .rst-current-version { background-color: rgb(23, 25, 26); color: rgb(99, 220, 150); } .rst-content .code-block-caption .rst-versions .rst-current-version .headerlink, .rst-content .rst-versions .rst-current-version .admonition-title, .rst-content code.download .rst-versions .rst-current-version span:first-child, .rst-content dl dt .rst-versions .rst-current-version .headerlink, .rst-content h1 .rst-versions .rst-current-version .headerlink, .rst-content h2 .rst-versions .rst-current-version .headerlink, .rst-content h3 .rst-versions .rst-current-version .headerlink, .rst-content h4 .rst-versions .rst-current-version .headerlink, .rst-content h5 .rst-versions .rst-current-version .headerlink, .rst-content h6 .rst-versions .rst-current-version .headerlink, .rst-content p.caption .rst-versions .rst-current-version .headerlink, .rst-content table > caption .rst-versions .rst-current-version .headerlink, .rst-content tt.download .rst-versions .rst-current-version span:first-child, .rst-versions .rst-current-version .fa, .rst-versions .rst-current-version .icon, .rst-versions .rst-current-version .rst-content .admonition-title, .rst-versions .rst-current-version .rst-content .code-block-caption .headerlink, .rst-versions .rst-current-version .rst-content code.download span:first-child, .rst-versions .rst-current-version .rst-content dl dt .headerlink, .rst-versions .rst-current-version .rst-content h1 .headerlink, .rst-versions .rst-current-version .rst-content h2 .headerlink, .rst-versions .rst-current-version .rst-content h3 .headerlink, .rst-versions .rst-current-version .rst-content h4 .headerlink, .rst-versions .rst-current-version .rst-content h5 .headerlink, .rst-versions .rst-current-version .rst-content h6 .headerlink, .rst-versions .rst-current-version .rst-content p.caption .headerlink, .rst-versions .rst-current-version .rst-content table > caption .headerlink, .rst-versions .rst-current-version .rst-content tt.download span:first-child, .rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand, .wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand { color: rgb(215, 211, 206); } .rst-versions .rst-current-version.rst-out-of-date { background-color: rgb(130, 26, 16); color: rgb(216, 212, 207); } .rst-versions .rst-current-version.rst-active-old-version { background-color: rgb(154, 125, 9); color: rgb(216, 212, 207); } .rst-versions .rst-other-versions { color: rgb(160, 151, 139); } .rst-versions .rst-other-versions hr { border-color: rgb(104, 97, 86) currentcolor currentcolor; } .rst-versions .rst-other-versions dd a { color: rgb(215, 211, 206); } .rst-versions.rst-badge { border-color: currentcolor; } .rst-content abbr[title] { text-decoration-color: currentcolor; } .rst-content.style-external-links a.reference.external::after { color: rgb(182, 176, 167); } .rst-content div[class^="highlight"], .rst-content pre.literal-block { border-color: rgb(120, 112, 99); } .rst-content div[class^="highlight"] div[class^="highlight"], .rst-content pre.literal-block div[class^="highlight"] { border-color: currentcolor; } .rst-content .linenodiv pre { border-right-color: rgb(121, 112, 99); } .rst-content .admonition table { border-color: rgba(84, 91, 95, 0.1); } .rst-content .admonition table td, .rst-content .admonition table th { background-color: transparent !important; background-image: none !important; border-color: rgba(84, 91, 95, 0.1) !important; } .rst-content .section ol.loweralpha, .rst-content .section ol.loweralpha > li { list-style-image: none; } .rst-content .section ol.upperalpha, .rst-content .section ol.upperalpha > li { list-style-image: none; } .rst-content .toc-backref { color: rgb(188, 182, 173); } .rst-content .sidebar { background-color: rgb(22, 29, 29); background-image: none; border-color: rgb(120, 112, 99); } .rst-content .sidebar .sidebar-title { background-color: rgb(32, 35, 36); background-image: none; } .rst-content .highlighted { background-color: rgb(154, 125, 9); background-image: none; box-shadow: rgb(154, 125, 9) 0px 0px 0px 2px; } html.writer-html4 .rst-content table.docutils.citation, html.writer-html4 .rst-content table.docutils.footnote { background-color: rgba(0, 0, 0, 0); background-image: none; border-color: currentcolor; } html.writer-html4 .rst-content table.docutils.citation td, html.writer-html4 .rst-content table.docutils.citation tr, html.writer-html4 .rst-content table.docutils.footnote td, html.writer-html4 .rst-content table.docutils.footnote tr { border-color: currentcolor; background-color: transparent !important; } .rst-content table.docutils.footnote, html.writer-html4 .rst-content table.docutils.citation, html.writer-html5 .rst-content dl.footnote { color: rgb(160, 151, 139); } .rst-content table.docutils.footnote code, .rst-content table.docutils.footnote tt, html.writer-html4 .rst-content table.docutils.citation code, html.writer-html4 .rst-content table.docutils.citation tt, html.writer-html5 .rst-content dl.footnote code, html.writer-html5 .rst-content dl.footnote tt { color: rgb(178, 172, 162); } .rst-content table.docutils th { border-color: rgb(120, 112, 99); } html.writer-html5 .rst-content table.docutils th { border-color: rgb(120, 112, 99); } .rst-content table.field-list, .rst-content table.field-list td { border-color: currentcolor; } .rst-content code, .rst-content tt { color: rgb(216, 212, 207); } .rst-content code.literal, .rst-content tt.literal { color: rgb(227, 134, 124); } .rst-content code.xref, .rst-content tt.xref, a .rst-content code, a .rst-content tt { color: rgb(188, 182, 173); } .rst-content a code, .rst-content a tt { color: rgb(94, 169, 219); } html.writer-html4 .rst-content dl:not(.docutils) > dt, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) > dt { background-color: rgb(26, 28, 29); background-image: none; color: rgb(94, 169, 219); border-top-color: rgb(37, 119, 171); } html.writer-html4 .rst-content dl:not(.docutils) > dt::before, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) > dt::before { color: rgb(111, 179, 223); } html.writer-html4 .rst-content dl:not(.docutils) > dt .headerlink, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) > dt .headerlink { color: rgb(188, 182, 173); } html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list) > dt, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list) > dt { border-color: currentcolor currentcolor currentcolor rgb(118, 110, 97); background-color: rgb(26, 28, 29); background-image: none; color: rgb(178, 172, 162); } html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list) > dt .headerlink, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list) > dt .headerlink { color: rgb(188, 182, 173); } html.writer-html4 .rst-content dl:not(.docutils) code.descclassname, html.writer-html4 .rst-content dl:not(.docutils) code.descname, html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname, html.writer-html4 .rst-content dl:not(.docutils) tt.descname, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descclassname, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descclassname, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname { background-color: transparent; border-color: currentcolor; } html.writer-html4 .rst-content dl:not(.docutils) .optional, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .optional { color: rgb(216, 212, 207); } .rst-content .viewcode-back, .rst-content .viewcode-link { color: rgb(99, 220, 150); } .rst-content code.download, .rst-content tt.download { background-color: inherit; background-image: inherit; color: inherit; border-color: inherit; } .rst-content .guilabel { border-color: rgb(38, 119, 172); background-color: rgb(26, 28, 29); background-image: none; } span[id*="MathJax-Span"] { color: rgb(188, 182, 173); } td.linenos .normal { color: inherit; background-color: transparent; } span.linenos { color: inherit; background-color: transparent; } td.linenos .special { color: rgb(216, 212, 207); background-color: rgb(71, 71, 0); } span.linenos.special { color: rgb(216, 212, 207); background-color: rgb(71, 71, 0); } .highlight .hll { background-color: rgb(66, 66, 0); } .highlight { background-color: rgb(32, 34, 36); background-image: none; } .highlight .c { color: rgb(124, 182, 197); } .highlight .err { border-color: rgb(201, 0, 0); } .highlight .k { color: rgb(114, 255, 154); } .highlight .o { color: rgb(171, 164, 153); } .highlight .ch { color: rgb(124, 182, 197); } .highlight .cm { color: rgb(124, 182, 197); } .highlight .cp { color: rgb(114, 255, 154); } .highlight .cpf { color: rgb(124, 182, 197); } .highlight .c1 { color: rgb(124, 182, 197); } .highlight .cs { color: rgb(124, 182, 197); background-color: rgb(48, 0, 0); } .highlight .gd { color: rgb(255, 90, 90); } .highlight .gr { color: rgb(255, 44, 44); } .highlight .gh { color: rgb(114, 185, 255); } .highlight .gi { color: rgb(90, 255, 90); } .highlight .go { color: rgb(193, 188, 180); } .highlight .gp { color: rgb(246, 151, 75); } .highlight .gu { color: rgb(255, 105, 255); } .highlight .gt { color: rgb(75, 173, 255); } .highlight .kc { color: rgb(114, 255, 154); } .highlight .kd { color: rgb(114, 255, 154); } .highlight .kn { color: rgb(114, 255, 154); } .highlight .kp { color: rgb(114, 255, 154); } .highlight .kr { color: rgb(114, 255, 154); } .highlight .kt { color: rgb(255, 133, 98); } .highlight .m { color: rgb(123, 222, 173); } .highlight .s { color: rgb(126, 170, 203); } .highlight .na { color: rgb(126, 170, 203); } .highlight .nb { color: rgb(114, 255, 154); } .highlight .nc { color: rgb(86, 196, 242); } .highlight .no { color: rgb(108, 180, 216); } .highlight .nd { color: rgb(178, 172, 162); } .highlight .ni { color: rgb(220, 111, 85); } .highlight .ne { color: rgb(114, 255, 154); } .highlight .nf { color: rgb(120, 189, 248); } .highlight .nl { color: rgb(121, 194, 255); } .highlight .nn { color: rgb(86, 196, 242); } .highlight .nt { color: rgb(125, 192, 248); } .highlight .nv { color: rgb(192, 108, 216); } .highlight .ow { color: rgb(114, 255, 154); } .highlight .w { color: rgb(186, 180, 171); } .highlight .mb { color: rgb(123, 222, 173); } .highlight .mf { color: rgb(123, 222, 173); } .highlight .mh { color: rgb(123, 222, 173); } .highlight .mi { color: rgb(123, 222, 173); } .highlight .mo { color: rgb(123, 222, 173); } .highlight .sa { color: rgb(126, 170, 203); } .highlight .sb { color: rgb(126, 170, 203); } .highlight .sc { color: rgb(126, 170, 203); } .highlight .dl { color: rgb(126, 170, 203); } .highlight .sd { color: rgb(126, 170, 203); } .highlight .s2 { color: rgb(126, 170, 203); } .highlight .se { color: rgb(126, 170, 203); } .highlight .sh { color: rgb(126, 170, 203); } .highlight .si { color: rgb(120, 172, 210); } .highlight .sx { color: rgb(246, 151, 75); } .highlight .sr { color: rgb(129, 182, 223); } .highlight .s1 { color: rgb(126, 170, 203); } .highlight .ss { color: rgb(186, 229, 123); } .highlight .bp { color: rgb(114, 255, 154); } .highlight .fm { color: rgb(120, 189, 248); } .highlight .vc { color: rgb(192, 108, 216); } .highlight .vg { color: rgb(192, 108, 216); } .highlight .vi { color: rgb(192, 108, 216); } .highlight .vm { color: rgb(192, 108, 216); } .highlight .il { color: rgb(123, 222, 173); } .rst-other-versions a { border-color: currentcolor; } .ethical-sidebar .ethical-image-link, .ethical-footer .ethical-image-link { border-color: currentcolor; } .ethical-sidebar, .ethical-footer { background-color: rgb(27, 29, 30); border-color: rgb(118, 110, 97); color: rgb(211, 208, 202); } .ethical-sidebar ul { list-style-image: none; } .ethical-sidebar ul li { background-color: rgb(4, 62, 97); color: rgb(216, 212, 207); } .ethical-sidebar a, .ethical-sidebar a:visited, .ethical-sidebar a:hover, .ethical-sidebar a:active, .ethical-footer a, .ethical-footer a:visited, .ethical-footer a:hover, .ethical-footer a:active { color: rgb(211, 208, 202); text-decoration-color: currentcolor !important; border-bottom-color: currentcolor !important; } .ethical-callout a { color: rgb(166, 159, 147) !important; text-decoration-color: currentcolor !important; } .ethical-fixedfooter { background-color: rgb(27, 29, 30); border-top-color: rgb(117, 109, 96); color: rgb(188, 182, 173); } .ethical-fixedfooter .ethical-text::before { background-color: rgb(49, 112, 51); color: rgb(216, 212, 207); } .ethical-fixedfooter .ethical-callout { color: rgb(171, 164, 153); } .ethical-fixedfooter a, .ethical-fixedfooter a:hover, .ethical-fixedfooter a:active, .ethical-fixedfooter a:visited { color: rgb(188, 182, 173); text-decoration-color: currentcolor; } .ethical-rtd .ethical-sidebar { color: rgb(182, 176, 167); } .ethical-alabaster a.ethical-image-link { border-color: currentcolor !important; } .ethical-dark-theme .ethical-sidebar { background-color: rgb(46, 50, 52); border-color: rgb(114, 106, 93); color: rgb(189, 183, 174) !important; } .ethical-dark-theme a, .ethical-dark-theme a:visited { color: rgb(205, 200, 194) !important; border-bottom-color: currentcolor !important; } .ethical-dark-theme .ethical-callout a { color: rgb(182, 176, 167) !important; } .keep-us-sustainable { border-color: rgb(104, 158, 45); } .keep-us-sustainable a, .keep-us-sustainable a:hover, .keep-us-sustainable a:visited { text-decoration-color: currentcolor; } .wy-nav-side .keep-us-sustainable { color: rgb(182, 176, 167); } .wy-nav-side .keep-us-sustainable a { color: rgb(209, 205, 199); } [data-ea-publisher].loaded a, [data-ea-type].loaded a { text-decoration-color: currentcolor; } [data-ea-publisher].loaded .ea-content, [data-ea-type].loaded .ea-content { background-color: rgba(0, 0, 0, 0.03); background-image: none; color: rgb(181, 174, 164); } [data-ea-publisher].loaded .ea-content a:link, [data-ea-type].loaded .ea-content a:link { color: rgb(181, 174, 164); } [data-ea-publisher].loaded .ea-content a:visited, [data-ea-type].loaded .ea-content a:visited { color: rgb(181, 174, 164); } [data-ea-publisher].loaded .ea-content a:hover, [data-ea-type].loaded .ea-content a:hover { color: rgb(192, 186, 178); } [data-ea-publisher].loaded .ea-content a:active, [data-ea-type].loaded .ea-content a:active { color: rgb(192, 186, 178); } [data-ea-publisher].loaded .ea-content a strong, [data-ea-publisher].loaded .ea-content a b, [data-ea-type].loaded .ea-content a strong, [data-ea-type].loaded .ea-content a b { color: rgb(64, 180, 248); } [data-ea-publisher].loaded .ea-callout a:link, [data-ea-type].loaded .ea-callout a:link { color: rgb(169, 162, 151); } [data-ea-publisher].loaded .ea-callout a:visited, [data-ea-type].loaded .ea-callout a:visited { color: rgb(169, 162, 151); } [data-ea-publisher].loaded .ea-callout a:hover, [data-ea-type].loaded .ea-callout a:hover { color: rgb(181, 174, 164); } [data-ea-publisher].loaded .ea-callout a:active, [data-ea-type].loaded .ea-callout a:active { color: rgb(181, 174, 164); } [data-ea-publisher].loaded .ea-callout a strong, [data-ea-publisher].loaded .ea-callout a b, [data-ea-type].loaded .ea-callout a strong, [data-ea-type].loaded .ea-callout a b { color: rgb(64, 180, 248); } [data-ea-publisher].loaded.dark .ea-content, [data-ea-type].loaded.dark .ea-content { background-color: rgba(19, 21, 22, 0.05); background-image: none; color: rgb(200, 196, 189); } [data-ea-publisher].loaded.dark .ea-content a:link, [data-ea-type].loaded.dark .ea-content a:link { color: rgb(200, 196, 189); } [data-ea-publisher].loaded.dark .ea-content a:visited, [data-ea-type].loaded.dark .ea-content a:visited { color: rgb(200, 196, 189); } [data-ea-publisher].loaded.dark .ea-content a:hover, [data-ea-type].loaded.dark .ea-content a:hover { color: rgb(212, 208, 202); } [data-ea-publisher].loaded.dark .ea-content a:active, [data-ea-type].loaded.dark .ea-content a:active { color: rgb(212, 208, 202); } [data-ea-publisher].loaded.dark .ea-content a strong, [data-ea-publisher].loaded.dark .ea-content a b, [data-ea-type].loaded.dark .ea-content a strong, [data-ea-type].loaded.dark .ea-content a b { color: rgb(85, 188, 249); } [data-ea-publisher].loaded.dark .ea-callout a:link, [data-ea-type].loaded.dark .ea-callout a:link { color: rgb(189, 184, 175); } [data-ea-publisher].loaded.dark .ea-callout a:visited, [data-ea-type].loaded.dark .ea-callout a:visited { color: rgb(189, 184, 175); } [data-ea-publisher].loaded.dark .ea-callout a:hover, [data-ea-type].loaded.dark .ea-callout a:hover { color: rgb(200, 196, 189); } [data-ea-publisher].loaded.dark .ea-callout a:active, [data-ea-type].loaded.dark .ea-callout a:active { color: rgb(200, 196, 189); } [data-ea-publisher].loaded.dark .ea-callout a strong, [data-ea-publisher].loaded.dark .ea-callout a b, [data-ea-type].loaded.dark .ea-callout a strong, [data-ea-type].loaded.dark .ea-callout a b { color: rgb(85, 188, 249); } @media (prefers-color-scheme: dark) { [data-ea-publisher].loaded.adaptive .ea-content, [data-ea-type].loaded.adaptive .ea-content { background-color: rgba(19, 21, 22, 0.05); background-image: none; color: rgb(200, 196, 189); } [data-ea-publisher].loaded.adaptive .ea-content a:link, [data-ea-type].loaded.adaptive .ea-content a:link { color: rgb(200, 196, 189); } [data-ea-publisher].loaded.adaptive .ea-content a:visited, [data-ea-type].loaded.adaptive .ea-content a:visited { color: rgb(200, 196, 189); } [data-ea-publisher].loaded.adaptive .ea-content a:hover, [data-ea-type].loaded.adaptive .ea-content a:hover { color: rgb(212, 208, 202); } [data-ea-publisher].loaded.adaptive .ea-content a:active, [data-ea-type].loaded.adaptive .ea-content a:active { color: rgb(212, 208, 202); } [data-ea-publisher].loaded.adaptive .ea-content a strong, [data-ea-publisher].loaded.adaptive .ea-content a b, [data-ea-type].loaded.adaptive .ea-content a strong, [data-ea-type].loaded.adaptive .ea-content a b { color: rgb(85, 188, 249); } [data-ea-publisher].loaded.adaptive .ea-callout a:link, [data-ea-type].loaded.adaptive .ea-callout a:link { color: rgb(189, 184, 175); } [data-ea-publisher].loaded.adaptive .ea-callout a:visited, [data-ea-type].loaded.adaptive .ea-callout a:visited { color: rgb(189, 184, 175); } [data-ea-publisher].loaded.adaptive .ea-callout a:hover, [data-ea-type].loaded.adaptive .ea-callout a:hover { color: rgb(200, 196, 189); } [data-ea-publisher].loaded.adaptive .ea-callout a:active, [data-ea-type].loaded.adaptive .ea-callout a:active { color: rgb(200, 196, 189); } [data-ea-publisher].loaded.adaptive .ea-callout a strong, [data-ea-publisher].loaded.adaptive .ea-callout a b, [data-ea-type].loaded.adaptive .ea-callout a strong, [data-ea-type].loaded.adaptive .ea-callout a b { color: rgb(85, 188, 249); } } [data-ea-publisher].loaded .ea-content, [data-ea-type].loaded .ea-content { border-color: currentcolor; box-shadow: rgba(0, 0, 0, 0.15) 0px 2px 3px; } [data-ea-publisher].loaded.raised .ea-content, [data-ea-type].loaded.raised .ea-content { border-color: currentcolor; box-shadow: rgba(0, 0, 0, 0.15) 0px 2px 3px; } [data-ea-publisher].loaded.bordered .ea-content, [data-ea-type].loaded.bordered .ea-content { border-color: rgba(84, 91, 95, 0.04); box-shadow: none; } [data-ea-publisher].loaded.bordered.dark .ea-content, [data-ea-type].loaded.bordered.dark .ea-content { border-color: rgba(123, 114, 101, 0.07); } @media (prefers-color-scheme: dark) { [data-ea-publisher].loaded.bordered.adaptive .ea-content, [data-ea-type].loaded.bordered.adaptive .ea-content { border-color: rgba(123, 114, 101, 0.07); } } [data-ea-publisher].loaded.flat .ea-content, [data-ea-type].loaded.flat .ea-content { border-color: currentcolor; box-shadow: none; } .vimvixen-hint { background-color: rgb(98, 66, 0) !important; border-color: rgb(170, 138, 15) !important; color: rgb(237, 221, 175) !important; } #edge-translate-panel-body { color: var(--darkreader-text--darkreader-neutral-text) !important; } } /* Override Style */ .vimvixen-hint { background-color: #7b5300 !important; border-color: #d8b013 !important; color: #f3e8c8 !important; } ::placeholder { opacity: 0.5 !important; } a[href="https://coinmarketcap.com/"] > svg[width="94"][height="16"] > path { fill: var(--darkreader-neutral-text) !important; } #edge-translate-panel-body { color: var(--darkreader-neutral-text) !important; } }python-telegram-bot-13.11/docs/source/changelog.rst000066400000000000000000000000361417656324400223440ustar00rootroot00000000000000.. include:: ../../CHANGES.rstpython-telegram-bot-13.11/docs/source/conf.py000066400000000000000000000234501417656324400211670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Python Telegram Bot documentation build configuration file, created by # sphinx-quickstart on Mon Aug 10 22:25:07 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # import telegram # 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. sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '3.5.2' # 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' ] # Don't show type hints in the signature - that just makes it hardly readable # and we document the types anyway autodoc_typehints = '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', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'python-telegram-bot' copyright = u'2015-2021, Leandro Toledo' author = u'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 = '13.11' # telegram.__version__[:3] # The full version, including alpha/beta/rc tags. release = '13.11' # telegram.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # 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 = 'sphinx_rtd_theme' # 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 = { 'style_external_links': True, } # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = 'ptb-logo-orange.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-orange.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'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'python-telegram-bot-doc' # -- 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', u'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' # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- 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', u'python-telegram-bot Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- 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', u'python-telegram-bot Documentation', author, 'python-telegram-bot', "We have made you a wrapper you can't refuse", 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # Napoleon stuff napoleon_use_admonition_for_examples = True # -- script stuff -------------------------------------------------------- def autodoc_skip_member(app, what, name, obj, skip, options): pass def setup(app): app.add_css_file("dark.css") app.connect('autodoc-skip-member', autodoc_skip_member) python-telegram-bot-13.11/docs/source/index.rst000066400000000000000000000025521417656324400215310ustar00rootroot00000000000000.. 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. Welcome to Python Telegram Bot's documentation! =============================================== Guides and tutorials ==================== If you're just starting out with the library, we recommend following our `"Your first Bot" `_ tutorial that you can find on our `wiki `_. On our wiki you will also find guides like how to use handlers, webhooks, emoji, proxies and much more. Examples ======== A great way to learn is by looking at examples. Ours can be found in our `examples folder on Github `_. Reference ========= Below you can find a reference of all the classes and methods in python-telegram-bot. Apart from the `telegram.ext` package the objects should reflect the types defined in the `official Telegram Bot API documentation `_. .. toctree:: telegram.ext .. toctree:: telegram Changelog --------- .. toctree:: :maxdepth: 2 changelog python-telegram-bot-13.11/docs/source/ptb-logo-orange.ico000066400000000000000000013226261417656324400233700ustar00rootroot00000000000000 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-13.11/docs/source/ptb-logo-orange.png000077500000000000000000002234721417656324400234030ustar00rootroot00000000000000PNG  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-13.11/docs/source/telegram.animation.rst000066400000000000000000000003371417656324400241770ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/animation.py telegram.Animation ================== .. autoclass:: telegram.Animation :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.audio.rst000066400000000000000000000003171417656324400233170ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/audio.py telegram.Audio ============== .. autoclass:: telegram.Audio :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.bot.rst000066400000000000000000000003001417656324400227720ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/bot.py telegram.Bot ============ .. autoclass:: telegram.Bot :members: :show-inheritance:python-telegram-bot-13.11/docs/source/telegram.botcommand.rst000066400000000000000000000003341417656324400243400ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommand.py telegram.BotCommand =================== .. autoclass:: telegram.BotCommand :members: :show-inheritance:python-telegram-bot-13.11/docs/source/telegram.botcommandscope.rst000066400000000000000000000003601417656324400253710ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py telegram.BotCommandScope ======================== .. autoclass:: telegram.BotCommandScope :members: :show-inheritance:python-telegram-bot-13.11/docs/source/telegram.botcommandscopeallchatadministrators.rst000066400000000000000000000004571417656324400317150ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py telegram.BotCommandScopeAllChatAdministrators ============================================= .. autoclass:: telegram.BotCommandScopeAllChatAdministrators :members: :show-inheritance:python-telegram-bot-13.11/docs/source/telegram.botcommandscopeallgroupchats.rst000066400000000000000000000004311417656324400301610ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py telegram.BotCommandScopeAllGroupChats ======================================= .. autoclass:: telegram.BotCommandScopeAllGroupChats :members: :show-inheritance:python-telegram-bot-13.11/docs/source/telegram.botcommandscopeallprivatechats.rst000066400000000000000000000004351417656324400305030ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py telegram.BotCommandScopeAllPrivateChats ======================================= .. autoclass:: telegram.BotCommandScopeAllPrivateChats :members: :show-inheritance:python-telegram-bot-13.11/docs/source/telegram.botcommandscopechat.rst000066400000000000000000000003741417656324400262360ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py telegram.BotCommandScopeChat ============================ .. autoclass:: telegram.BotCommandScopeChat :members: :show-inheritance:python-telegram-bot-13.11/docs/source/telegram.botcommandscopechatadministrators.rst000066400000000000000000000004461417656324400312220ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py telegram.BotCommandScopeChatAdministrators ========================================== .. autoclass:: telegram.BotCommandScopeChatAdministrators :members: :show-inheritance:python-telegram-bot-13.11/docs/source/telegram.botcommandscopechatmember.rst000066400000000000000000000004161417656324400274230ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py telegram.BotCommandScopeChatMember ================================== .. autoclass:: telegram.BotCommandScopeChatMember :members: :show-inheritance:python-telegram-bot-13.11/docs/source/telegram.botcommandscopedefault.rst000066400000000000000000000004051417656324400267360ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py telegram.BotCommandScopeDefault =============================== .. autoclass:: telegram.BotCommandScopeDefault :members: :show-inheritance:python-telegram-bot-13.11/docs/source/telegram.callbackgame.rst000066400000000000000000000003531417656324400246040ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/games/callbackgame.py telegram.Callbackgame ===================== .. autoclass:: telegram.CallbackGame :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.callbackquery.rst000066400000000000000000000003551417656324400250420ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/callbackquery.py telegram.CallbackQuery ====================== .. autoclass:: telegram.CallbackQuery :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chat.rst000066400000000000000000000003051417656324400231320ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chat.py telegram.Chat ============= .. autoclass:: telegram.Chat :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chataction.rst000066400000000000000000000003351417656324400243330ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chataction.py telegram.ChatAction =================== .. autoclass:: telegram.ChatAction :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatinvitelink.rst000066400000000000000000000003551417656324400252340ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatinvitelink.py telegram.ChatInviteLink ======================= .. autoclass:: telegram.ChatInviteLink :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatjoinrequest.rst000066400000000000000000000003611417656324400254250ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatjoinrequest.py telegram.ChatJoinRequest ======================== .. autoclass:: telegram.ChatJoinRequest :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatlocation.rst000066400000000000000000000003451417656324400246670ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatlocation.py telegram.ChatLocation ===================== .. autoclass:: telegram.ChatLocation :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatmember.rst000066400000000000000000000003351417656324400243250ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py telegram.ChatMember =================== .. autoclass:: telegram.ChatMember :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatmemberadministrator.rst000066400000000000000000000004041417656324400271230ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py telegram.ChatMemberAdministrator ================================ .. autoclass:: telegram.ChatMemberAdministrator :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatmemberbanned.rst000066400000000000000000000003571417656324400255010ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py telegram.ChatMemberBanned ========================= .. autoclass:: telegram.ChatMemberBanned :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatmemberleft.rst000066400000000000000000000003511417656324400251760ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py telegram.ChatMemberLeft ======================= .. autoclass:: telegram.ChatMemberLeft :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatmembermember.rst000066400000000000000000000003571417656324400255210ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py telegram.ChatMemberMember ========================= .. autoclass:: telegram.ChatMemberMember :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatmemberowner.rst000066400000000000000000000003551417656324400254020ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py telegram.ChatMemberOwner ======================== .. autoclass:: telegram.ChatMemberOwner :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatmemberrestricted.rst000066400000000000000000000003731417656324400264200ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py telegram.ChatMemberRestricted ============================= .. autoclass:: telegram.ChatMemberRestricted :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatmemberupdated.rst000066400000000000000000000003711417656324400256740ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmemberupdated.py telegram.ChatMemberUpdated ========================== .. autoclass:: telegram.ChatMemberUpdated :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatpermissions.rst000066400000000000000000000003611417656324400254300ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatpermissions.py telegram.ChatPermissions ======================== .. autoclass:: telegram.ChatPermissions :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.chatphoto.rst000066400000000000000000000003371417656324400242110ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/chatphoto.py telegram.ChatPhoto ================== .. autoclass:: telegram.ChatPhoto :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.choseninlineresult.rst000066400000000000000000000003751417656324400261370ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/choseninlineresult.py telegram.ChosenInlineResult =========================== .. autoclass:: telegram.ChosenInlineResult :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.constants.rst000066400000000000000000000003501417656324400242270ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/constants.py telegram.constants Module ========================= .. automodule:: telegram.constants :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.contact.rst000066400000000000000000000003271417656324400236520ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/contact.py telegram.Contact ================ .. autoclass:: telegram.Contact :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.credentials.rst000066400000000000000000000003521417656324400245120ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/credentials.py telegram.Credentials ==================== .. autoclass:: telegram.Credentials :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.datacredentials.rst000066400000000000000000000003661417656324400253510ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/credentials.py telegram.DataCredentials ======================== .. autoclass:: telegram.DataCredentials :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.dice.rst000066400000000000000000000003041417656324400231160ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/dice.py telegram.Dice ============= .. autoclass:: telegram.Dice :members: :show-inheritance:python-telegram-bot-13.11/docs/source/telegram.document.rst000066400000000000000000000003331417656324400240320ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/document.py telegram.Document ================= .. autoclass:: telegram.Document :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.encryptedcredentials.rst000066400000000000000000000004051417656324400264270ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/credentials.py telegram.EncryptedCredentials ============================= .. autoclass:: telegram.EncryptedCredentials :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.encryptedpassportelement.rst000066400000000000000000000004361417656324400273630ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/encryptedpassportelement.py telegram.EncryptedPassportElement ================================= .. autoclass:: telegram.EncryptedPassportElement :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.error.rst000066400000000000000000000003541417656324400233500ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/error.py telegram.error module ===================== .. automodule:: telegram.error :members: :undoc-members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.basepersistence.rst000066400000000000000000000004011417656324400261660ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/basepersistence.py telegram.ext.BasePersistence ============================ .. autoclass:: telegram.ext.BasePersistence :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.callbackcontext.rst000066400000000000000000000003521417656324400261550ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/callbackcontext.py telegram.ext.CallbackContext ============================ .. autoclass:: telegram.ext.CallbackContext :members: python-telegram-bot-13.11/docs/source/telegram.ext.callbackdatacache.rst000066400000000000000000000004111417656324400263620ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/callbackdatacache.py telegram.ext.CallbackDataCache ============================== .. autoclass:: telegram.ext.CallbackDataCache :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.callbackqueryhandler.rst000066400000000000000000000004251417656324400271750ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/callbackqueryhandler.py telegram.ext.CallbackQueryHandler ================================= .. autoclass:: telegram.ext.CallbackQueryHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.chatjoinrequesthandler.rst000066400000000000000000000004351417656324400275640ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/chatjoinrequesthandler.py telegram.ext.ChatJoinRequestHandler =================================== .. autoclass:: telegram.ext.ChatJoinRequestHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.chatmemberhandler.rst000066400000000000000000000004111417656324400264550ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/chatmemberhandler.py telegram.ext.ChatMemberHandler ============================== .. autoclass:: telegram.ext.ChatMemberHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.choseninlineresulthandler.rst000066400000000000000000000004511417656324400302670ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/choseninlineresulthandler.py telegram.ext.ChosenInlineResultHandler ====================================== .. autoclass:: telegram.ext.ChosenInlineResultHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.commandhandler.rst000066400000000000000000000003751417656324400257750ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/commandhandler.py telegram.ext.CommandHandler =========================== .. autoclass:: telegram.ext.CommandHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.contexttypes.rst000066400000000000000000000003651417656324400255710ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/contexttypes.py telegram.ext.ContextTypes ========================= .. autoclass:: telegram.ext.ContextTypes :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.conversationhandler.rst000066400000000000000000000004211417656324400270610ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/conversationhandler.py telegram.ext.ConversationHandler ================================ .. autoclass:: telegram.ext.ConversationHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.defaults.rst000066400000000000000000000003451417656324400246250ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/defaults.py telegram.ext.Defaults ===================== .. autoclass:: telegram.ext.Defaults :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.delayqueue.rst000066400000000000000000000004051417656324400251560ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/messagequeue.py telegram.ext.DelayQueue ======================= .. autoclass:: telegram.ext.DelayQueue :members: :show-inheritance: :special-members: python-telegram-bot-13.11/docs/source/telegram.ext.dictpersistence.rst000066400000000000000000000004011417656324400261770ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/dictpersistence.py telegram.ext.DictPersistence ============================ .. autoclass:: telegram.ext.DictPersistence :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.dispatcher.rst000066400000000000000000000003551417656324400251450ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/dispatcher.py telegram.ext.Dispatcher ======================= .. autoclass:: telegram.ext.Dispatcher :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.dispatcherhandlerstop.rst000066400000000000000000000004161417656324400274070ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/dispatcher.py telegram.ext.DispatcherHandlerStop ================================== .. autoclass:: telegram.ext.DispatcherHandlerStop :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.extbot.rst000066400000000000000000000004171417656324400243230ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/extbot.py telegram.ext.ExtBot =================== .. autoclass:: telegram.ext.ExtBot :show-inheritance: .. autofunction:: telegram.ext.ExtBot.insert_callback_data python-telegram-bot-13.11/docs/source/telegram.ext.filters.rst000066400000000000000000000003601417656324400244630ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/filters.py telegram.ext.filters Module =========================== .. automodule:: telegram.ext.filters :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.handler.rst000066400000000000000000000003411417656324400244270ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/handler.py telegram.ext.Handler ==================== .. autoclass:: telegram.ext.Handler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.inlinequeryhandler.rst000066400000000000000000000004151417656324400267160ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/inlinequeryhandler.py telegram.ext.InlineQueryHandler =============================== .. autoclass:: telegram.ext.InlineQueryHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.invalidcallbackdata.rst000066400000000000000000000004171417656324400267530ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/callbackdatacache.py telegram.ext.InvalidCallbackData ================================ .. autoclass:: telegram.ext.InvalidCallbackData :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.job.rst000066400000000000000000000003331417656324400235650ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/jobqueue.py telegram.ext.Job ===================== .. autoclass:: telegram.ext.Job :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.jobqueue.rst000066400000000000000000000003451417656324400246350ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/jobqueue.py telegram.ext.JobQueue ===================== .. autoclass:: telegram.ext.JobQueue :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.messagehandler.rst000066400000000000000000000003751417656324400260030ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/messagehandler.py telegram.ext.MessageHandler =========================== .. autoclass:: telegram.ext.MessageHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.messagequeue.rst000066400000000000000000000004131417656324400255030ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/messagequeue.py telegram.ext.MessageQueue ========================= .. autoclass:: telegram.ext.MessageQueue :members: :show-inheritance: :special-members: python-telegram-bot-13.11/docs/source/telegram.ext.picklepersistence.rst000066400000000000000000000004111417656324400265240ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/picklepersistence.py telegram.ext.PicklePersistence ============================== .. autoclass:: telegram.ext.PicklePersistence :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.pollanswerhandler.rst000066400000000000000000000004111417656324400265340ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/pollanswerhandler.py telegram.ext.PollAnswerHandler ============================== .. autoclass:: telegram.ext.PollAnswerHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.pollhandler.rst000066400000000000000000000003611417656324400253200ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/pollhandler.py telegram.ext.PollHandler ======================== .. autoclass:: telegram.ext.PollHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.precheckoutqueryhandler.rst000066400000000000000000000004411417656324400277530ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/precheckoutqueryhandler.py telegram.ext.PreCheckoutQueryHandler ==================================== .. autoclass:: telegram.ext.PreCheckoutQueryHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.prefixhandler.rst000066400000000000000000000003731417656324400256520ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/commandhandler.py telegram.ext.PrefixHandler =========================== .. autoclass:: telegram.ext.PrefixHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.regexhandler.rst000066400000000000000000000003651417656324400254700ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/regexhandler.py telegram.ext.RegexHandler ========================= .. autoclass:: telegram.ext.RegexHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.rst000066400000000000000000000026071417656324400230220ustar00rootroot00000000000000telegram.ext package ==================== .. toctree:: telegram.ext.extbot telegram.ext.updater telegram.ext.dispatcher telegram.ext.dispatcherhandlerstop telegram.ext.callbackcontext telegram.ext.job telegram.ext.jobqueue telegram.ext.messagequeue telegram.ext.delayqueue telegram.ext.contexttypes telegram.ext.defaults Handlers -------- .. toctree:: telegram.ext.handler telegram.ext.callbackqueryhandler telegram.ext.chatjoinrequesthandler telegram.ext.chatmemberhandler telegram.ext.choseninlineresulthandler telegram.ext.commandhandler telegram.ext.conversationhandler telegram.ext.inlinequeryhandler telegram.ext.messagehandler telegram.ext.filters telegram.ext.pollanswerhandler telegram.ext.pollhandler telegram.ext.precheckoutqueryhandler telegram.ext.prefixhandler telegram.ext.regexhandler telegram.ext.shippingqueryhandler telegram.ext.stringcommandhandler telegram.ext.stringregexhandler telegram.ext.typehandler Persistence ----------- .. toctree:: telegram.ext.basepersistence telegram.ext.picklepersistence telegram.ext.dictpersistence Arbitrary Callback Data ----------------------- .. toctree:: telegram.ext.callbackdatacache telegram.ext.invalidcallbackdata utils ----- .. toctree:: telegram.ext.utils.promise telegram.ext.utils.typespython-telegram-bot-13.11/docs/source/telegram.ext.shippingqueryhandler.rst000066400000000000000000000004251417656324400272620ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/shippingqueryhandler.py telegram.ext.ShippingQueryHandler ================================= .. autoclass:: telegram.ext.ShippingQueryHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.stringcommandhandler.rst000066400000000000000000000004251417656324400272200ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/stringcommandhandler.py telegram.ext.StringCommandHandler ================================= .. autoclass:: telegram.ext.StringCommandHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.stringregexhandler.rst000066400000000000000000000004151417656324400267130ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/stringregexhandler.py telegram.ext.StringRegexHandler =============================== .. autoclass:: telegram.ext.StringRegexHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.typehandler.rst000066400000000000000000000003611417656324400253330ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/typehandler.py telegram.ext.TypeHandler ======================== .. autoclass:: telegram.ext.TypeHandler :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.updater.rst000066400000000000000000000003411417656324400244560ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/updater.py telegram.ext.Updater ==================== .. autoclass:: telegram.ext.Updater :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.utils.promise.rst000066400000000000000000000004211417656324400256260ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/utils/promise.py telegram.ext.utils.promise.Promise ================================== .. autoclass:: telegram.ext.utils.promise.Promise :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.ext.utils.types.rst000066400000000000000000000004011417656324400253120ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/utils/types.py telegram.ext.utils.types Module ================================ .. automodule:: telegram.ext.utils.types :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.file.rst000066400000000000000000000003131417656324400231310ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/file.py telegram.File ============= .. autoclass:: telegram.File :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.filecredentials.rst000066400000000000000000000003661417656324400253570ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/credentials.py telegram.FileCredentials ======================== .. autoclass:: telegram.FileCredentials :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.forcereply.rst000066400000000000000000000003351417656324400243700ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/forcereply.py telegram.ForceReply =================== .. autoclass:: telegram.ForceReply :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.game.rst000066400000000000000000000003131417656324400231230ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/games/game.py telegram.Game ============= .. autoclass:: telegram.Game :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.gamehighscore.rst000066400000000000000000000003571417656324400250270ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/games/gamehighscore.py telegram.GameHighScore ====================== .. autoclass:: telegram.GameHighScore :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.iddocumentdata.rst000066400000000000000000000003541417656324400252040ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/data.py telegram.IdDocumentData ======================= .. autoclass:: telegram.IdDocumentData :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinekeyboardbutton.rst000066400000000000000000000004141417656324400264470ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinekeyboardbutton.py telegram.InlineKeyboardButton ============================= .. autoclass:: telegram.InlineKeyboardButton :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinekeyboardmarkup.rst000066400000000000000000000004141417656324400264330ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinekeyboardmarkup.py telegram.InlineKeyboardMarkup ============================= .. autoclass:: telegram.InlineKeyboardMarkup :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequery.rst000066400000000000000000000003501417656324400245570ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequery.py telegram.InlineQuery ==================== .. autoclass:: telegram.InlineQuery :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresult.rst000066400000000000000000000004001417656324400260120ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresult.py telegram.InlineQueryResult ========================== .. autoclass:: telegram.InlineQueryResult :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultarticle.rst000066400000000000000000000004341417656324400273650ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultarticle.py telegram.InlineQueryResultArticle ================================= .. autoclass:: telegram.InlineQueryResultArticle :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultaudio.rst000066400000000000000000000004241417656324400270420ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultaudio.py telegram.InlineQueryResultAudio =============================== .. autoclass:: telegram.InlineQueryResultAudio :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultcachedaudio.rst000066400000000000000000000004541417656324400301750ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultcachedaudio.py telegram.InlineQueryResultCachedAudio ===================================== .. autoclass:: telegram.InlineQueryResultCachedAudio :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultcacheddocument.rst000066400000000000000000000004701417656324400307100ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultcacheddocument.py telegram.InlineQueryResultCachedDocument ======================================== .. autoclass:: telegram.InlineQueryResultCachedDocument :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultcachedgif.rst000066400000000000000000000004441417656324400276400ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultcachedgif.py telegram.InlineQueryResultCachedGif =================================== .. autoclass:: telegram.InlineQueryResultCachedGif :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultcachedmpeg4gif.rst000066400000000000000000000004701417656324400305740ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultcachedmpeg4gif.py telegram.InlineQueryResultCachedMpeg4Gif ======================================== .. autoclass:: telegram.InlineQueryResultCachedMpeg4Gif :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultcachedphoto.rst000066400000000000000000000004541417656324400302250ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultcachedphoto.py telegram.InlineQueryResultCachedPhoto ===================================== .. autoclass:: telegram.InlineQueryResultCachedPhoto :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultcachedsticker.rst000066400000000000000000000004641417656324400305410ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultcachedsticker.py telegram.InlineQueryResultCachedSticker ======================================= .. autoclass:: telegram.InlineQueryResultCachedSticker :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultcachedvideo.rst000066400000000000000000000004541417656324400302020ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultcachedvideo.py telegram.InlineQueryResultCachedVideo ===================================== .. autoclass:: telegram.InlineQueryResultCachedVideo :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultcachedvoice.rst000066400000000000000000000004541417656324400302010ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultcachedvoice.py telegram.InlineQueryResultCachedVoice ===================================== .. autoclass:: telegram.InlineQueryResultCachedVoice :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultcontact.rst000066400000000000000000000004341417656324400273750ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultcontact.py telegram.InlineQueryResultContact ================================= .. autoclass:: telegram.InlineQueryResultContact :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultdocument.rst000066400000000000000000000004401417656324400275550ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultdocument.py telegram.InlineQueryResultDocument ================================== .. autoclass:: telegram.InlineQueryResultDocument :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultgame.rst000066400000000000000000000004201417656324400266460ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultgame.py telegram.InlineQueryResultGame ============================== .. autoclass:: telegram.InlineQueryResultGame :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultgif.rst000066400000000000000000000004141417656324400265050ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultgif.py telegram.InlineQueryResultGif ============================= .. autoclass:: telegram.InlineQueryResultGif :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultlocation.rst000066400000000000000000000004401417656324400275470ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultlocation.py telegram.InlineQueryResultLocation ================================== .. autoclass:: telegram.InlineQueryResultLocation :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultmpeg4gif.rst000066400000000000000000000004401417656324400274410ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultmpeg4gif.py telegram.InlineQueryResultMpeg4Gif ================================== .. autoclass:: telegram.InlineQueryResultMpeg4Gif :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultphoto.rst000066400000000000000000000004241417656324400270720ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultphoto.py telegram.InlineQueryResultPhoto =============================== .. autoclass:: telegram.InlineQueryResultPhoto :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultvenue.rst000066400000000000000000000004241417656324400270630ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultvenue.py telegram.InlineQueryResultVenue =============================== .. autoclass:: telegram.InlineQueryResultVenue :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultvideo.rst000066400000000000000000000004241417656324400270470ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultvideo.py telegram.InlineQueryResultVideo =============================== .. autoclass:: telegram.InlineQueryResultVideo :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inlinequeryresultvoice.rst000066400000000000000000000004241417656324400270460ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inlinequeryresultvoice.py telegram.InlineQueryResultVoice =============================== .. autoclass:: telegram.InlineQueryResultVoice :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputcontactmessagecontent.rst000066400000000000000000000004441417656324400276720ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inputcontactmessagecontent.py telegram.InputContactMessageContent =================================== .. autoclass:: telegram.InputContactMessageContent :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputfile.rst000066400000000000000000000003371417656324400242170ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/inputfile.py telegram.InputFile ================== .. autoclass:: telegram.InputFile :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputinvoicemessagecontent.rst000066400000000000000000000004441417656324400276730ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inputinvoicemessagecontent.py telegram.InputInvoiceMessageContent =================================== .. autoclass:: telegram.InputInvoiceMessageContent :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputlocationmessagecontent.rst000066400000000000000000000004501417656324400300440ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inputlocationmessagecontent.py telegram.InputLocationMessageContent ==================================== .. autoclass:: telegram.InputLocationMessageContent :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputmedia.rst000066400000000000000000000003431417656324400243540ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/inputmedia.py telegram.InputMedia =================== .. autoclass:: telegram.InputMedia :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputmediaanimation.rst000066400000000000000000000003761417656324400262620ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/inputmedia.py telegram.InputMediaAnimation ============================ .. autoclass:: telegram.InputMediaAnimation :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputmediaaudio.rst000066400000000000000000000003621417656324400253770ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/inputmedia.py telegram.InputMediaAudio ======================== .. autoclass:: telegram.InputMediaAudio :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputmediadocument.rst000066400000000000000000000003731417656324400261160ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/inputmedia.py telegram.InputMediaDocument =========================== .. autoclass:: telegram.InputMediaDocument :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputmediaphoto.rst000066400000000000000000000003621417656324400254270ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/inputmedia.py telegram.InputMediaPhoto ======================== .. autoclass:: telegram.InputMediaPhoto :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputmediavideo.rst000066400000000000000000000003621417656324400254040ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/inputmedia.py telegram.InputMediaVideo ======================== .. autoclass:: telegram.InputMediaVideo :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputmessagecontent.rst000066400000000000000000000004101417656324400263070ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inputmessagecontent.py telegram.InputMessageContent ============================ .. autoclass:: telegram.InputMessageContent :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputtextmessagecontent.rst000066400000000000000000000004301417656324400272160ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inputtextmessagecontent.py telegram.InputTextMessageContent ================================ .. autoclass:: telegram.InputTextMessageContent :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.inputvenuemessagecontent.rst000066400000000000000000000004341417656324400273600ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/inline/inputvenuemessagecontent.py telegram.InputVenueMessageContent ================================= .. autoclass:: telegram.InputVenueMessageContent :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.invoice.rst000066400000000000000000000003311417656324400236460ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/payment/invoice.py telegram.Invoice ================ .. autoclass:: telegram.Invoice :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.keyboardbutton.rst000066400000000000000000000003551417656324400252540ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/keyboardbutton.py telegram.KeyboardButton ======================= .. autoclass:: telegram.KeyboardButton :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.keyboardbuttonpolltype.rst000066400000000000000000000004151417656324400270420ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/keyboardbuttonpolltype.py telegram.KeyboardButtonPollType =============================== .. autoclass:: telegram.KeyboardButtonPollType :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.labeledprice.rst000066400000000000000000000003551417656324400246330ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/payment/labeledprice.py telegram.LabeledPrice ===================== .. autoclass:: telegram.LabeledPrice :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.location.rst000066400000000000000000000003331417656324400240240ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/location.py telegram.Location ================= .. autoclass:: telegram.Location :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.loginurl.rst000066400000000000000000000003251417656324400240500ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/loginurl.py telegram.LoginUrl ================= .. autoclass:: telegram.LoginUrl :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.maskposition.rst000066400000000000000000000003461417656324400247400ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/sticker.py telegram.MaskPosition ===================== .. autoclass:: telegram.MaskPosition :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.message.rst000066400000000000000000000003211417656324400236350ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/message.py telegram.Message ================ .. autoclass:: telegram.Message :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.messageautodeletetimerchanged.rst000066400000000000000000000004511417656324400302700ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/messageautodeletetimerchanged.py telegram.MessageAutoDeleteTimerChanged ====================================== .. autoclass:: telegram.MessageAutoDeleteTimerChanged :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.messageentity.rst000066400000000000000000000003511417656324400250750ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/messageentity.py telegram.MessageEntity ====================== .. autoclass:: telegram.MessageEntity :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.messageid.rst000066400000000000000000000003311417656324400241530ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/messageid.py telegram.MessageId ================== .. autoclass:: telegram.MessageId :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.orderinfo.rst000066400000000000000000000003411417656324400242020ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/payment/orderinfo.py telegram.OrderInfo ================== .. autoclass:: telegram.OrderInfo :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.parsemode.rst000066400000000000000000000003311417656324400241710ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/parsemode.py telegram.ParseMode ================== .. autoclass:: telegram.ParseMode :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportdata.rst000066400000000000000000000003561417656324400247260ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportdata.py telegram.PassportData ===================== .. autoclass:: telegram.PassportData :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportelementerror.rst000066400000000000000000000004171417656324400265160ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportelementerrors.py telegram.PassportElementError ============================= .. autoclass:: telegram.PassportElementError :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportelementerrordatafield.rst000066400000000000000000000004521417656324400303530ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportelementerrors.py telegram.PassportElementErrorDataField ====================================== .. autoclass:: telegram.PassportElementErrorDataField :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportelementerrorfile.rst000066400000000000000000000004331417656324400273540ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportelementerrors.py telegram.PassportElementErrorFile ================================= .. autoclass:: telegram.PassportElementErrorFile :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportelementerrorfiles.rst000066400000000000000000000004361417656324400275420ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportelementerrors.py telegram.PassportElementErrorFiles ================================== .. autoclass:: telegram.PassportElementErrorFiles :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportelementerrorfrontside.rst000066400000000000000000000004521417656324400304330ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportelementerrors.py telegram.PassportElementErrorFrontSide ====================================== .. autoclass:: telegram.PassportElementErrorFrontSide :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportelementerrorreverseside.rst000066400000000000000000000004601417656324400307550ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportelementerrors.py telegram.PassportElementErrorReverseSide ======================================== .. autoclass:: telegram.PassportElementErrorReverseSide :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportelementerrorselfie.rst000066400000000000000000000004461417656324400277100ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportelementerrors.py telegram.PassportElementErrorSelfie ======================================== .. autoclass:: telegram.PassportElementErrorSelfie :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportelementerrortranslationfile.rst000066400000000000000000000004741417656324400316400ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportelementerrors.py telegram.PassportElementErrorTranslationFile ============================================ .. autoclass:: telegram.PassportElementErrorTranslationFile :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportelementerrortranslationfiles.rst000066400000000000000000000004771417656324400320260ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportelementerrors.py telegram.PassportElementErrorTranslationFiles ============================================= .. autoclass:: telegram.PassportElementErrorTranslationFiles :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportelementerrorunspecified.rst000066400000000000000000000004601417656324400307330ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportelementerrors.py telegram.PassportElementErrorUnspecified ======================================== .. autoclass:: telegram.PassportElementErrorUnspecified :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.passportfile.rst000066400000000000000000000003561417656324400247340ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/passportfile.py telegram.PassportFile ===================== .. autoclass:: telegram.PassportFile :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.personaldetails.rst000066400000000000000000000003571417656324400254130ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/data.py telegram.PersonalDetails ======================== .. autoclass:: telegram.PersonalDetails :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.photosize.rst000066400000000000000000000003371417656324400242440ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/photosize.py telegram.PhotoSize ================== .. autoclass:: telegram.PhotoSize :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.poll.rst000066400000000000000000000003051417656324400231610ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/poll.py telegram.Poll ============= .. autoclass:: telegram.Poll :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.pollanswer.rst000066400000000000000000000003271417656324400244050ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/poll.py telegram.PollAnswer =================== .. autoclass:: telegram.PollAnswer :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.polloption.rst000066400000000000000000000003271417656324400244160ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/poll.py telegram.PollOption =================== .. autoclass:: telegram.PollOption :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.precheckoutquery.rst000066400000000000000000000003751417656324400256240ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/payment/precheckoutquery.py telegram.PreCheckoutQuery ========================= .. autoclass:: telegram.PreCheckoutQuery :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.proximityalerttriggered.rst000066400000000000000000000004211417656324400272030ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/proximityalerttriggered.py telegram.ProximityAlertTriggered ================================ .. autoclass:: telegram.ProximityAlertTriggered :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.replykeyboardmarkup.rst000066400000000000000000000004011417656324400263040ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/replykeyboardmarkup.py telegram.ReplyKeyboardMarkup ============================ .. autoclass:: telegram.ReplyKeyboardMarkup :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.replykeyboardremove.rst000066400000000000000000000004011417656324400263020ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/replykeyboardremove.py telegram.ReplyKeyboardRemove ============================ .. autoclass:: telegram.ReplyKeyboardRemove :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.replymarkup.rst000066400000000000000000000003411417656324400245660ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/replymarkup.py telegram.ReplyMarkup ==================== .. autoclass:: telegram.ReplyMarkup :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.residentialaddress.rst000066400000000000000000000003701417656324400260660ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/data.py telegram.ResidentialAddress =========================== .. autoclass:: telegram.ResidentialAddress :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.rst000066400000000000000000000110401417656324400222120ustar00rootroot00000000000000telegram package ================ .. toctree:: telegram.animation telegram.audio telegram.bot telegram.botcommand telegram.botcommandscope telegram.botcommandscopedefault telegram.botcommandscopeallprivatechats telegram.botcommandscopeallgroupchats telegram.botcommandscopeallchatadministrators telegram.botcommandscopechat telegram.botcommandscopechatadministrators telegram.botcommandscopechatmember telegram.callbackquery telegram.chat telegram.chataction telegram.chatinvitelink telegram.chatjoinrequest telegram.chatlocation telegram.chatmember telegram.chatmemberowner telegram.chatmemberadministrator telegram.chatmembermember telegram.chatmemberrestricted telegram.chatmemberleft telegram.chatmemberbanned telegram.chatmemberupdated telegram.chatpermissions telegram.chatphoto telegram.constants telegram.contact telegram.dice telegram.document telegram.error telegram.file telegram.forcereply telegram.inlinekeyboardbutton telegram.inlinekeyboardmarkup telegram.inputfile telegram.inputmedia telegram.inputmediaanimation telegram.inputmediaaudio telegram.inputmediadocument telegram.inputmediaphoto telegram.inputmediavideo telegram.keyboardbutton telegram.keyboardbuttonpolltype telegram.location telegram.loginurl telegram.message telegram.messageautodeletetimerchanged telegram.messageid telegram.messageentity telegram.parsemode telegram.photosize telegram.poll telegram.pollanswer telegram.polloption telegram.proximityalerttriggered telegram.replykeyboardremove telegram.replykeyboardmarkup telegram.replymarkup telegram.telegramobject telegram.update telegram.user telegram.userprofilephotos telegram.venue telegram.video telegram.videonote telegram.voice telegram.voicechatstarted telegram.voicechatended telegram.voicechatscheduled telegram.voicechatparticipantsinvited telegram.webhookinfo Stickers -------- .. toctree:: telegram.sticker telegram.stickerset telegram.maskposition Inline Mode ----------- .. toctree:: telegram.inlinequery telegram.inlinequeryresult telegram.inlinequeryresultarticle telegram.inlinequeryresultaudio telegram.inlinequeryresultcachedaudio telegram.inlinequeryresultcacheddocument telegram.inlinequeryresultcachedgif telegram.inlinequeryresultcachedmpeg4gif telegram.inlinequeryresultcachedphoto telegram.inlinequeryresultcachedsticker telegram.inlinequeryresultcachedvideo telegram.inlinequeryresultcachedvoice telegram.inlinequeryresultcontact telegram.inlinequeryresultdocument telegram.inlinequeryresultgame telegram.inlinequeryresultgif telegram.inlinequeryresultlocation telegram.inlinequeryresultmpeg4gif telegram.inlinequeryresultphoto telegram.inlinequeryresultvenue telegram.inlinequeryresultvideo telegram.inlinequeryresultvoice telegram.inputmessagecontent telegram.inputtextmessagecontent telegram.inputlocationmessagecontent telegram.inputvenuemessagecontent telegram.inputcontactmessagecontent telegram.inputinvoicemessagecontent telegram.choseninlineresult Payments -------- .. toctree:: telegram.labeledprice telegram.invoice telegram.shippingaddress telegram.orderinfo telegram.shippingoption telegram.successfulpayment telegram.shippingquery telegram.precheckoutquery Games ----- .. toctree:: telegram.game telegram.callbackgame telegram.gamehighscore Passport -------- .. toctree:: telegram.passportelementerror telegram.passportelementerrorfile telegram.passportelementerrorfiles telegram.passportelementerrorreverseside telegram.passportelementerrorfrontside telegram.passportelementerrordatafield telegram.passportelementerrorselfie telegram.passportelementerrortranslationfile telegram.passportelementerrortranslationfiles telegram.passportelementerrorunspecified telegram.credentials telegram.datacredentials telegram.securedata telegram.securevalue telegram.filecredentials telegram.iddocumentdata telegram.personaldetails telegram.residentialaddress telegram.passportdata telegram.passportfile telegram.encryptedpassportelement telegram.encryptedcredentials utils ----- .. toctree:: telegram.utils.helpers telegram.utils.promise telegram.utils.request telegram.utils.types python-telegram-bot-13.11/docs/source/telegram.securedata.rst000066400000000000000000000003471417656324400243410ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/credentials.py telegram.SecureData =================== .. autoclass:: telegram.SecureData :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.securevalue.rst000066400000000000000000000003521417656324400245400ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/passport/credentials.py telegram.SecureValue ==================== .. autoclass:: telegram.SecureValue :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.shippingaddress.rst000066400000000000000000000003711417656324400254050ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/payment/shippingaddress.py telegram.ShippingAddress ======================== .. autoclass:: telegram.ShippingAddress :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.shippingoption.rst000066400000000000000000000003651417656324400252730ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/payment/shippingoption.py telegram.ShippingOption ======================= .. autoclass:: telegram.ShippingOption :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.shippingquery.rst000066400000000000000000000003611417656324400251240ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/payment/shippingquery.py telegram.ShippingQuery ====================== .. autoclass:: telegram.ShippingQuery :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.sticker.rst000066400000000000000000000003271417656324400236630ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/sticker.py telegram.Sticker ================ .. autoclass:: telegram.Sticker :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.stickerset.rst000066400000000000000000000003401417656324400243720ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/sticker.py telegram.StickerSet =================== .. autoclass:: telegram.StickerSet :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.successfulpayment.rst000066400000000000000000000004011417656324400257650ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/payment/successfulpayment.py telegram.SuccessfulPayment ========================== .. autoclass:: telegram.SuccessfulPayment :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.telegramobject.rst000066400000000000000000000001751417656324400252070ustar00rootroot00000000000000telegram.TelegramObject ======================= .. autoclass:: telegram.TelegramObject :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.update.rst000066400000000000000000000003151417656324400234760ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/update.py telegram.Update =============== .. autoclass:: telegram.Update :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.user.rst000066400000000000000000000003051417656324400231710ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/user.py telegram.User ============= .. autoclass:: telegram.User :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.userprofilephotos.rst000066400000000000000000000003711417656324400260120ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/userprofilephotos.py telegram.UserProfilePhotos ========================== .. autoclass:: telegram.UserProfilePhotos :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.utils.helpers.rst000066400000000000000000000003701417656324400250160ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/utils/helpers.py telegram.utils.helpers Module ============================= .. automodule:: telegram.utils.helpers :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.utils.promise.rst000066400000000000000000000004021417656324400250260ustar00rootroot00000000000000telegram.utils.promise.Promise ============================== .. py:class:: telegram.utils.promise.Promise Shortcut for :class:`telegram.ext.utils.promise.Promise`. .. deprecated:: 13.2 Use :class:`telegram.ext.utils.promise.Promise` instead. python-telegram-bot-13.11/docs/source/telegram.utils.request.rst000066400000000000000000000004011417656324400250370ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/utils/request.py telegram.utils.request.Request ============================== .. autoclass:: telegram.utils.request.Request :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.utils.types.rst000066400000000000000000000003601417656324400245170ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/utils/types.py telegram.utils.types Module =========================== .. automodule:: telegram.utils.types :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.venue.rst000066400000000000000000000003171417656324400233400ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/venue.py telegram.Venue ============== .. autoclass:: telegram.Venue :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.video.rst000066400000000000000000000003171417656324400233240ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/video.py telegram.Video ============== .. autoclass:: telegram.Video :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.videonote.rst000066400000000000000000000003371417656324400242140ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/videonote.py telegram.VideoNote ================== .. autoclass:: telegram.VideoNote :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.voice.rst000066400000000000000000000003171417656324400233230ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/files/voice.py telegram.Voice ============== .. autoclass:: telegram.Voice :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.voicechatended.rst000066400000000000000000000003511417656324400251610ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/voicechat.py telegram.VoiceChatEnded ======================= .. autoclass:: telegram.VoiceChatEnded :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.voicechatparticipantsinvited.rst000066400000000000000000000004231417656324400301660ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/voicechat.py telegram.VoiceChatParticipantsInvited ===================================== .. autoclass:: telegram.VoiceChatParticipantsInvited :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.voicechatscheduled.rst000066400000000000000000000003651417656324400260470ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/voicechat.py telegram.VoiceChatScheduled =========================== .. autoclass:: telegram.VoiceChatScheduled :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.voicechatstarted.rst000066400000000000000000000003571417656324400255560ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/voicechat.py telegram.VoiceChatStarted ========================= .. autoclass:: telegram.VoiceChatStarted :members: :show-inheritance: python-telegram-bot-13.11/docs/source/telegram.webhookinfo.rst000066400000000000000000000003411417656324400245250ustar00rootroot00000000000000:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/webhookinfo.py telegram.WebhookInfo ==================== .. autoclass:: telegram.WebhookInfo :members: :show-inheritance: python-telegram-bot-13.11/examples/000077500000000000000000000000001417656324400172525ustar00rootroot00000000000000python-telegram-bot-13.11/examples/LICENSE.txt000066400000000000000000000146331417656324400211040ustar00rootroot00000000000000CC0 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-13.11/examples/README.md000066400000000000000000000153111417656324400205320ustar00rootroot00000000000000# Examples In this folder are 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 [`rawapibot.py`](#pure-api) example, they all use the high-level framework this library provides with the [`telegram.ext`](https://python-telegram-bot.readthedocs.io/en/latest/telegram.ext.html) submodule. 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. 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. ### [`echobot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py) 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. ### [`timerbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/timerbot.py) This bot uses the [`JobQueue`](https://python-telegram-bot.readthedocs.io/en/latest/telegram.ext.jobqueue.html) 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](https://github.com/python-telegram-bot/python-telegram-bot/wiki/Extensions-%E2%80%93-JobQueue). ### [`conversationbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/conversationbot.py) A common task for a bot is to ask information from the user. In v5.0 of this library, we introduced the [`ConversationHandler`](https://python-telegram-bot.readthedocs.io/en/latest/telegram.ext.conversationhandler.html) 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 [state diagram](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/conversationbot.png). ### [`conversationbot2.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/conversationbot2.py) A more complex example of a bot that uses the `ConversationHandler`. It is also more confusing. Good thing there is a [fancy state diagram](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/conversationbot2.png) for this one, too! ### [`nestedconversationbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/nestedconversationbot.py) 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 [fancy state diagram](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/nestedconversationbot.png) for this example, too! ### [`persistentconversationbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/persistentconversationbot.py) A basic example of a bot store conversation state and user_data over multiple restarts. ### [`inlinekeyboard.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/inlinekeyboard.py) This example sheds some light on inline keyboards, callback queries and message editing. A wikipedia site explaining this examples lives at https://git.io/JOmFw. ### [`inlinekeyboard2.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/inlinekeyboard2.py) 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. ### [`deeplinking.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/deeplinking.py) A basic example on how to use deeplinking with inline keyboards. ### [`inlinebot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/inlinebot.py) A basic example of an [inline bot](https://core.telegram.org/bots/inline). Don't forget to enable inline mode with [@BotFather](https://telegram.me/BotFather). ### [`pollbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/pollbot.py) This example sheds some light on polls, poll answers and the corresponding handlers. ### [`passportbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/passportbot.py) A basic example of a bot that can accept passports. Use in combination with [`passportbot.html`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/passportbot.html). Don't forget to enable and configure payments with [@BotFather](https://telegram.me/BotFather). Check out this [guide](https://git.io/fAvYd) on Telegram passports in PTB. ### [`paymentbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/paymentbot.py) A basic example of a bot that can accept payments. Don't forget to enable and configure payments with [@BotFather](https://telegram.me/BotFather). ### [`errorhandlerbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/errorhandlerbot.py) A basic example on how to set up a custom error handler. ### [`chatmemberbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/chatmemberbot.py) A basic example on how `(my_)chat_member` updates can be used. ### [`contexttypesbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/contexttypesbot.py) This example showcases how `telegram.ext.ContextTypes` can be used to customize the `context` argument of handler and job callbacks. ### [`arbitrarycallbackdatabot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/arbitrarycallbackdatabot.py) This example showcases how PTBs "arbitrary callback data" feature can be used. ## Pure API The [`rawapibot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/rawapibot.py) example uses only the pure, "bare-metal" API wrapper. python-telegram-bot-13.11/examples/arbitrarycallbackdatabot.py000066400000000000000000000077111417656324400246450ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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://git.io/JGBDI """ import logging from typing import List, Tuple, cast from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ( Updater, CommandHandler, CallbackQueryHandler, CallbackContext, InvalidCallbackData, PicklePersistence, ) logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) def start(update: Update, context: CallbackContext) -> None: """Sends a message with 5 inline buttons attached.""" number_list: List[int] = [] update.message.reply_text('Please choose:', reply_markup=build_keyboard(number_list)) def help_command(update: Update, context: CallbackContext) -> None: """Displays info on how to use the bot.""" 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. " ) def clear(update: Update, context: CallbackContext) -> None: """Clears the callback data cache""" context.bot.callback_data_cache.clear_callback_data() # type: ignore[attr-defined] context.bot.callback_data_cache.clear_callback_queries() # type: ignore[attr-defined] 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)] ) def list_button(update: Update, context: CallbackContext) -> None: """Parses the CallbackQuery and updates the message text.""" query = update.callback_query 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) 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) def handle_invalid_button(update: Update, context: CallbackContext) -> None: """Informs the user that the button is no longer available.""" update.callback_query.answer() 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( filename='arbitrarycallbackdatabot.pickle', store_callback_data=True ) # Create the Updater and pass it your bot's token. updater = Updater("TOKEN", persistence=persistence, arbitrary_callback_data=True) updater.dispatcher.add_handler(CommandHandler('start', start)) updater.dispatcher.add_handler(CommandHandler('help', help_command)) updater.dispatcher.add_handler(CommandHandler('clear', clear)) updater.dispatcher.add_handler( CallbackQueryHandler(handle_invalid_button, pattern=InvalidCallbackData) ) updater.dispatcher.add_handler(CallbackQueryHandler(list_button)) # Start the Bot updater.start_polling() # Run the bot until the user presses Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/chatmemberbot.py000066400000000000000000000137321417656324400224460ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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 Tuple, Optional from telegram import Update, Chat, ChatMember, ParseMode, ChatMemberUpdated from telegram.ext import ( Updater, CommandHandler, CallbackContext, ChatMemberHandler, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) 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.CREATOR, ChatMember.ADMINISTRATOR, ] or (old_status == ChatMember.RESTRICTED and old_is_member is True) ) is_member = ( new_status in [ ChatMember.MEMBER, ChatMember.CREATOR, ChatMember.ADMINISTRATOR, ] or (new_status == ChatMember.RESTRICTED and new_is_member is True) ) return was_member, is_member def track_chats(update: Update, context: CallbackContext) -> 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: logger.info("%s started 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) else: if 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) def show_chats(update: Update, context: CallbackContext) -> 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}." ) update.effective_message.reply_text(text) def greet_chat_members(update: Update, context: CallbackContext) -> 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: 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: update.effective_chat.send_message( f"{member_name} is no longer with us. Thanks a lot, {cause_name} ...", parse_mode=ParseMode.HTML, ) def main() -> None: """Start the bot.""" # Create the Updater and pass it your bot's token. updater = Updater("TOKEN") # Get the dispatcher to register handlers dispatcher = updater.dispatcher # Keep track of which chats the bot is in dispatcher.add_handler(ChatMemberHandler(track_chats, ChatMemberHandler.MY_CHAT_MEMBER)) dispatcher.add_handler(CommandHandler("show_chats", show_chats)) # Handle members joining/leaving chats. dispatcher.add_handler(ChatMemberHandler(greet_chat_members, ChatMemberHandler.CHAT_MEMBER)) # Start the Bot # We pass 'allowed_updates' handle *all* updates including `chat_member` updates # To reset this, simply pass `allowed_updates=[]` updater.start_polling(allowed_updates=Update.ALL_TYPES) # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == "__main__": main() python-telegram-bot-13.11/examples/contexttypesbot.py000066400000000000000000000103421417656324400231020ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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. """ from collections import defaultdict from typing import DefaultDict, Optional, Set from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ParseMode from telegram.ext import ( Updater, CommandHandler, CallbackContext, ContextTypes, CallbackQueryHandler, TypeHandler, Dispatcher, ) 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 [dict, ChatData, dict] is for type checkers like mypy class CustomContext(CallbackContext[dict, ChatData, dict]): """Custom class for context.""" def __init__(self, dispatcher: Dispatcher): super().__init__(dispatcher=dispatcher) 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 obejct.') self.chat_data.clicks_per_message[self._message_id] = value @classmethod def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CustomContext': """Override from_update to set _message_id.""" # Make sure to call super() context = super().from_update(update, dispatcher) if context.chat_data and isinstance(update, Update) and update.effective_message: context._message_id = update.effective_message.message_id # pylint: disable=W0212 # Remember to return the object return context def start(update: Update, context: CustomContext) -> None: """Display a message with a button.""" update.message.reply_html( 'This button was clicked 0 times.', reply_markup=InlineKeyboardMarkup.from_button( InlineKeyboardButton(text='Click me!', callback_data='button') ), ) def count_click(update: Update, context: CustomContext) -> None: """Update the click count for the message.""" context.message_clicks += 1 update.callback_query.answer() 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, ) def print_users(update: Update, context: CustomContext) -> None: """Show which users have been using this bot.""" update.message.reply_text( 'The following user IDs have used this bot: ' f'{", ".join(map(str, context.bot_user_ids))}' ) 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) updater = Updater("TOKEN", context_types=context_types) dispatcher = updater.dispatcher # run track_users in its own group to not interfere with the user handlers dispatcher.add_handler(TypeHandler(Update, track_users), group=-1) dispatcher.add_handler(CommandHandler("start", start)) dispatcher.add_handler(CallbackQueryHandler(count_click)) dispatcher.add_handler(CommandHandler("print_users", print_users)) updater.start_polling() updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/conversationbot.png000066400000000000000000002445541417656324400232150ustar00rootroot00000000000000PNG  IHDRePMsBIT|d pHYsgRtEXtSoftwarewww.inkscape.org< IDATxw|g8"$AB{GRF'龵J5RZw*5kRJ_hbTPԺm$?"$d9 \{}?ԭ5 ))))Aqqq VddJ(%Ke䈍 ,m֭֭)J*^z*S<==oԩS. ã֭[zWrJ(999]?F!`J ۷oEڿ ִi:ci- oFB 777={V2aF S&SRRe˖=, Zrr6lؐYakk 6X*#C[n$ C1"##-P E3c)S<\y( .+`0~5hUXA ޽{dӂ<kիWUBdZ=SڵkWgf+QNIg&A  иqd0dcsֶrrrO?ZjQCGȑ#fy{{KYg0L34i={駟J9yxiڵ_t9rD5jPvԣGl%dAi۷oW-zjKk ( (App/^l2pǞ={ԠA >ڥoΞ=#FX eڵ[ڵkgR|35e<^¯[nZv-a"8rxTq]tU]tO?N:Y,.KuJU(={jÆ . "oyxypA|0Ty=<yAC7OW2%<giO}&aC!<Hy A08!Py ü޽{%`qv.@.Xb2:]]9E\R+Vٺ/99Yqqqj߾֬Ynݺ= {y@>p;^j5jU3VhT]-T]-z<<=ٺS55+f\n݊gy@>ۥ Rr= s/cfRJfogw-oo@~yEߊ)K塞zZO1#S*",BC'cQ7?t5'=⥊;nER|lmZ ]|ڭ[ԵK⥊vڪdt5\|MGUTxnD)!>AΎ^4#`ЮC$݌iVOy A`EnkEP]\O~Q5_A_.Rxh[2s`Y]҈F{fa4!fm}[;oU_S*?t j=H3GϘ;éf8A`E˦.STxN*'kIIo#&JNJ*[l¯k#OgjVzt[vm%xrpr߻g?ӏ~3/?u*&'%KZKHJ=YvGǦ?_ϼ݆TR`kk3.:w mekg+I{z+L+YLǙexx|}ge$)1IW.\`0o!u|LljڷOXOU]ڶn^ownЬY2:-,uy@xy=RWzuEE`T銥5l0yXa- 7F֭[ ϥ1OҮvi;e`EUNeUb6fڕTq98g}m雧kӴqFmydAGoEk7K7gO-9$Gm꫹;2> 4~iO}:,AҠM y@Y:en\<s;F^jZ RGjPx G<`kgaSY P( ( ( ( ( (] I҄7'r5ȍ1w Ըqc >\gΜv) *q."$jĉ. Sy@@y@@y@@y@@v.]ҢE]#!!!:wM&oookP ؝;wN#Fvx ݸqC7ohv)]/.-ZX<&oܨwRիVZE \]Y $IC_zI/Vݵzj9::Z2.`5v՗Chƍzgg-7{ޜ0AΞv)_y0,IjꫲֽNڸq|}}uQ@'Aw+Wŋu짟T\9kƸqZGVmӠ.=ef1:w7OUАIU:blmvo.Jٲشi8[/ժS_y.@iHJJJm]U_~gw_NYyY-ٸQ};u2]v^$I'zԠ=s|*`Q.{{IFܝ`0ٳ,)u6ބyd0$I,߮Oy._&I2(umVK ,1#/m]6m4+o޺umj$IQQ طOm4H_JJ6ء˫b9+ :vLQQ TBbzyQڪ\lڰc]pqqQվqce’t pA^U=ݸ<ΩXѢڲe{ގ[S?k˖ Q`.y/W>=Zv}}F K ^Q'Ο׸9sԫm[}>wծZiiF#Gs0Mx]l.F 稞Fӵ 4H4kw͟|RM3q}]T)Y'N!U+ ~ z7_.b|~۵+þ3fhߢERge(23ҖrrҨօ~ 9 &{+ (HԲ^ד$Iin(r6̚Kk̙:~YOww^BM1vj۰89?lxgm o˿~;Z}X=՚U-?^Wꅏ>; cy:#oEY[LIҙ;JdNgywf998mSwVڵաISvvV5A%$&hI*'I^Ӏ1ctתX׳?T:wVбcu0]jҥ}[%SNf3$WOb:>6M{Q5sHAޟ ={-X^+=a,urp`￯/,ЧwiwvѲ^=y9&Rg:tH k2cIiA[f|J^CC3x{>u:IsOKsKF|m,<]pa$)uIŋlfIR6m4g }k ԹsOgYrwOpեE M?_Ouj{{詾}WN^zUzu}|\USm4Pq77|Y<>T/uh7GA^ըvm+Z4ݵJeʨ~)YQQROW$^z8EݾmZUqq4ZU7Cɋiϑ#d.Vt'zyxA͚^Y …Zoa%ȣHըPA-5sqvVS__=Q$ɣH5][*U$,V,ur۪_]T[UBbJ/:t=_aǎ)͚5Ӫ/T6m]#髯3 ټ9@SgOUYSׯ`۷EZzz<֟!r ς^{;&F?\4W/kGD< wPOO?wN##ϫaZ.A8;k}^] Q͊է];l<XA8ѿFoRb<D/٩OXGJ͛:).!AOT. P}GA*뢲+6>^1$G998y zN11$bD״.0P)))9rhȩE\\,1Lxyxh]'^TaʕjS٦Ms%IRʕ@V͝;W.?S/ڵkgrEOHХW+O+Z<ǣH3Fty7(998 99wprEKP͛$&!1Qg.]xz>xT.]"..*.G1ӾI HpwW 77d~`q:y ;;OL_Ø8+Z4K#xwsgF|ׯסCmRI?z))9Y`4*/ytnEG}T_K洌)YRt%,t옚 vܼ-wWWkcbƸqZye0ԳM=ZZN|%UCII6MnUΜΥ66T͚;zjU'ք yYBfF͘MmN {¿3G7dժZ6aB@O>=db׹sk:QoMk\U}%V~ =0x饗1IKU||/ժU\y^ŊiĀJLJү;vS4…?gծ;t{" ~]er\?~\cbTLSH'$w۶ش\u="Blrsuѣ?fVԾQ# *h՗_ws7BHg^|t5 K|ji+VA:|<1Ckݺٳf=;:jiWdgk#FuEM]LƎUZT\r-$ >YԿi|{miZT*]+uԱIkGБ3gtuQPKy i[oIB_Sn*`֬ C$K;p^hPϞ7][v5}~GA0w)WVdxI+;i8^UV+l;PM|)*sȣHIRǦMոM[4$9}ZW4_?}9d[QJJV~|ToĤ$g,rvt$9;:jZq^fm%Jq㯿<][NnCO<ӊ/vx9ScgRDxZ)KofaʗWbR͙[,>`>\ Q^T綾t%M>\͞xj̠Az[7_w$ f0[qϝӉUbE8~\wn݊3_[FVm٢?Te͙I Ҿ\@]o둑zgL!$SP%٩Y3ջV,av.]$V~tܚW7nN>mr<찋iǁ7{&̝Z*ƍ5ݳ']ND*19:8]ԿKpzk zUTejiϹ˗%af]]?WW_/vկ;vhɦMZieݺhgNphhYD 9Y~RŋkY ¼y IDAT; k֦SwϏy*ς<7WW]Hk㯿rŋUp\_R%]B#m֛YS_:"%I/ua tQ˺u60P[[{W^1kgk5_}{}Ún.\h54 1rae5%/Z)5zukM:T̛CK~_,VLttis»D I'n6?j׋/j?R2?gNKm[?>{Mwiԩo,1+Mhxbr^Z2[ h뫍SUiwwN ~ݚeWF$I 7l0kO ?dǮS<}ti>]%Rǎ9~O2[J :8:vY9sLc}4u9bֿji$7јAKƏW߅ ;կQCǏϼ&;,=Νec0hԩ5cV[2!9aYziS.՜6kssuՄwU.]k ~]?Yu$/4iRI5`]yEZr;W7n %6CCXRC˗C2Ӳ^=߰?]VpwW?'t:8XeTF  LY>qbJRNbǎ:p޸..V Zկs+4<\ar-THʔGNC}з""uJ+*e˦NF﯃\'2 $W|b~0XCS47WtqQHsZ5Ki]mmlTF1 H I3`v2mV+UJJo*`}yE Y @jخ+1Ө}#< "WrA䱫1 r+&>2:d GElt$ieuѯٺץN: *ƍrf<@@^ P,r},]{ᢅ{kekp :(Ҟ*Qu9/C'Ο$ 4)GDžkܸqy, @STqU sj԰]CIR]~4f\<Fn !!zjUy-*ÝY\|Jm:vw}dr Ց3gT'G :u<<<2 Zhuz= kco_U[-] !OUtnER`USY KҢ$iEz5ԾQ#+W5񉉊}[a.yZ5=Y-Ye0Tpa V9 FN=G3<նaCiNF{{Y˭J+s~ķ&*!>u@W}çoY~km]U*Qlof֪tey&!!Aͳvs)66V&M-@N(Ȼ#OJX//{;;y_.\-/"E<L]L͞?,Po\s9O]tUlҞ-{4fB辣?hT*3GPBf7Z7oխy׷Wkjգ8~ߧ⥊#$..N vŋ#w3oﯾ`Pjv>xFM]LBBTGiԬOl|z ߯voe>ԹyWFRNRǎfRRROU[yMk\GӦi=JԦAM|]2.0P߭\9FɻD S{rrz_/Qar-TH>SՕ֬ѻ_~'kر..u]qC7oz,Sj锥 6_7t}LIɚ|V$Mi:U3VyI3 ];/vxHowW]#iZ7o>c>vI|TQڲgj5p`cgҘ3Uti[W;K ѬO>ݻ%''͛ />1Q7Lcr\oQmۿ_ѱ邼cgjƍzBոEDC&OHв~tp2هP?vRիfA^|b~$<-gogzV}ڰ}za j޹""ttQa5~߾}*[)ēE aۆ X7Uh&T̞_sk/*^\_ *IZfזoެĤ$}kr4%INzu+:ZwY~+>!A2kطOA-RJJVzO{yjݶmyV7tU[h֯_CCW%zy떦X ]PN :v;{Vwpk5ff!XF,f疩TFm{[\l겒{ԫWOk~~y=UrY hI?MҰ4rHIbf^| 8rY{wuwЇ[]uAAjӠԟ T|yy^TXDݳѨ*hѣyV7tޚ0A:s钂CCli ךRw??I_ɋiʕ˦o飷ܹZתc=n~U®>%uE%%&;+{yi֦yS۱g^w -Iews-THJLJr݌V|biSjQO\|Ԭi $^w+V r"99Y+Ў ;{ϛ]kٵ~>WoTW:K=CR dt4O'o_@7,rfWEo_U/_>Kc9Fq 1}R_gR{'uqq1 ~dњQ=~,Yut**^\}Wϝ311W|`7WWU(]Z.^mSk~X}N2hҐIzz{fWJӸ7i5uTl 4zPqmllԤCfuI ,%-k޹>cM0%3vԑ$ XF{{y/s/[;AIR`PP$Y+UR`P,KWɓ~N,%If(,zE7hW oޙJ]%=E 6]rtwo݌cAteU}ʩYWwW|jw+5*\&$ߦ/CywTŷf[^eTbx= ! Pбc :vLWJN^`jg\ WL\B_+ﯾ^ݬ_Νf+WUtڵ o\<֬QtlN?cr]w̻۷ǫYƳ*M ?^nڍzuX%&%Ν>Y$iy:|ꔒs]+<*,vEΪTN5ΣHIL 3M0-< @Wq;.[\V3Thގ})%SzsjԾ}l -߼9][|Szvvt4w==|p7^ŵP!:uzQ3fhԌf4=Ƹqzc8IR!''mj:6'=T)KjBջ] ٻ߯E6)Eۢ<׾vdַNzOM_Bu{NF{{%''  Ńcgf{TD *^\[QRrlmR'pAg/_V-3\ɒ !!jT}5<\?HJ=aZ!>uJA=ZJŽnؠ8=(8]~l_]ԾOU}зZ?T6l_A5*TP@P.VOVZe=Y~ٶMU\BK_zXT)\L~MΞZ֭OJ݊d̙/Zy._V3ކF&L+]nUҢEܴշS'orEv(RDEɝ%8uwU*<2R3kݺٳ)u;6VƏWJJ^~&S Ԫ%.TuUpa9}ZU}|G[Ѡ%I϶n&MoϝS@wwuU6m2y%)u ?Q f8ɋ+d3>y53G%۷5j@<8@:gSU~u՗E0:e;|ꔶݫ^mf8`99^Qe&$PR%թ\9NӲ~Ӷekkv GV}|SO7i;wFTW&_ 4sHIʴ0@]Z$[}hر*A:n߼juՏ_e6y0`0hĈ. w/rWrt}ײӨ]HҥKRȎk_tpԦAy(a@౒3Gȥ{?nCr!D?)<$%9EIپϳ+xb͊پ7;z{yWyޭ{Cv998mÆ:3y 1c.nr.&˛o֨~Tģmȳx n! xyPwsF;ËsN: @vtQ}3ܯIR'jИAjٵervI WX@JJL;ޑz~+Ȼ$)JC}N~qNCEQѱfm:5C2YWVv[}'gG>j=xGDhgP}M8^vv2 OH$9;:dbze0|f$IE `P[JNNӵs3fɐCIInnpxflITS]=" yDTV&45Ɣ8d}[ǯ~-9ŴDɛxE4fAs&"oú98STC:cXTVCN'r` Ҁ>po玵J^ͬX)d3x-<'MBC[[@֭pmYy RU3$'I)>g!/ׯsbKЬNLrsϜ`ddfU]M y#+k~#>1svժ6oc7n-]5~޸mGHڟ~ U\I/c<3򈨬c!b>'#U7?7ѠU=<f6޺9.&PpfXVD>HdggEý;OzKyiX0|R?l}~hjkbiPS.q/"^78=|.epǃ}Kyc$mYYOLDy#\H*-ݺbS[O(c-J~{)iiXk<'^!#3SRL,hQ $<#2?݈Trm=m܁:C@Wّ|xƈS'q0^EBvև45ѹgsϵ?Y\K\G/ M \<`@fz&BNHf>%șI\́A<'>Ey)J]--3޿p][[Ҧ*BS]qƑ8ާ͛%)SpL] 3>66X;mZTWX,;e>Ob+WP7RӱС' ҥ%Dj_Q¾uPimġf3DDTf6 >:X,ٛfl2s̔>i]KW G_-DV]uC.*3oFX髡aaha!i THIKd݃>q9pjZ8 k\y{OB۷>x0ӑӾ$[QUS/#My yAWCl_@rXYwʗ+mcLDl,Q}hwdg`UIPUMkKŘm2YB{E%J?a 5_ %mCfMMQںy>Tv "*t#""""R T͡^=b..R1 tu__gog݋j05X'BB0g:Y 5ddf"{܏=~(L fTnPVP4@_>XM$WWqu-B!&!C0sȐ/w$)\RKTzGDDDDDyr+`QɱjNKx RU!L {Oµ ˻Q"RUų(^6ש_ &j))!g1y6ab`MM'&"6>doyJNJDDBQ]x3rQx=ut`UdoϽшJ򈈈 57m6Uv""GDcʏSCeG!""""""`!JGbС.XƍÞ={䐊㻎c#ѧvsK/{̇.DDDDDDDBZ_6lPqvލ+WrJF_ˆy0u*B``lG<r0&h+%*>PCSCDjw`لeXJI{ZJEċ?c<{SZ""'OVv "޽;>}sbԨQPU[w"? (XbzN:oGXA1AE?gI8[s-\`]:U쪠]d!"""i֭pwwi&^-[Tv,"*Xȣ2/>.1G}t<6 u $1b::2!rLJ:6}{ԩ .'OFVoooXYY);)@)PlܸQڞ={pQ̜9+VTNoQ9#6?E?h]eWگZm"Z浢mh 9xLDDDDT***pssý{0vX9r5kĜ9sxD$,Q3l04nSNE||< -- &MeH۞m 6J~:5ymC[|w#yTt::́&KHxP񉈈e``OOO\zܹsQn]\3̌L<7S_sLuvڡnKP$-z<*RŜ_栅v tV0(l(DDDDDP^=?xڷo>}ʎFD_ȣ2QF4h6m8t(펪"ps b_©z>uVU^7á-BKsAC\1ꚹ6F=;W -% hԦ$tu>*'"""oMrۮ]by-7DEDIOOGxx8RSSPЭ[7$%%AMM 033SvB҂@gL]3U!S=> )) ZZE?|ׯcQNxyy!""nnnG(b'zSjj*,XիW#..Nq%==...ʎQhڵòe`gg8DDDDDT,äIЪU+4o\ٱHX#ѹsg\xmڴAXSNѱM3Ľ{m64iӧXl;300#~Wc׮]JHFDT4b"}5C Kh"[qYK.۷/~*WXDD۶mðaÐ{{{ԩS-+/_b۶mł K]o׮?~`+; ) y˚5kw^5KKK]ݻw+.\55'Qٴw^VZ@*ULx O)S@ `ҤIk={DϞ=`1vX@TXQш+a!$>X >+իW ( /_^f0D"TZ5׵H$$$vڹ~ϟGRR`oo/u=&&b^P{{{BI[JJ Ξ=W^mڴ*,@$$$}l"""`! 555?%++ ƍӧO1m4jjjHJJիWѢE\;cǎ-c^...߿?̙[Dpp0֭[iӦɓ'T.]H: uϬYpUܹsG}ݘ3g%mNNNXre˖|?T@@Xx= 2_\ƍQJWHLLDDDDB!99ZZZ ̙3}6ƎAIڵѺukk.www9r[nP(XYYI͈8p ///tڵȇll۶ YYY?X|9];wbĉD055侌 ~'˚OƳg777!C}!88X򢡡dIDDDT\,$۳g  ann0p@̚5P>330,,L,׾6/rf͚jݻ7֯__YyҒ񩠠 m۶E r  uuu۷P "͛7z0)X,Ʈ]0k,ƢbŊ @?_Q{fffo\ |]JHӡ&:**JGa)o&uOhh(BCCs$'0j(L:ȳ򴴴d\@f!oR_cٲeVZ3 .׮]èQz񁿿WqQw}';ܹ3WY[FdggC$ji(r|136QiBA ڰZ^TUUNVƅ p] 6L=ÇyMGG'ѣ֮]+WXoɜM+s J-^qqq[nDrE6LDD;̙3^^^Ŋ+0zhB|_ƍ}vԪU Z3fȼWVю<RSS2nܸlfff.RWW~G(bј4i:T{SRR,sVFmL>6m¯ZL7 "*=b10i$DGG_~@Z???LMM BGGƤI | <"""*X#I!ٳg B4(;J0OX+Te"*VڅWprrBPPP`z*c~\O|P ǎy^[hvl޼HKKG(_짮ٳgcƌx=tuu 5ӱ0bccRDDDDBAWW*U;w (T:u(9IMjRv "*9}<@6mpiXXX(1W@X"B! 2v'OahҤ ]X(Tׯ_ 7n(DDe./]ptt˗/xիWD__DD򈈈a!|(%$$MC)quԬY3ϽH:4kՊy:::@(jժʎBDDD$<4k pe%'/IKK͛7ѤIeG!"|b\X!!!_> ""<GUvg"99]vUv"oNfd‹yHHH@XXBƧ!,, NNNʎBD߰˗cݺu@ra!ݻwGpp0qQcǎʎBDMؼ‹y9E .}l_pp0Y#":{,FƍҥKʎCD% y$ѻwodffȑ#ʎByHJJBPP8DD,EV:u`r, FFFhݺ7, x7o777yFٱ`!$Zl [[[p w uV$&&bʎBDSt1ox!BCC:.͛78w e!o p}3۷oG5jD8HB `ڴiġC>M6YfpttTv""by|}}6&o֭ĠA`hhOOO\|?~<5j/*;) y$ժU+a.]X̙3GQ*aĈ8r?Iy{%6mgggԩSGq4h/|}} 888p-7i1InżYfGff\$.\l,[LQdr["X#0o<aĉHJJRvoѣG1~xԬYWVv""CN1/ _֭[>}:?`S///=z&MBՕ(_.Y&Ə{{{.%F*;LSLA\\<<+1Tm۔`쌴 tkݺP=}'.]>͛+Gvv6 7={ .(/^ 00wAMMMڵkJLHDTp߿uuu~P`ƍ JHDs)_" TW؟~BjZ"^ęP?s\\ilɛpUsG>ӧ咁Mϟ/gBC8l喧wݻ7^xׯ_mo!*W͛7ɓ'_/Q1<{ x" ;eG""a!Hȑoz.4şc(9}wHxiihHno6s&|BZ0wo=QIgii KKKe(ӌallDDJIIŋxbdggcر]6hD XHNMų(屌scc%3$#3/ž@RS 4~Qa…PSeLXy.TZz:"^$}_l.&$UL 0k2c/MIKË7o˙SK$s)Qs΅nܸOOO+;)gr{N5kp?2RVOZN+K8;8{TX+ozF~_CJZut[6hlSSQQ3̛6X[yTTZYވz"UUv耕&@W7Wߺ.ZGK Qd>lVxlقױ>.6̲|6,ظPWS,0:ZZzXX};b|(oGW_,cMزTWǰ=رm5|VVVcWBQ)v&4SSerHUUf3liEcW6Æ Efcеuk8ԫ.a7޿ ̥șxzϞ^kn^kv>{s>>P vNN036hl ğ7'{^|9l+Uˆ^`oacn.3M0 MkNNv>|Gċ8U_[`ƚ5[:ut:6Oիxfn.-[ҭ[Xs'y%7,"֣ tu{x~#>1>;k~HdԭV̬,SGDDTZFpBhkk+;}E,b\l_vvŘľqO2CCf߰! ~Y?MH$YyI))`ΞCJ׮8~Fݺyܓ!!xΣGi[҇럽.X/jxyk,, {?. -=4$YjUZ,dadl:p<{YYhՠݿ/RǂdQQQA'/_JDFΣG֦T=z...ڵ+222'NG <"Rl?`ߩSXMA͚hݰ!~卌>Xd 8}O+[/^/X >&Hl9K'!RUE9CCl\\жqb<޽u>5r6?xD@|bf/c'[{;;th c]])3ǖ-ز@Y c8-$OO4U 7̵;'k1{"g'{I[Ǣ^ "=""N2 @GGGшHX#"*D"P\AXMԡ9'65V NxhDԢE tka!,Ay9Q̷OկQC/c$\?W_C:32͝;pu\u VùP:XMj*ܓL33ۇ-͛yVcMPeС2BiY~sDVN%66zzznҦcǎ8w*U(;,r***ٶdg~ǖ-X3u_~vf&&֣5758ԫt~BWץU^3gVx8滻#C(ȑYlSWS~~ベr0F,_sJK׮}j2iih`D^ѫb1~_[`SS\YVNNr͐G|ܖiԨbbb@ ͛ޱsXsD.]=;&O ggg9"<"2EzoH7v8˜=ظ?cNݏd-yѦaC:^fr5, j!C??6?9ٞGEI 68΅(TTTP YY@nmPbűn$:!|yDi4HaTb9+HDTJڵ [C򒑙ۏ!;;[uSCCT03+ b␐ SSԮZ>9]CL\QZ5X/gw IDAT/tX/vvȳK s8+HKO:0>{Ά9}̥x޿}]eXD>~TX_,, /_"&.I))17G5۷y II(gd{;;s jW| 1qqHIKCE 4YNAx34g=zښI))x9jU@k줔{bcaZUEgIq&4Æ誠=IqLLMЬs3(TS]8_FB$G 8#dZu+~[)iip>iiJ)ͭPB 0K>D*#"R ̙Nbu>]-|O\%KBѾoڴЙcjm-KVӶR%V~V*"i^}f9l } ~B*VV2۵55󜹙s]^FXsr gWWB^Çsf]Ɖʀb^FV:ʎD߈b=BBB򈈈b1<"R򈈈Lc1<"7򈈈c1%b yDDDDDM`1kԿp,YYYPSSS@"7ZKDDDDD fKR櫜f۫W/xxx &&&׵ŋwTS'a 2[QܠPq?Xu[u^="@2GHaQ} ct())I BL>k! ,-=#.[cD.P H^>G' ?{n޼y… ^KDQ%& |K!""*N%y f<""4&.[֣G˻ " O,˻Ja <"*-N%"JqHrp|Y9u%l<]~z^Fw:v ? ,$ X}`_%"'qhaY[#$, nDZz Gz&2-Y__\Torbx@G&& Ö{pߏffիqMXD_qml<|7l7(LȀ6iwҭ[HHJH,KMH ⓒC˗hR622rͿh"008%2K*,@fFfmXc ގ<3]r; C{{7çJ%LeԄn ~L @QQѸQ)_/Ns'Ij^GDU{7B==18x`ϙ3eCK]/Nݻqn:<;~&L@\b"8Q_݅ 4gDFb9.޽m[؁6ݸUee,gɷA(o.lߎ'OEx>j{hrD{yM&PQRBe2}/ڶ OCCjtߏGcȑxUy!"*O~ل0X=X!^fF&"D"U$H+Lb\"‹EĿOEFzFڗDBlB 1.XYc͙5K!ʽdc9~Cy}.OU` JIUY6xԄeժA&:>'ѥeKlNr| ʵ&@@ r{\~޽;> SF 85k_#3_8qHA,ؕ+S,cs25y87f 547Qyzn m:NB[VGX1e n@pR%4ɿ|UYW.Pw0,e)I)e/h@GXzPY= 0n? "Wfn@m XTRk]@eKk˖!)%E݌ի ]@eKPS'>w7Z-&ٰah6lzN*/]\˦Mtt)tڵ#d6 }-Fǹs2׬A d'$'c hm:}bS֒K9'"DT"2PPP@VJAAAj`ddf]tu6%- ޕKYYi9oU!.\X q3-z2phPoz%&Md$8;KM[TWGZp$u YY8i`,|_G֍Ѻ[kdfdś8\WBEMEsFb!CprIL>ǟ]|tw )5p81 HO#{DFf&_dsRx́!!Xu+=+WJ+}Zfj8rf+I62&=ʀ'OO`!svugv }>sbFzzcU  b/AP20c*T10Ϳ>D"͜޸|:6o.ZRf]vj@z9:_}}|7kH dLI9~7{:C<"""]CR]/0n8B\=yXضIvtmh7ɱj5?F|Lw7ÊJ3D"n]U~,k["!6 弶^,C,#2:=B۷sw"":{>@(bkzph;h86k& vua\^.<#&>CvͮG}2Gen~;WxUjϏ:TTKyR\y|@D?j "@QAnބH$*  ߽{˗Kre=sЬ^R4gJ?F%9hXVYfO#FRW|ݓy!.?m:PVTk~ׯC$fjhZH%85kCND_ݰ025BXHXэk-7W02&7Q+ңs|Y~rh}|IUUHxokk{`oRò#r5y }Y;]jiACM w՜jx│fl,>wI;wbi>\'+ikw`maQ?W@^Ь^=nv-AUGD hjbD9̝+dX$!|}@EY?SIMO͛qetlj(u_6hݸ1](1&26n8;{]|KwH$BFf&峞\]׭r%R%3`,R=iE]7ww.=}[r)*+kdefɄxhdfoۂ ŭ=*mXikh@UYY&*Hrj*@CMM긺*RJ]"& g/_1|(<6G#*&ӆJON :uQ2_hTKJν֤cFd?uz*BȮ7IDTQpD'?Ux8^.@GI))HMO?^~۔)٫C]U1HM/zBnݠ_#=#66سpaO0nzL3ccD|̬,,4 --fD5`ǩSDZz:.oڄvӇױj^i G;;|׿5坲[ݒM QeX5 P8232*6Ml\Kpb^f2"n )I;eP%{upK30޽|5h~iAZx5223\&#w:߻!<{a[֣G{6 ݤwY]ϩYB_g%˗Y/כH6(hۂl-5,z5ooSHF UD J۾}а!54d/ܯ=Z y.Muu\Xgp>@GS ѫ}8/]BpXb1aMo {^#*&m4AѷC(*(H3Ӧ;vTϞ_d\jl<|w`;kV֣ [)xA_`Uq~:o\u bvvwB1Z1u*bb(Sǚ֩XMk7z~(Əw#m[[\?~б_RqfdefEҼcsX{^Hb\"Z0./f2>{8q:qy@6M/&*sm^P pv5~DXƍ!pUOrdΨܝKC,D}HHNFVVjK6ȫmڋ.^ķ}H&M>>p^?@UYYj^˪U/Š7}}1~޴ :.SQ !EzF[:u숣/ԩp9@4YSjѽz{1irfjXs'Q5U //l?qZ*D~j0Cff&.ݺ=gHO8$ (ųFDy0#"""":Vد#. OѶkl:#Hoܪ1~3M?Wкtmj2wFv:jhtk #{xj8rJ7E*ȳ251fhq[:Us̙O }svkpS*L$"HUqM4'luD78tv@VfjԫQh)M~Ԛt`je PO\޵=ξ>^&5Ks2Ǟ[{t7oCXjl;>w.LI&ENI]'!9!.ڗ A-ss ;wp'(YYY:hMuzMqknz=.89J~7ϝ[ 0RRfkVKLDlB(8׮Ų]ydy -uu^$y]ѭS'jǼ>&FH~;vlǫp$d/%3//3ccFDt4TTPZ5tvp(41{YzPfDT1#""""*UL2 ߴǀ  ZVE{עWURzZV-5oP(]{;݋D"?moxkf]v%2 ^AIQ:vDOnZ87m`8y2Pؕ+7s&<,:נfM4(ڱ@uTZZTd;%EEh[tk>2(DD5O,0Ae}jmlU+a5tRy%7' f ,=&/_T:VVH-?""*?GDDrI%}5llm\8t]DlTKV7ň#cdhi˻}|(Ȟ=Qj**KL͇ ؾ}\)ׇAQ T:9E?d? w_3f@K]ϝW˜Ď_~ }v 򈈈*ƭc_>XX[Ȼ `ԩm< 11MH6MLL>yDDDDDb;7@ږmYDDp """"""""JAQ%D~3gBEYYї&%5<"jFBXXK!/Tw++tYeA}԰sNyADDDDDDT,\#`GDDDDDDDRa IDATDT 0#""""""""U삈(O`.>؄"x[ &5=]%Q% #;w~? w)iiiUV2utt`ia×.Kr5w DT 0#""""={~zyA2444dkiiᶿ?^z%k&MȻ "}D(BOOOePbdd###yADD_9nvADDDDDDDDT 0#""""""""U 򈈈*yDDDDDDDDD<""""""""JAQ% `GDDDDDDDDT 0#""""""""U 򈈈*yDDDDDDDDD<""""""""JAQ% `GDDDDDDDDT 0#""""""""U 򈈈*yDDDDDDDDD<""""""""JAQ% `GDDDDDDDDT 0#""""""""U.*.ɻ J͛h۶-MV~Q.\Eɻ JԩSCvJ<"""""""*қs砦"2*ƃCl,\\\p̙Ry 򈈈HZZPWUwD"4iaa֭<==T~' ͛QE_=z􀗗W`GDDDDDDDDXT͛aW0AgbQ l)U 3*m 3+M HJ1#""""""""y 򈈈䨸a<"""""""""9+N (*cGDDDDDDDDTA1#"""""""" {{)ʹ."""""""~bח95}F+2Iyp;9:HTtgΜaGDDDDDDD5fBVD5kp5dܼ  ?1X,2(ׇ@5J|ׯqm$%%1#""""""" m܈^Kv""2}{=|ܾ SSSGDDDDDDDã`rEeUXGDDDDDDDrg%EE,ز:t(4h‡84U .m@!uv.ׇz}EDuDZ6jTnRH/<{ 0G5ѰV۟~A071Av,޳gxuUUXV,r6""`b`$xLhꪪvDb14iZ^{#\Zٮ uuK]ǗAɝ f+W+\hSWb9HNMk٨N'Wy5 ̷6`'$}J>ĉVwo"#cT 25UZmoAbrL::8j4i"u\,cXk3203+fzU@ <(+)¹E It_q䘆vΟ_俅 Q0_?c-D{?]--\޴ ߿?߿5k$Tcw؄pf'?xǒ%2mb1ϙ/^`;^9˖!)%}f@ZzTLh!N! !NaوMH7 _a֭hdm˛6თޞ?e #3Iw(ASؽ`223!ko s]9u [ϛeK *Uee=X+oSHLN:wwteժX?{6VgHFia7lZ@\b"BaW|k 룑5,Tiw>|cML֩~6 !oG5T736a€g/_JiXs'LMquVt6BI|&p03C/GGԱBdL jax0G۷v)iiXwׯ gȊHG˥/<"""""""0 U`֭'0 ٮ@ @oGG޳g Y..'[e 9#*um p<^^}kJ@rE|NrKLKPWU-*RLVSUEjZ 19"?['-ǎAYI5yDDDDDDDTa(+c7`8VhAQAAx5y ⓒ$GXg&!>19#>9G#2:ś7!`_̚ H}8o_SX;.T܈U]Ǐ׀AU(suŲ]`VI8 Rʕ;]V-'˝ZCU3GPh(Z #.1ZZn))*bp\B pA`-7ilmld,uk9禘s&Ldk|ƠZ"""""""P1owȜnf؄:(8r{Ϟ˘4p ttЪqctj+A\b\"C]]h'NĦ9s$杵ue:s~R('< >s6-!yDDDDDDDT 1bnɱзoqe03 %EE kqFfVVAi.AM|G rt܉0sbLiej ?eꫬ֪:VV8|.ܸ!s%Z"""""""pر2tꄵO-jHJIIj!6!A2/W7o򚔒r-[44FzzPVTQښ)S0`is?fY#GsqXt<ѠfMD#,"U z& u~n?>[=<"""""""u 6ggټt놫xyvU qfLw8^)v+v,G5 kՌkVI+wpCEH*ycPWWacivF[[4WOކ2}X[XRmml?oތ׮[PWUE51ORc5>6Vp#kkYF_6M=u󃗿?4Фvmo_Z41'}͛E!)) %9HT$bI)),=D p-ݸ!>UFff,5HK???GDDDDDDD@ d!^H/aIB)íUZ*yDDDDDDDDX{7^c.#"""""""~\^ۨ(`ф .<""""""""$%&A͚ۧ뇪.<"""""""" 25򈈈*yDDDDDDDDTl)223]WAQ)56 ;;e]'FnP(r:\#&!K$bHHNwI%jb1>C<"""""""R+ *&ks5%w5 &LY0i"۫*+͹sHKOE*BʋAW*3+Kb`ʡ"0#"""""" #)%} c==[Y"␐ @S##(+)[%0()kFE!-=PS+]L|ɩ0)V삈՟0=9w ;9ȥKRE"_cgg4tsCΝcon'K7oJyhocggtuE=ݮ͜r,\\P_?puEq'([C WWulCGaZz:nnzXsL^wrww}''89!b..Rmn=zT}>}ƃG..8pLׯCm[88jΨs..xH<"""""""*W-,!.иvms>>GmKK?oڄ%;v:uµwqzOۥF}2!.1񈎏Xl,Ƣ=:u@ @rj*<}|p(8|y9?;a?ffX5}:"z>8;#==?]ev3G@ۈOLj  tt;l>zs֭CuSS EҶ)uwpkFZ 036[ `Xnǃ/p}y?6* ƍCH_jjXo̝ 85k&6!9\\&2NKaQ iS' U[[(*(; FI$4؄ؽMllpaz(+)a€ {[ܢEjjݸ1'~Aݻ7oI]޲LLU`eX'i%~۽ƍ(o@_NX/GG42>>RA)6#ᬯ/:u!C ^.-[X}TG۶عTvCؕ+ rpj-+-uu4SEFscc08F]yӻԺu|Vר bwr ޿Gӧpm^;ߪHM G/D"L: 66DpXXj/׮ \۷kdm֍@ه UU?{GQsl =B'phO` b1ھ27:nmaxIj!ʹʥj&&27 3 0ir6"=#@)iie)Db1^GDeÆ2xX[Xݻ 3#.3k ]{;;_yC2@fGsWvYgö,elb1}**(r~QkӤI:yy֠ר!ն!FzD__긚 R15ɽʕ%!,"B>M!ŘZ%ODdKWK R{)P( M=xQ@:u{223 \:C 9ΟNLM"?M: lS 5pɩ}|vMJ'! }@gUBO[z jxQ=DK?~,9aW.41ׯ/cΝhV/-,еU+I[nPHv=+]ԬӇg,PWUŜu됞 *㳝OXh{{p%n]h.H)7f ˡaGDDDDDDDݒ%cotkG`#s]y04p ?sG/5sףS*:sejt ;~Irʕ$۾} ~߳Wn_M>Z[3V'$k ؠfMI'p1d\۸6n꧰ 3-^gcκuc줂IB\zxHwc%7]{.Eg Sng a..R[ PII)5^ qQc3қ7o-Z$:__qq50~Jxsڼ0ׇSfn{x>@K]::0ՕL7^xL O%:>wB߾ōvv/bX~ɩ010@:u y< W%@G071fgfe޳g}LL`Wdc KLćjhEÆR?\x˪U4h(*(@G'}D"h^JZ޽_hyE !9 B!,V-t%Ϗ#򈈈TWGj>6 *4[惘 $ "`ejZzJ_[lgejZ uu ^ц"EQTP]ݺ[7:BoGB()*|$J#= )7 """"""Ojݺɹʍ#򈈈܄ED`ҥ>k Ȼt(%ݻp5J8Cɻ/P(DѮ];yBDT!0#""" 1c$[Æ{{8燴tE6m  ڄkws˖А:wMjj¾~2/p #==ԬV =ڶͷ}H^^ tt֩L ?*&.^ĻѠfMM!};AAx E86kxX{7}Kv(SDJJUѣGxyyɻ$""bGDDDDZVзCl8|Zmc,81ߵ69e%%^FF0󡮪Z氪"#CBT54̷W8z28;cTW]ݺ/HJInOw5_A$DThjVѠUjڔTR_GkEg+99?44D>ǣ8~ߧ's +mex8 'VV|٢:v'A{R_4i°7?Nӽ(Y o^nHSBp-҃KGyBO !B,o'K1WWVnƮcPk4ɕ S# RB4 ]f#B!ƍz֯7v!2D iٺ5V܁hذ!sϥvڙ.՗^ܺ$ۆw޼xBy0f~"""BdKFB!BlMBRB!B<~8]:y6s HzRBFB!BdCQј['0>U|(i1~:S[=k56Mvr{kVꚢ,-y////_N=2(Bd>) !B!D6~mɶTiXq+ƥXbccS|!UMurɕsMMz*wIXBaTRB!Blf̈́ũS_2:%^OJ.d^^>˗̨"p6Carؤs-ߟ1GD`kmJٕy`x_v2"O!BdW8s* nn,D}ribbb^۷GFFxZ\j6Xn?|߰DɃP?N>;; JtjpTt4_DDEaafF 7DGv Oߟݻ:ŋo +T__>o8^˷oSѽ72ʊשAۥLSzjłᅬW*ݾ=wJ%7q tw)ѶmB ]5cшojN/;wjUO)_>oܠ|451šC}s͘ HkGō3w~-S l3\:yO"zSO׾{!郧Yr<`*oX"i!2}'OxUJ ֨[˖;>^!- kЀ-Z)ww:xy޸1n-[1B^'u+*ViP(tlԈ(V~Jv ͛q#?BU(?>_>X#;/! "ɢ9saPA\ Cyo^a⍘YQqt[!r ) !Bԥ ֯gѣ)_>lт‰iPWS)RɉQH$ɕnny0Y}[[ZRhQN_bp'j|պ=ZbΝ 4(DhXg^EZEKhw#=^;~',43`>5/Bx9_A!B%(A!!Ǝbq@$ӧy,׃P |C:"/n4^qr/ 7~_֖tD}ϓ/y?`<]kvIOŹhco?wK$Xț;;\OCxvN};o"#y9sʗnlwʂ[f;^V9cS|;vQkQF !DZI!Od;:M6;HP :Tv"B"?ZM!G4efj,i<ލ* }Nŵ@VG,x vu='N$4,eG ;w#jɹ$֨K=2XӁkmJ{RA<ݿ?Ύ69٣gekm SzBDKǎC|57v !Ȝ_oV,;wVf{7vTw;ݻwu6mҽFժP(X};6Տ رi|,^?OݻZ/RG xB!r)lQF:߼۶r{a7;B,jDDE!v;F%ysv{sׯٳݳвvm-v 2vb:|=4!ɓw왦ܚ9Wa eaX[ZU(Y_+ *. Oq֨}rԯO.kkDE4Z>-Zc>cˋOr6**Elf.]Wʝ;[9 n8Yח*>>s<݉ү͗sׯ3e FgiϬ.6&"PYT+%qqaͮ]ٵ  -3dHsK?~?xؒ035M7{wx ~ysf_i hZ qѪA;++X'2nɒx䶱1xlinn5)ǎĄu{_8]C>-hanY5[_w_D 7s&o7aٲxSe',[s%qټVe 9+WꏛТVDݻlܷ6B%\dficf7"V!r") !J1O!D޶{ E+lY?u*QQy&&޽rpwWJ)++^aațRE$qg^BBP*͝vvm|_nW.kkN̢ùz.oD.Q011gݔ)hbb--Y?u*S}}vyRD7MoMdCB_}VʦMܼcgk{B䲶N[թþŋhu^6F;B|t'HwV1-J1O!DܜqsvN)fNܹ]|/必GF&>ܯR"6_b+%qq47OH*J1Wda*BB"CH1Oq޽cbb{;| !Bޤ'0RBdWv' zo4!B!S B$*իDYp# !DP*\|1D251I4_!ByB '#B| ̞Z1v !B`2"O)ddB֖Ǝ!BL yBL#СCݻZj~ƌÊ+۷Ȃ'D>wmŒKFҪG+LLl{&%#iק]&'5tl1&M:5IGU6\74_ʝ+wv JUʖќ~j2W1B ԇBcnnF!DV6c vټysx"//bŊPd !>|;ll/."tCRgqf]?柁B!ݝ￳k׮ʊiӦe@B!DzBtL7u ?nV|7a|; &$(Ŵ[oqd\h珟U{#D$ؘX ^x/* #6&UV}B!"밴dܺuٳg'ׯ_3rH<<<۷o&B'D"mndۊ*2zhƦśh֥[MRIO t:ʅ/kL/*իD@VM_讣{A΃:3(,B~طq/f輡ocj IDATmk6ɨg>L2R9j,̒Mm< W\rݖYϰ4hj)u/|I߉}ܠ2-݆u\?{ݠ}㎍)\0BVVe;ӦaفR^%*7:FDGrj/"墣2e JoŋƎ$#VeȐ!It~ufB:!DZȈ\yr FFXhF˗䄹y;h=ʪU2d'Oo5v,!r`Fիqvv& cB=z4k׮e4h xm~>ܹse ! ) <yxeNJFnb;|8[Ix?|̥p/뎅O]&zȽTeIDܮ ˊbcceɒ%/_RZ5T*]ll,'N`ܹ׏/`ymdE1f޾}?B!,--5k۷gٌ5ׯ_3bʔ)# H!OD88pj̓C\+usr{iRTR;`zQQ\2i[-?MYg)Kb߼c!TٺuƯJ׮];vlLήT*5k֤zL8%KV^mhB`m2eK!G]v4k֌ɓ'ӥKƌChh(k׮ .F7]!QQ5"<s -ſ/~]:$})VnVZ\E Ҽksm9K<`тʒ#ۏ`ߞK̴l2~Wz㥈Rիk֬᧟~2v$!>zРA^|I@@"BL3o޼x_\rEѵkWիgpBvHD X?o}ϱwZjvqDZvZ~Vn3hWNvnʙgh_= G-19t؅ve~h?sTO*%+H0K5g2I&w$|'TenbРATPC;N0tP*Vu떱Qh4ST)֯_׮]"k!)ټy .Ȇdj(Q?oMj&:ik.v@RK M ;keŔ/ƕ(_ xjFQRiBhۻ-[Wl%2<+[xYf>}&OPI& ڒF|{6'h={L*ٳgӨQ#&MD@@# QymݺuYpBaTGfݺu_̙3ggg#'BBBԔnú7/C$ ]*t!" Яq?:H灝q%s--)\pJ%nI|2|?|LQ=3 ,o1roaƍ,YqZ->痱8 !0JÇ)PW2l0vMll͛7s*-[2sL+f8BH{Yw~=+}~z_O/2ydcGK'<ɐ6C"S>6F;Vt:;UT)T`A*Wƍ={%D) ̈B t{{{cB-W\F޽vڡRҥ E#M/_AƎibccy&7o|lٲ/-$DV$3cFԱcG<<<5kjqH1) !Xn߾|+ƭ[rmBd+++Əυ dBI@@!!!QDлwo>|ȺuEB"yZruBdN-]LB t]Ν;7J ,@RM`` <0v!RD~ !ϟRK/qT*111:?((vѨQ%DPtiϺu(WBuV)BbŌEd!KŅ[;)bbBUF377ŅZjaii'Oet:]}zyyQhx:ğIdd$%Js)YUtt43gdԩ0acG"ԩ:u2v !DGGn("Q(ԬY;vGD ߩBpMV\\h޼W2cƌDWț4iT*lllؾ};֭cݺu8;;ͤիW_>ϧL2Ǝ%Baܹsj<==EdA~)6l=" &-ZGGGΜ9Yx1 4˗/Sn]Ξ=˙3g`ذa9s3gеkWG% OOON8A`` #G$888Kp LJ +8pB%PlY#'YQ\.D@ yB(JJ%s&4jԈUVT*ټysslmmɝ;7VVVXXX;wnrqȓ'JO?f (4 ,Yǵk1v4!BD>})b(" *VRيL֡l"%45rrr"wDFFׯcccç~jpVZ\p7oRr4]#dB!7oRhQ$M$777nݺe(B$Dcbb;vΙ;Ǝ#Q%M=ͪvɳgϨ]vy ϟxN3bV^32O!JPPLLPPclRْ~1!2Û7oXt)\tcǎMfԯNKbBgFaѢE=h,B!h iӦv(֭[G͚5)QD{6mÇ888ЦMmhؿ?O&<<gggj׮Mw݋-;w]v$s,X-Z_qʕХKx###ٸq#/_Ɔ Rfx6mDUVJ%^^^IM ggg=V"[B^ ݸq{[OL֠ZlwxÙ9s eʔAբR,}?^z,X@ !"[z!8;;g5ݻǔ)SӧCM9ٓHfܹ =zD>}z*n 稨(ϟ˩WA}ѷo_h4lڴ?{{{}n߾dV777}!oʕ9r$v*;bjj? $::_~={2bnjCLL k׮2Vy1NNNr !ғ!22ydɒxCn---iӦ cƌdɒʕ+ԭ[ٵkjPm̙3{.'O6vtѡC߿5^^^|'̛7ر07o;B4 ɒ@V3p@t:?3uٓÇSZ5ccc0`ׯ_t ;;;BCC9yd֯_B`;vaÆ{ns[n%::Zx׮]3I&Ѽysqsss,YBTT ѣ>}BA`„ w_FϞ=]F :s믿MGGGBBB')%ŋx{{娨(֯_oƌ3L>{:tgү_? ?^@ZnX)UԫWB' yXXX[YYƽ/333̀wSrʕtW^o><<|3p@<=="̥K4͍/_C/^H"Bsiʔ)TGʹ B!%03c x_θܻwSSdONTƻ8={6Si##Dv!D,YD?vԨQ4i҄SNf͚ŪUӧ*UBղi&T*ܸq`7)Y$֭\v Y Olɓ'ԯ_ڵkS\9|}}e5֯_O_>#G`cs1uTVݻwӴiS|||ի;6S} k׮dɒc\B!V\Q);R.DriJ*E\ǫTBQneggG=2uT^n. Y(ׯ_NRѣL8QחK.Qvm,Xa !2Ff;X* KŊ*xxx$W T*/_O?ľ} ˋ~i'bgg?=z_~ 8+W`xk!Bd%666f7**^|7oȱc sppɓN^jуL[+/n9-kk A y +/^{{{nݚ`[Nƍ7o5kԏ;w>>>??dmР 4HQߟ4ZnzjJ:uڵ+3gԯ"BM+de֭[#nٿmܹCѢES2$5B֖9sy?2s9!B y x}_氰0JekEDD[:V˾}P()SJ*qرx!Rٳg .+R. ۶jՊqƱpBoew[!Bd9Y;/ =o1ё/K>SPT,]SSSΟ?Ϗ?ȉ'2wϟ?܋/ݻrJ3$ƓB.䷮[J*\2^[J'ܸqC[3((L.DNWP 5rC]ﯥ!qm}||mWZŒ%KZjFFB!HYiWWWMիWx"nnnׯϾ}طo۷OX)UՋ|ɒ%3gNK.i$.\xw{ϟ?[[[_3sLLLLpqqɰ2Vd7RK~%QQQz={УGϟիW vj̞=Z͆ }ٓ[F׮]={v[o !R(P م'aq#d*VYr%ÇK.ɞ!Bќweuܜ)P_fɒ%(J6lhоYf3m4 ,߁ӧ1KCռyRhQN:)9;;Sti>̩SR ZYfhԨQ}b 24kgg EvM%ZBTR4ho߾tJ%۷z1i$>|NܜSw{_)Q NBLܹ7rCRgϞܼy?3 !B 2Cjݺuԯ_?|2y#44oooK͹qzO>17W\̚5 F///W_C.n }UjU>}ʝ;wRu]tSz| 6~4j(^cǎĄ=zЫW/ZjŶmۨ\2m۶Mz;UYSѣGXZZmȈD;7ju 6dƍ/hZ6j1c0yd_~  B `ذaxiӦhذaׯΝ;FTC˖-0a ,`ǎ7__ A'$**J+H(hӦPT)\1D6z"mܨ.yB!%Jfbccw.Ott4ݣI&Ǝ"DI!/ SNZj <;wPT /Z-fff 0'eL"TVXN8A':qYLT*&,ֲ%}ŋK1O!D*WO?č7(U,իh4Tb(B$u4oޜg޽Ktt4ԨQ֭[?ctۚ5kt[!RyXYYw^)={YfƎ"D7Kbaa!#B$r\pA y" .H!Od+RK&SSS7nLƍӭϸ͚5c\]v1m4z)VVV4nܘV1333vlGV~6mlJR_GP?ʿB!ʊ .$ȹ.\@ܹhCJi9~8 믿ٳƎ%D׭[7^xƍ%[ڸq#/_[nƎ"DlܹsǣGp|4.Μ8q?o%KЬY3u]=z#GHٶmEuYB䈴¼_~E'4Z!;7n~c.IȲe˸>"_ O\ba//Z Jn$E_˗e044~̞=={ ah.\Hjr!͛iР*UuI"]zyЧO]#[ O\dom-a^S*ݛ޽{OPPѺ.Kg *,, ]yQQQO !DP(XjիWg,YD /^>Ɔ+WfoovB2 r>Eu|ǎi764~{''Vu4j6n+5ڵkL0!)"cVVV=Zvrb]vt֍&MP\9 ucj۷osQٿ?.K&A: akViߞ_e .ΰMmNZU'>~<QQcu\\3ovVV9VoAPpaNJϞ=6lLB!$"D4]v$! Ǐ3ˋfnn޹cXڽ6躼40o_}|mO 8Ϲk4<|cgeʼn+yqK&N 9S'}TB<Ȯ]B!H'D/Q>H#6oLt]FJ_/bfb„ Epcbbc1%*EhzwR~r!ym.]4ml¢EhڴB!yyB#,2OR3mۤV-uOgti<Lzoo.^Gݺ:u7'*&f:JEB|<;vdʕ|9T"P  cٶmΛ烼xIuNTɵժUѣ;͛3lllt]B!ȣ$]Hױcvر uRBԯ;/WTwv~k>{ƾljJ"g&dk]> 긚eh` aǎP(t]NP(۷/:tܹs !B I'4Z֭ڵZ\ɓyZm+.o!8|,B SSrM10,8 NT,]+õwqqrY.C!Bq2_FhPbE\+Ucbl1JtB[,8ѿBl\ֹWҽH҈,ݧ '3gx qrٲUfK7orm%T8m4$USW9aabajJGG֪Y=3-UmQ??x9EllhQ&o!7S ߰ԯV B7 сVêU|4v,uCU٫WiU+vT,]g!!L2*K;{zzwDZ43]4Q#Wjxb._2D燫+K.u)B! DXukcgǜ98}:~ׯk+fgŋ\XPH ܡd"x/\H2e2Ǹ̃'OX:iR _eڲek?r% Wϰo!DUɉzUjFϥżpaN\ɜ?`Ą2|a:m|lC0+\8Q?Nҭ[UW֖zUR\9 u\\4t*^:..+QBsB෩Si^6˷o稟  ٓEq{n/ p.\@ ㏳۷o}XXXФI]":#AƏsf 0ɋqnʈ=ڮ{)jkpl/k2f\Οi@W!!o.YˆٳSݗRo YaŸU̜QT QQQ3F!tG֚)116fL8ة;uR[+ssLFV|ę|_Z5) yz3[歷#wuإ O߿?@yÇg֟3ԩSԭ[Wץ!NH'5Emm67oH˖Tm˗,ܜ '9(df&&f v=Yty=GTm<|@>q͚ZZRX,j!BMTRŅ l2dlOl~ۅYZń$BH`myb RKLlliq.U \4}z!@ձ05%K~Q|yB! }==.ͰaYeplK'/s} I'($9 N8ny42>}pӇ~~T6Ukk =O0sذtC/:v,[gƳqco3i"ͱ"$<ǎ"4)e{=BzKJK@׮]'(((1eʔJܓV i6D6B vqTѢ/S*QHT֨A1[[c_t놥 7ndbfb%k(Ŏf&&ԭR*NNRVŴP!1q\Y략]\8hccoꊩ ```3t]:zP(ׯ"0o` = DLyu9?mһm[j5 "v&&Ng*}ݷֱNtrwOն ˖:AF|ШQu!yJeh@dt4*Vu)BFI'"'((dYfm2şs{"{2rH2skw>vSt\׏Kɝ&`l b\qot]jq̛7Cu Bk !x4V[rѣTA쬬ӓW˴WH\KI\+U!WjU\3Q;Bu1ܸޚPkyخ9{H4Ie WƹdI0/\XӦ;|}Yw/Iu-&*eӖǜ?HO#QQZjڌ=ξDErA y"ǔiߞErdٲ>$, {~_[_J!߹s BMb@Y"ʗ, @@`XhDQQ8/S߹CW ׽J JXA6mS* c )$T *i:qm…5DŰ6ø|2u[eèV9Uuʩ+l^9|gg2|miKs,uFNt3s玦7Mo]G 8Ao݇BED?匉%>!!5tYp0m`blLc.G+Ab߳bj[<0eSNJLZ_(qȫO:t;4~YH>iߞv uhg->~=Z4!w$ '$P G+l8p۷AF>r%J`dhȦ>~|J88hYabl̽GR1 k_GtDk bbHP*"$tڴ᜿?WM͚߰QjU~۵WqqkB#":s&J…5oNff+v`ֽxya:4iTz5 Eh(?ZGݺl5KdRRɼi۰!;~.O_ ,l,(Rzz]~p3H͗Q-a!aC t_cJVvV,YL=iO4lPkBo B_/ B}qqrH0 IDAT {131a urߟi_u˷nU2\q_ёƵ 6B.To V@H4H3*,< :ī3gő#toՊtVݓn:V9v˖M5L_OSx<;tϺvЙ3p*mAM :|#G>t(O+I='NK уpbYL0 U7i! & /bVē$)!Ifɳ`ʕק+* )^[3޿Zf*ڊ^zѱ\G;l/ZصK.|Vv ~+юҡl<=ضl[~.݊•Lhid*\qUR߸>/j3մgTO^dZ}wYړnUѭj7ڗmϩR۰`nJ7Y7ctw+wڕlF `SĆg|N`6,ؐi{!dD^ͿOR([8қRpLML(bcBB "OHqPQQ8ۧƓ"#yL LÀG]jY3~(_qz5kXm__ϿOW_Z~b/knec?&!>Os 7z*cc!&:߯dTQlRΥ4m_lޜS`apʶoϿOj]olhG-[`X9 ~]xb{Yw/8 CӦ5Jk@!Cx7mK:'lʳC~Y3YMJyŒ׏֎aRg(A:ʪS=w.OOu`Qo|昽5'L`h׮[ kO35iX0vfQ2kss6OgJ_+^&0z\z'C_O&MRC22BPxIt)-`m!W,]kk|ϟ:4B/[֫Ǿ'8}&S՜rcCCZ퟼xg̓&?`h][P((Y7ה*R$ÇKo}Ʊb .⃋16IadlĪYg?OM{stQZuoŌ?fd:EPBWU]FsVWF*Ԩƅ9s {سq}ƅG/԰g1+ʼnG5L^695ͻ4bvvwخc9xAZCx"6BI" [SJκ|FFpl\>}Jrֽ;Jhv9š{QլymGQؘ}RٙF͙-vqaXnDzőt '.]Sܫj: ӓZ+qxy*.lKoh`BB<=ݮ߾MPH67;++3>s}CC273zիM7iٿ&>!..Xp| yo?ak pI!~Hms/'~H25^իkΧ56A]c>y@䫸T#N nnQH  Ei)Y/BC5^vߡgS]hou!8u!dFKn[##}0}}}6Gx_={ Jd]9u0]tԙUVq9 M>[sWQ2^5rsjZK4z7u͞>|*AB [ J~Ⱦl_dIw~XT֍GV5#b23ܚ5u&-Z-Wqrnƍ;e S-cŔ)vM]]ڲ_?? ޣGiZ:z_?? 3R:ۏOΨ޽MP`/AyRIu猟'316VŊ7L$4ST8N3_{ԫZmMbDޠm.;8РzuMr4hF|զf}*NN$x)0N~j.[~=^|IDs׭㫟̌[YP(K\||N$ j=L _ܢXbX{Dj`@ %˗=‚8}wM\lbX{xTaBSq>w11M\"2<2K-mAB!$+ [ }|xp))ԫZ48"IK_ C+I}ڵcʼnSSynn( |[7fŊZS]+Ub㜺zv Y'"jޣgX{7k%Z?x][զf}ZeUR<+ddS\uooLp5-3V7l|-n%c([%?a!aY5M$$f{}"qF;T* J'чg+wON:^)g0,?B$Y\JTL w&{~* GEDhOKfcvP>OYښKsOFynnZ}ǧyg@F !w^e䥳FޛNޑut}-ۮ]T,]:..Zyz% >&hUTёѴ+.뒃˨yIivFTzڦwovyBU/BC2z4.\Dv|J{Zw5O}(Yo'٫8J~][4];[[`m/C!U1^adhNhoπYce;t^ժ22"$,WIսNPйyslB篿f@J2T*뇽w={XfTucۛ}'NZ ˷n#ool,ntOi4 5W640`wK+GEy7٦֚ ٳqؑ&jaajJhD7O]AƊ+88n2 I.bTu|2HkPIMZclYq exK랭iA#w}cİ[ά(TT=O\ѽG6[*Aũ(QųvB 1srFàΝ5԰YXN}'/,}Q- p131I [:ySI>ހNRQΎ ʕx=k[SB!._w;Mj[2iUc\q ;++Zի'*&~~R9̀)](~ׯ.ۗzNst!-bΝl:x;RȈ:..x6nNŔ,RIʾ@PhFF5jpeF~~}6 ܙ==~٣N%JbvݽKLl,VUL :AFԭR%=[իEդxf!*\(ecbjZ S S£C3Ę#zm]v,f ִFϖdlZ@Zvk= 2 59Ww֒-K`moK yEuWrC 6-܄5e}6uZ讣ܻ~@c֥[@TҶw[M`B0/~CᗯATsD,omK: L]!D$+.ݼ1Gzkdh}׭R…ä4os7ߧsf7uuլ7{w&BLk:O˺uz0 7n䗯B]724d?kS*߱#;v} fin~M14F5jШF, .]6S RhQ6L7(M-{Kܘ~ިHO_3gVL_?| 1zhf Ğ57T@j5:^? mQ[~ᮞ /`x=iYYx?P䙘0|~2c榪/.)0g&ȜQs4ǟT+uƯ׺ٿkv(5a73({4i$\jkOq70Lc`=#GO_R9ȰH掙Ka+öB!R pws͛4O;tҒWxř(\ _~F׏r%J<$$:of@EOO.1w: ښq ' 6ndu}^m`enΝ=C\T5M][KKu-Q!X\)wLUQ!#ycb]0"Q! Fph(kc9R52OOL|Ǝ^_>}cڲe|>sV_n+uT*S/q(e˲ld/uS ۸qIz ]D ;0TȈC^^ ;v; n >n> o͔.VLei]O08r<QQT.[YÇk-/Dz"y%&{۷JuggW'gzz,ܿA1h_ch66|#DzΊ-fmJe(SLĵ*Ԭ@V@18/Nr2%ҹY3n#GiQNmذs>}ʥ[PT8SB2w?PP`BܤNoub1d9Ј|)O_F\}˜k ;++ O43cI.M֬8FU^7}11_D>ӇO14c⒉|8C]%yHԙIkqiH 22q͚[Nŋf%6i7-ZdھC-뻢 !Dnj۰!mu;|}yGdt߾9Xx_ipuR{tb.MVvV|;:o;VBAREU/iKB|M<^z/khoOڵu]ByGqtd.CկЭƞ3+"$͇!\+UBVr.xY2"+"QrB!yy8(CgΰALL=r$]=vIS]S 5k9xEmm7f KNEh(ӗ/SjZիǷk7G㉌zOߺ:zظ8Z7hY㾋s 3K־B! *$ #zٵ6t${޳ISBSZq#ϧ &͚ȟ~"6.fnnܸwFv>v/؅ %,2Rs=~}}/O:u(S'O9b'Ojz5 4YREfAO?e Ύ%[Po_^JYp0!aauE"# #$,[}QQt5ƍԕ+y6n 7nB! *'zJ%eu]"4uue?OR4ǣbb8O55#.ܸז-xԭy040Ro_g>j;x0SyYM ;~eb|;m2nz4%l"B!&#BKi?u qq6aݬֹ(T*T*GkjZB!"m !ȷܓ޸_ASEmxd$zJ% :<0<**Wjĥ/XMF~۵o,…T.[V64"HxIZ231APZ""&66͍OT*(u<($Dg^_OEdzЈn2<{++aom]dt4O5׌Mg!!DFGpc ?XXȔ_!BLe6sv;$9V͞SuTaKZ>(OR x%/]b_*66:7^^vqax!$<B柫={RjU|Okd>}hR{|iF !BHbeiɲmXmHTb/BD8*ő牉o}4Fuo ʉqIDATՊoh՝qqr={}U\JjsGN | *>y%&&DFGrN߻G4B}2:9FѠzuj5 |[y2I;yo=|==~ Bl]] ԩ|Ҿ}mU*}&O|߭e g2Y޹Yv /yj377ةkײ篿2ի,<Ι͍fnn 4ֈLE:SWp=F훭ׄB!7#Ɯ:}+Wˊ+F:ut]yg~Ȩ9s(ֺ5q񘚘P7{d聑!bcfy)ُ#F~HLLoԔ.%Zu}|4Mظ8J+qRܥ jCj324CӦnH.,ڴONeӳM%dP  ;u˼}o5iC1saԬXQ6WB! *PB]!y^pi~ݱk]W/{9%KI8`+ogc9VX#q\9K57^cK|%˖K_I% 0SK]Fix8"m& 0kstw޳'nniiO fVT $WӬXS~eJTҤV-vjMhѺ5'._kj틙 Mj ͅH)Y0 /uB{Z !DN O!DP(ֲ%J_wsfw.U28)]fbV*Z/{rdKdL~otM1;;Fi*QhVrK i.#G8GW qgS͈P(X8n_tƟqߟI9reH֡CY5kKssse9A!: Bl܈>hRuMkGu 2:s׮-QoR2T*SzQW/6oΖ /ykhDm">>Le! B;vqAOdp}jMyZ'._&6.KyEFfKNŋ!{6ni5{2ozݛmfؾI @Odܹg0Ç8JEm&LxW"B$BݿW:4! (RIOk}$m4HvѣL8뜿?+wtn֌Uhڟ|ŠOl6ziV@ph HT {z- 1zf|9R_߿s?~ /^hU6B!yyB!NW.\NxT՝qwuB ™fܹ(JF溇ۏaZ6zz.T(ö&&(JT*UB!M ZuB! Fիu]xOaTǏg̙.']:u c[]̀vW.QQ<}(R&NIVscb^\p,tk/GҤI]#B2"O!=aO'l3lu 9~!BJB!B! O!B!B|@ !Z$_ !ByB!tyL?q קqƺ.C!B&ABW_}2{N__333]!B![ O!D`eeB!"O.B!B!$B!B!" O!B!B|@B!C"|萮"Su]BQH'BGTP;wm8]"D.B! ZV!B*K.!D~aggGɒ%u]BQ`ϯʋ0^IENDB`python-telegram-bot-13.11/examples/conversationbot.py000066400000000000000000000127741417656324400230560ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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 Dispatcher 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 ( Updater, CommandHandler, MessageHandler, Filters, ConversationHandler, CallbackContext, ) # Enable logging logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) GENDER, PHOTO, LOCATION, BIO = range(4) def start(update: Update, context: CallbackContext) -> int: """Starts the conversation and asks the user about their gender.""" reply_keyboard = [['Boy', 'Girl', 'Other']] 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 def gender(update: Update, context: CallbackContext) -> 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) 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 def photo(update: Update, context: CallbackContext) -> int: """Stores the photo and asks for a location.""" user = update.message.from_user photo_file = update.message.photo[-1].get_file() photo_file.download('user_photo.jpg') logger.info("Photo of %s: %s", user.first_name, 'user_photo.jpg') update.message.reply_text( 'Gorgeous! Now, send me your location please, or send /skip if you don\'t want to.' ) return LOCATION def skip_photo(update: Update, context: CallbackContext) -> 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) update.message.reply_text( 'I bet you look great! Now, send me your location please, or send /skip.' ) return LOCATION def location(update: Update, context: CallbackContext) -> 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 ) update.message.reply_text( 'Maybe I can visit you sometime! At last, tell me something about yourself.' ) return BIO def skip_location(update: Update, context: CallbackContext) -> 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) update.message.reply_text( 'You seem a bit paranoid! At last, tell me something about yourself.' ) return BIO def bio(update: Update, context: CallbackContext) -> 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) update.message.reply_text('Thank you! I hope we can talk again some day.') return ConversationHandler.END def cancel(update: Update, context: CallbackContext) -> int: """Cancels and ends the conversation.""" user = update.message.from_user logger.info("User %s canceled the conversation.", user.first_name) 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 Updater and pass it your bot's token. updater = Updater("TOKEN") # Get the dispatcher to register handlers dispatcher = updater.dispatcher # 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)], ) dispatcher.add_handler(conv_handler) # Start the Bot updater.start_polling() # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/conversationbot2.png000066400000000000000000002431161417656324400232700ustar00rootroot00000000000000PNG  IHDRIrsBIT|d pHYsgRtEXtSoftwarewww.inkscape.org< IDATxwtTULz Z. & )J(bA:"6TB"H""I B 1R&Z.s!gc0fDDl… \t WWWJ.K ŋRD j֬I*Uy愄DDDD~`HdǼyx׉HF#&G} PX1{)""""b "e&L`ذa|7FŋcJ,KؖBdɪUh׮vNF#kf888dy"""""9BkE$n޼ɠA0Zl2صkDDDDDrBdڵk9z(əh42qJDDDD$)DHX1k}L&ٳgm\HSL;qDlPHRLnejDDDDDrBdZѢEvRlTHQLUV6KC=dÊDDDDDrBdZΝ|hUVx{{۰"ܡ-"Vzu:uꔥf3cƌɁDDDDDrBdĉ)TPF4PU"""""9K!ZDL2\oo{H uƄ r<-"YV~}vMf̀K/.؂l6][la,_SNQ|yVJ۶m֭.QDDDD$EĦMF9r+Ww9"""""6"""""""-""""""AGDQFl26ő/H~]HSlRHNSIDžXJJW.k3y 5QY|7TzB |mm݇}\(IǬHJL^h_""""B#""v*VPFetjlf=DDDD)D^~^|E~i||K]v]JȌ~K @g[2d]?赏ilK˗moK`@w}#_nӺ89;Q6lv?:A!ZD6wݭ  >M/o?o귨ϱT+-e׋a׭u0.^8~ylZ ] ?@YrDDnI(4l63.hMFJ l6gAbI-""""h[|2 ʻ; йog.]Ͼ㝮PZM=/g-\cg0L;uKR|l"""""ii$ZD[}`0ol?ֆe}/W/_eps8kRuWNׇ?ޅqtuelP[DDDDC#""dlv>4hՀPI^yօJ*iڦYkNՙٿϦrʖUdRջ"""""Hxͺ,QRiv~bEe2]CFVJyLֶwEDDDDҧh)23 ٿ}?ͅqrC^T+>Ċy+qeKp|q:/?ii0""""+h)2; }#ټWI_Q?>ՔW{^c0$BV.F\LU>cPh!Xȏ_)ل iR*{x{PYz'?´XjvIE׬^)E@Kso޷:1!g ׋<$Ҵ1 tۅ.}p#7; =2qC&c-""""> n%A5x3}}Fe4)@ےH(DH5k,2< -"""""R`j\,BH-"V.CDDDDM h R h R h R h R h R h rw""w~6vػ !w """"-"yV%شlmw)<==w""""-"yMٱC*UPX1{!"""! 66yd~w{!Tbb"{eРA[.'J.Mҥ]BܹEQxq{"Prr2g.\`ԨQ<#4h%Ws@!ooN,]j2:A6mpqtu֬^ZAZDDDDĆ+ЫO?+[f.GDDDD価-r*ˆ3EDDDDlL!Z>UtiiS)HؖB}NAZDDDDvE iP) EDDDDO!Z=Hhт͛7ۻ$|E!ZI ҾkNAZDDDD$E iQ)Rt!OOi R)EDDDD2G!x F๑#w9Lf37}|XӶm[V\V?"""""bHo8:8еU4e@׮iΝ 5k׿?^wd&͟Se֮ytJӭ[ps H7s&93K}oߞWkDDDDD ШSquvN7DOgmD\bxMF#EyWy/xgp0fomغo̝kuŋt1mͪ?#X} B|#""""")&ZVvrw--\ȜKqtpHiw+L5._I5EhGz'#ѩϮ$zv%K,ϝl慶mb%l9}{l˖xzZϏVũ߻d}DFElT@Wiݺi'L|h/\ ,"_//(WڵK|m:u3/N 8q;F!//mْeʤGdt4/3OP* ӲEDDDD$g(Dg/^χZkVJuN p#Fd9D̊hӓĤ$߸@Nzqq?%xw1weyDہ189:t&|59s8pk{GFL|b"̝KȄ j KQDDDDDlKӹH^4;z4MJKs_9lϵi{hM}l߿~ȗ_[ԩݻֱ&gTcnlF nKĬ%KsU{OO"֯'~v.ruFzoωsVf5jDL}}og܀HH`ukxnY;m7mcϏ?Rpa9,O|*u[/@%}m>;Ç١eK%ݭ]* lܵ+M_wx3%:;**=ȑ4-xz2i0 ?~>j :5i!ӓz.}hٌ``ݔ(R$GD唋W0k< qqv[~}V<[!ZDDDD$brW񉉖+u&RF/^¡S,sj*89sx1v,չV"""""b?.p&FѣJuB?̝:34TfZcX%G[eE1^y)-“OҨF \\c+;*ww>}Q2\x8eX0DDDDDDC!:zWzprtuǁ\h0~{+\¾67nlyJ:ޮQ#>49KdF6롛?08H,UEiۨQiG~f d2ckӺaCK;gZ/wwl҄o\P!7n#5jXߵU++d}>tbYn+"""""g0gw[aҽ{wV\Iĺu.c6)r${I>fg6mȑ#T\ؗDK矜 [V.EDDDDD (hɳNa Sߟn[sI}15ulxxH1yr[DDDDDv ba0ձ#}}qwue/w:>Gƿ3 ԉeg,ndЄ /ZD@ 02eu }.%!1r\ɒ|5d{ X6֭5mji3OM&LW瓘@@~?ZAAVm>77F͈^2O!:Ԥ nn:رx>8_z/7s작65" v@M/`BD **eTKQ???Zey+RjS'CDQ/ZVMNNŋ|<=ۥlf?H?7ٴ{7Q\9jW5u*g.^dΝ\|%J0j]2t(=:t`ϡC|:oJKVhf֒% -lڽ |0M=7`>n[v 9. уӦ~ ^:fΤ=ڷ""""""Btp*,a_}eudXL U>>K3ۺaCapuq3Oww(Wןy7uzrq˜eXq#Jfgb_'Ofȗ_2o Br.0 +Wҭi/_~a@go<vŕ(&kw2a^^iާ3F&dHmɦwΪ+X.SE_Nrrr眝ps:M+\v Gʗ*E_]Bb"q񸻺Z=*^΅q}}[=iL\fr2 b`={u%*'O΃+n /Q|<=WZHy3g[>fg:_dĴi߿?Gr6PDDDDľ4G>~V%gLT,3ݝGj=}|h\=/\ ߵZp IDATNNl*""""""A """""""-""""""A """""""-""""""A """""""-""""""AzN]vG&dsqrL<צ ֬iDDDDDDv3Ll޳-{r5:/\ŋiܻ7&Lٽ2Z?bb(ֲ%NEDDDDD;h;+R̝K…)S8~Wӹys&6mɽ] 2GFre[DDDDD~Geʯ[ڻAk4g'' _m'ORpazuݺa0ڍ6OsY߸Ç[}ay}5:~|Bl\vj3дn4?~fѣx{xOgq0ZO޶Ȩ("' |_|DDDDDDrBt[N|B/kUxqyfM9>ĹsL:Ԫs}㹙C, ZMy+׮Y?U4SgƄ lݷ>ت/kײc~6jDb0=}!!, …%""""";׏X:EBRGSJv7/\|CbhL^{iU`i?8~%=ˉeXCQ??,X@إKjۖ.-Z0l6qtp`yT)[̐y}{5jdu+'N:_߳z6nLDDDDD$7iMtb2Ml\Ĥ$mLF(= tw睞=1L,ۼ9ju NWǎT)[>}X~jERpr:"""""w)!s$L,۴WǎAl=GNNժVק>~lռq-bފiΟnUҥ6MDDDDD(6l6yPA _//b_ϵ:P3(E9XڹVHNSRG9c х RdU[YX*Wgg2t_W{kVѭ[ɯ&:z5U˕پݪm뚕+W%~F5ݝ3/޵]X3;=KE/Zd~7v x"f/YB\|]&L||9<דH$ݼɰ0Nin}1NYz4WҮ~n+O4n|ˡSprtJ6X1:.V,ӵHa0kΦwΪ+Xޥdژ;k111xg6mȑ#T|JkEDDDDDD2H!ZDDDDDD$EDDDDDD2H!ZDDDDDD$EDDDDDD2H!ZDDDDDD$E$?~^ʃ>HHH = "aEa,^8[5PxqU&"""";E$ÆJ4hׯ_R{!88^zFٸB-"ԩS9{,q7L 0,]/""""bo.~c0M'w)"I6mh߾=&L{e9sm6+M|I!ƌCbb"&ޥdIfk)S0p@֬Y~^ȑ#^:E"""""N!*UO?d2DrT2e:t(>-駟5#G,XGG#""""DH1ʕ+3x{n2gk^z%\PDDDDE$K\\\4i=73L_}A!ZDutЁ &pȑt̙3۷3vXm&&""""Bd˔)Spttdiݾ믿nDDDDDlK!ZDt 6kײpBsmN!ZDwޡrʼ$&&pkzDDDDa0f{!"իiӦ [fԬY'OraBLNXr%7o`ҤI鮕ɯHQbcc?w)u֬\%JPH,X`r# ޥHH$%%1n8>3bcc]؉#ݻw/刈)D@bb"O?4+W/&GRJ{뢣Yb!!!TRui=}F!Zz-K B߾}]Yhh( f͚l޼{d]EBBEՈ(Ddۊ+ر#:ub„ .GE1l0Fq]NvEz]d9sիE%))+b4Yl.I~a;F2e]N!qTVH:=5quueҥj%xڝ[$[Μ9äI%Ç~z&L)S]ڊv/w"!l_% ӱcG.\OE(un>}ًQ߾}g޼y.ED1¾ i +VwI""BH/8::jZjժTX ڻ[t.]EDD!Z$ Yt)7HצMvʼn']s ""-[lҥK I9Yl+})Dd-[hԨ+J*uV{"" iQɂ;vPhQ/nR$0 ԨQm۶ٻ(H؇BHܹ5kڻ GjԨ w)"rQ} "ty衇]#_vDD7 ""K!Z$RwXP+|ڡ[DrDj.Y#-"E2dɒ-D򕀀9}KTj.UHrw"Mjg,Xc0TPVZѴiSK)SJ>}\pB>ȑ#1 o\~ʕ++Prt눈`̙ݻ#<+W3uT6,5mu+o<.lۊxM[Z&ly{~.WI,z1E|3|]NMXzÇ3v؜*QDQɤ˗/Sp\ϩZ*SLtn YfVZţ>̙3qqqa˖-ݛ1cư|r&L1޽{c6fbTP`Kݐ2_L_{5^{5>S?>UVcaaa,\nݺ,^f͚駟Ha@… l>?͎u;ىJUⅷ^sm]=]]ۜ<">.>S!:)) I7Uc^UdQIs~N<ڇ9PH-Iqqqf|);ٲe k׶ жrIܨRڵk QfM{xxĞ={0Ls儖-[f͚{F>|sѽ{wܸ~o&ܭs)[Kʴxn\#pso<=9 nlXErX\\nnODDIII(Q"Gqi=ݸ d2Y4lȗ_~ɹs93ةS'ʕ+ϟ?ccFk\ #RNJ̹/aDDDDB#"KѢEs^_XX=zHsitHݽ󩿾}gﻵZ*gɴnݚ{6mЬY;Y 0~{tۥ9nذ]vE߾}mRݤ`xn`̱3t~ҳqFf;ͅ϶/vN$l8W)Q6xwu%Jۖĩb0eօ#.6`p5vgF`4yo{Ty*u".6HREyۏҕ5˝3;ØcEԕ(FG`@,mOzuk oL.t鮿wv*I&m6f3 [5dg)Q6_V #`vW?*iq>}_Hfo ZpvuS HU:vpǍ8~)Nse{Ytxմ\'g'1._l2cpH¿g?T_FmJ6.ȨFat0Ҫk+K$^o:vࡆ7`0u`WU .ѳaObЪk+ؼ|3{ˏ{H"Y;j46}K.ѰuCu+Glt,1Wc(Z2ёziѥNlZO先Hvɉ؈մ圔3|M`` K,IsW^aÆ 6-ZhA-tk׮eڴi3z4ho!!!*U*M>O>ѣG3h lْ:2"&77 Mƅ3K׬;7s<l6ml򻕄. EVtۅO5e\q,{Rj9Ft2 {!e`w!ܠ #V_sk7fL<]x~|o~ՈQS3nj'U2uTx }7ܜdy})>ԕ%XEyίuf䌑ijP5ѧʵkLY=-plؓ5sT޽q7\d? _0q)^SGNeYEDD$=Z-IʽR&n!)&rMNUhQ^x-[FѢEψVڴiCPP3fHwmtx%1|||ruTƍM~3gCkc>|Cvj{h!.@L S(lYy/>uвݬy J[j/OƵ;.[*Dd;r"EBXXX ҬU|2es!CӪU+{N>MjyͽF D~ Is>00.99fԍ7 (t*G'G:Dޝ&dF3`~܉0XJVחRG{Jm$HE\2M9u*«ڴToS=j5e9}v8}~MӜ7MYz@Ƶht3BsNj4AƵгŬ7BÕWҭ jOED`S$///bbbr^...| *d'Oh4Rxq-[RZ5֮]\J(S k׮wޱ[٦j7/|gg%Kg/;=cF\]342y'&xJoM϶$`JpիƱ}YFlʲZA4l0y/_,hHcolZv_2Lm25|6.6媖񎏧O,"""hL*UVA2'꫄ҧO&Nht.]zY},Yq1g<<«yN:Š MEuũ!CK捸x"6i\Ա{~zvCWt?#';+;+ ~RC>!S7gMcr2$1ݧɟa^~eS121Ro&e.@*TBJb / /кs"cxF3c#{nEՋX ?TyTgVZo&IBT.D jʯzUZlYnW&Mzh{sEW(у=z(*U;6illX+%jkǏ_9'''Ǝ[-ZE%WYv-^ߓV\W [x.@ɋ&\^ : "REqoA&m_F}q_~~qBԕS~■<_`?+կ f3<8qlL1/޽w_wP} hɲ˘|0}C},m,q3]uu֥~x}pwcY"&q̘c`T^)3|Zwn-m}VںMO Ϥ;~. D㜾>]t-r٣>VyB֭?d޽EkCdd$-Z`/4FZFZ+$`lGv-4egek.=1m_nK55$%+^w3QxwȀWnri|MMJ%lSW^AO_ZJcDAqq lm}u{K/qUڼFܜ\[=8Hb\bR(QݺMKӤ$טC}&7U,LJb wnԡSMK0g',6:D_ :δ CcbIMJe_;~:U,У!M}-s$&p'в}ˇ& quLpIG6(u_CXpY8/KkW(xG?~еB!($BBjhժ[la„ Gc v+BQouB<^xW_}UVqAm#!7o䫯QFG!BIxB ,ёcp3 ++qƑo(BQYH-~#.._ӧOk;$En⭷b̙3777m$B!ʐ Q|||ذa{xxxPjUmX8|0yyy{iW^/TF !E#G䧟~vBQiH-DgggӼys ~̘1C8""//rKbbbسg*P.^)ZRT?K.qBgZB!*-x&̟?(/_x̙_8pR(˟}O>ᅣ͛QT@fx{{デGSuyj !BT6D !.&&SҺuk|Mnݺ>geeŴi5j-bZT\\\pqqa@~R]0{ࠞݩS'jԨͰB!* BqƑ?PÇɔ)S}"|v0|p֬Yí[LJFA͚5]6#F ((8m-BQaI-Ъp~7FEf͊lP(X`|GašT*c͚5̙3qqqaʕۗUj$ ]!$Z5ټ_<Ceٲe۷" iM8P eĉ888|ruRݼys&MJ"##Cۡ !B<$Bhͼy󈊊b֬YXZZ>̙3f̘1C1̜9puR=~xk:t耙FRȅB!NwvBb9r={V:111|Jʊ/#G&ceT]" ֭[޽[Q٬Y5k&&&xyy6k B !J©V揾BQXl~~~ 0@ᔙ,XX ˙2e {)F|(u """PTlٲE]NjժjJwikT !BTDWcfk-ZbamA-`3~M>ֵlْ>K>(c ޞ6m@ǎYbXx1ÇvB<9<EBN_?FeȐ!:f̘1%L8[,^z //2RԩSm۶kT7۷Iˑ ڳ'U-,ϔεkիk׮DZ>>ХKUͰ(s_\ɊX KلOhQJZg.]b֊u]ooдiS6mDڵNWise^}U>!o4me҂IU)CSWN}7,; Osrr8z:֨Q]0}XYYi9r!}#Og_ E|"DD|9J%]ŋ$H-iܹ9r SShިZA͚OwrZ;I3gΰi&}'ET2rg$:R=u'6CBQA=XN+;;(uR]PNKPРAHuǎ177v9v5&3/sҥbȓ'9pTK\^Nݞ=2t(SG,{_6mbয়'ڷlp]#1%bFuwg /]Bc_ڿӧSnR_G4I=Q(4k Yr:Td+al@Ĉ!AW,#-K}CPx]>oN!3AWWW#NMMe߾}zɒ%zGre2rK{B~9JAnn.)vϲy7o_~}ڹKfPΞKmڔV:.{kk J݇R)l"ߴ>ffyˡ݇7d0n\A*vALHM`T(F֊^7}Fa҂IfB<3,IuA9-###???)%*&цg(#6 ^n^c\۲Ea TT$dIRP321µ+QQdgekln\ڻwaq꣫5;n.g5i~w6A? ~^B<4ꘘكJ"44Tirc(\iEVv65hZ>A5 8z4/]"&>==׬׹FѼ:iz͛}:::|;v,~VoOIٳQޞv-Z0\.\ƫ&qmnaciIzbԨB߃|"n߹r asb򍼼<~~ ڭ[ԯQ s03-P"F1?eCӟ~ٳ' O~ZOL$#3 SSZ2i`yn8Zi⌉ |%C l,-yvmB'1%sSӇ"I݇) joTu~c~hr[Bׄ]1 ɄB-5ƆL72Xh!E+TN+<<6oެNmllWT{xx<[! 9xXNצY3l,-ą 8矅֨Z[XAU ߺ{d վ榦E$Ob̙9Z)^eK#620ʊu`nj ݿǎqߩu_[Be*7XR(|üIݺڱ#;ĉC~Uq] wL WW6ogKDk tʙK\.dg32ȓ'ygOtuumݻ s~*&&{Fk777g/*f!\\\8p PrZ;wz [T@NJMe'ޠW-]j:ŋlBڹa/vh=x0+6nH`/\q߾۱c[Ǡ ڵhY4F聇66l7Oۙ_67?1fL>1ri|}Y7*$`(&Λǀ]5Icd`@Uꝭܼ{~܌U{A{|Vɉ<~)6ofEv/4Icٳλ@03c3Wp̿yBFLΡSJt4iivvӋ%2GÏ>MC\}6lZm"wE2^i%,|<C.Z-{7ބ jҝ$~k֦fBE* )Vvo!tWݻFhXwh|p!^}dVSΎsW<xLCBnBSL' c_u+yyyL4v=7gt><r۶dpB T%&ӱs8w zV!ڼరb߯/nތRMfl ׸'LfgߞÇt=۶cV{xеMs!*rIǍ7ƾ}.XcHS3 ~ ?JЏAnȅ/V~q}bucԊUA!Dq9{P9+Wh&:te`Dfߨcݻ$0_iӬOfڒ%6}z7+H ?V|<.^$.1Բ{3%pEtJ֪X{:]#!9Y=]Ic?}>CyZLJ%6ttL;IԹsjZTj8B!`llwJJ Ok}}}ї2Fϝqqlٻ>>pu?O?D%^4 ֍nꝡG7]VlHɓ :ȟ=zB}C223L'RԺhzbMz"DeW `Ro_mhùcB_k9bic5vx{PϭF,F%lPԤT.D5 :TԭBT#uwV'ճfb֬Yn۬Y2 Y< FwFFޜʊ}/XI߽ˮCuwgk356.6fgR_q vb@׮m{I),Pq #6!:t;߯}d QU蟢gGMp=B ;+ӇO&_-3`b[bd˯eq4jш̌L,Zwt_>^S}B!DIҧO-Zą ~:k֬9sI&Ѽys֭fСCi;t0R6puU?`zD#FttӬݾ]4QI=]]<_xm&4$"OЩS޽03c̙w.ݸ$87BlBmjU*Y};N ;'xZcjl̏AA?~؄v:ĕ75 {kk>]O~sWӧksrs ޳kssTAZ[ܜW;vTוNMOЩS;=]]wRڏPmq.GGsu+woWbO|_!* =mlf̴_ޑ~C'U|<N^Y\= 670sL=W}Ĉ~M!Oc۷o'88{{{ڴiC@@:uF [}== :#FhW_ek ~>XL 8ۂI:i-׶lywy k}ĻgمT `<+eu075%pdޚ6Vo>ZNjYe|z!_.Y—KhT(h߲%.]KKmlhעvJ}q8L^ 7aZw̌e7V_OZFJ0.DeQXh-Ʀxv8Kxظb#N\RB!DٻVnn.N"""JŶm }83S<OBmgg<xuȑԭ^]c7ˋjvvmVo-yq~6sE2JVǃX};^Ғ-[>ٛ/sgΝ#9eK>Pk}t>teKDQ(XcmnNƍ;ٸ1[""~.tiݺPsjZDDp)rrsqZ\kV''JbJ ));w-L[MӴ).zߊݱ'N`afF??֯_(iF1wotK+5V֭,۰䴴D/ry%F&F$'$3sLzE~3 yB!B+ >x ѣszzׯQEDѡT11N;*&& {C}}zл1W.NNtuGw?bWڹ 6`{hst{}>Gv$e׹zt8nVCdΪUx4lunnݿsD|'#OvZ.Z>F&Fy7E>-G*BiQd9-ooo133vBTX!TbV̎7P_䴴r ~&6!D yh;'dj?q7HK GiB!$MVi xԭj~ bD^4U SccRS9t?/Te4U g;;8^꽅$%tŵ !Ϲiݾ}]vRrZB1cf͌B͙;nz#L $+;[}ҒߦO[LBQz666;GGGJb֭T*T'K>Oq8z 7OL '[[שXVjո};GrmZX) Ő$ZݸtĸDk;g; or!BrZ;vP'vvv@x("wQ/O榦tj BT"Ş765F}{(cΫ_wיNbDB!*ifRQN^|EBQ""bDl(RW?wuY0ydn]Ezj:ܜ\m$?ѣ:((_~ENi !xU$:;;f1FF@@&$'$`-#RR*9tT*VXQN`SNTRE !#h; !B<###|||aĉo>H?y jӦ _!s$я\ֵ[YQ۵6 KO"+3 k{B2u5};;+SNs-s+sj6MOFʹ+Ze?CTԻ)U- $.&E^oljIԤTn]ER|dgecicI16"211)Vxx8rZƴnݚDOOO !xb:{6!$:*ȖN_/j+Bׄo56SۍaK5޹}}&rh!ڮ\\8e385, D|}_,cPq,^"m{?V~$'#OcOBg߃n޼IXX*ߦjJi !x$ΊNڜ*VUz*K\ʆepvǐ궍[5F{_S۵vŮSG}%cKF~[x#O~ʡ݇0~|ٻu/+f>ˁ_4|Zh!%.t҅jgBҩTII<<1Y#.K# q`jnʡ]4GEDǓzg87^8b ^̑#Nb8s {W'~9qxR(t nMZ7y* !VN+88XFu(urB!*rI ƭ~V{( n/iBǮ_*H3IMN ''дbRA6͈AR|UEGrzh%kߚ7r:flcοksЩR0 ?-ryVvv6QQQꤺBA;bnn?cKX)9m DW_Q6?7*9z&YYo@nN5Ő(qh!ھȟmlf D8띳5d\VA999>QB!ӠT?XNkɒ%*メaRfM oM+h88>>=q9:pܹSoEq'''6,.4kK*fw^Quё;p ?BNvCCgef7IOMǾ=;8vmR-9r?}.y_uh߫=:::.p-ڷ(:3ttIK,oSsSn?8/sNoӮFg{\ !%effTİgT*۶m+rZP!*.Que˯[# _*P*hաngŬDlu'-%;@|L|? 8A^6u!=50{luuu@WfB WaaxvĵkP(4hHHL^cMGokD/,Zۍb#ۏdTQ?I,j9}hcScO[L\L P(HIL)F!(Kvvvi͛I 퍫k?_BT"60ҬDmJ6wf/ذly"hN:IQ( ?zndfdxvIx{+Ǻu#= h2J]%?xwU_>>Lbr,L}ʼnί+ ּms6o;g;>]iSVV0cfՅWաk3 3p!Y₋ 4iT"iuܙիk3l!eLg߾}y^^^X=Nuv<*-%1bam9z?WXTV0b=1.kQձjcA~ٰX⓰u̢YY\\9Bxx8l۶M]`ooooڷo#BGT`| ;o鏷7Vqj=>(͆eհїK猑3^Ϝ躕_U[ +Q{!Q(xxx{fbccd̙rJ|M]6#F ((m.BbDlP)SP(5 L;+[+&0QK>݄Zs-fԨc~b\=K/C GXˑ P8cRU!+U0R|"iy{{cdT7!DEWwE(<=:ûch螞Kclj̒%TX4F[5h ߛ+^#zQ۵6N.2}M!rZ)))?in+崄d*|}_,Q*Sw4CFCU vB!3ԴrZRNK!SI z*[Wo-4]d$9!SsSm"B rZфRغum.τJD{u,r)]u)6@nN.zO R4$'UbekW'/-,m,5;I|k')4lސwzfmi۵~K\Bnn.o?NuzX’iK8q,̰ueoqvoמ( fqq~;gβx"}2f28玝uXX󯝌0]4n^cbYEDiL~)Wofh$&Т] :zJҝ$,- W(q#GoNRϭV敛o'|S8kDc9v̬b۞>|(<;xҬMT+ٸ[_qin+y&-ڵGp,5U}>gD(thݹ5v$%``h BgރN:EDD*P9-ooo|||xqvvrBQ>*o=`嬕pmEկsssy-9L|L= _~ը.ciY3T"tt:e(^fo[ZBA݋4Tahlǚ4߃#GHIL)X,8w99O~59u$99lcSߣ}-:::\=U4tt:0Ù#g4|z !NTKiOXXF+۷/6664oޜI&qFBr޵~\zhg2U/,.<ֵ=ОOz*N0638^I]"wF}9MK,~=pkFЏAZ w_wiƋoIrςvŧ046Ȁ;h߿~@;//ЬM3mNW;t`ƍDDDh; !SJTTSLcǎG<|||8q"۷O=R={"iiM3m IDATm/R.ѯȝmīI$J]%>Ƨ?e󯛟-=5PIkFZFŢǢ? ޭ{ =e3pBj+: 1M[}W†ew"o~&ɟ?I5hձF{+[+8"rg$!aV}]z۱c}B?y\dgf?D/>])M} {j9zc{ߧ˲JG7BT8ULgfsNqxYN+<<۷j[[[IúBrUitہӗ7* +fؾcoZ_cg5[~!Sf&V̟4k1{Cw/k\p˪XX˚ks;vܜr[yy&rPfZƦԨ_xwhjԫAx6ϳƹ;vӜ:KF]+// /Zeges7:#Uk]T[jut'x,m,10,l!-w:!,^]Ǐm۶HWաrZ@.]V6B<*|]0)? 1A@^/?֢4i/GBAnٻﰨÿ  WXQcĒhU+Fc/kl`;łRg?`cbsY3gZ2*שlyxrwpB3j(cn]1vU\ttӍ~L;=ֹѮ_; 91^c޽yti>sH$jk=zs J1)bB"10\rӆr)_0f^^jc7ؔc{A2ǵAΟODZcE"JTmңZ1KݢE LM?OkRAF&훰 `y}؅cJj9;kN=ATdUTE&zxxy`Q"bO& }Kh lsXTaή9ercWyL?REn <4]֫J}moݻ5vUsȷ[SQ yorV<-m]m-{Xbvè1CC *gˣ"a||3nf ~+=GJ*DE1#̩Z*UVu ֲ~}H'-y2dʕHRjԨAÆ qttŅB>oMA/ׯϬh٣e~#_ҭj7ֹѺwkOj/'+w3sLܹCJ2iӦgPP,--5kg׎=W~A>z<:(9cRT)HҶӺ~:2L!} |2IIh/GUV644^z &hA@5q[ƚkX9uy]d|>D' DZHZ;-???Ξ=ˁ4jذ!+N dL$т mm߶<{DE ՒbQ6_>hAH̰֖>}|}}5ruudɒ _ D B&,[e? _"H Y^;-???9V133Ao  eghִ)P?~<^^^solmmY~=ݺu;;; Ν;A"A!i!7hkk+j޾}Ǐp¬^nݺaffFZ0awd2-Zċ/;A&܂ $)YѨF|r+0شiV=z/_ơbE:9Y$Id2sqϵjY>,Ø1ʥֹ6100P./^T3gSfMƍ㍌dĉ,Y_rHA2!+:\R{g3F{{Q,pONVKEy\ْ%ygsRfF̙8,=!FFF*IuvZ>>>N_5j B… w^ڷoOf8)jBI B&r891_?sE ·^2T/W󍷩óS~3`/ʠTbڱ4l1c2(iiIEʖ,/]Y~ dEvZ/_ٳr!e;"EдiS6l#ySI+g'mmwժE2eԮ͛Ԯ\B*cnb> YH߾ĕ+H371|^A(i9880j(yrz\PQI6jՊ[ҽ{wZlɱcDkAS!,ܼ rÃ{ÇR.TIt)gח*OY;wovŋ+?|ƃ 屲%KB*.\.m T{\c"% ~9F]3HHLTk^6,@6eV rҍ}t4LƮ ؑU&;u*?;6Q=cV瘳am`o_MʯUwʕacٶq Cta*7$^}K#ʆiӔO\BA6d Pm|=o)L<ܻ7TvZZ111;wN9Sn:V\ʤaÆg}"N:m6wǎ4_ |D-?g…Tc8)CLl,aԨPAzkk.n@ %?~^~Aym5{$]CӦ*㜸rEUD~w :u8u.m4g֜뙶j3}ʗ*ELl,nJyOOlw?f;s7lSfԯVMyݝG4s&%--8}:͙bbӬo_8:ɉLC+zM3tqrbD $|R/UFFHR>a:]}ĉ\unUIߟ͇1}*6͘ɫW9u*ivŋcW8R 5U+b dJRkN:/~~~YnեKd2z___ .%"Ae+WƬP!f] @^\f|˖(T*Y˕ȈW?y r\d+|;3!ՌPnd;QQtuQT.e˗*O] kTҢ],ݹO)]tixeߺ<$8Lƭ@Itڵi^vb 2W`d`͛U~HiӦ G(^ |mD-Lٻ`N%Kk ܥ SF_OO1/a?'&6HofITJ58Gxdr/"n*_[k=Y; yf9'[3ˊdwdthJ!=$<@9o߿'>!H==kIǫ/w9.PO͂ bŊNx{{ɽ7n...Lƀpuuȑ#)((f"Aݵہ\sݰ11,Ok`ƍե|IJx>kԬڪH嬮NJk[PZChE12_ԎG|^͈bZZ-J*Y\5kfל9juut4"Xw/>0e@Vˢ-[֭rXO#BKNK&q=Ξ=/GUiհaCzͦMhժ dHA򈶖c_<:vƏ?aNL:`f*VȿTyNgp y wfx UZE;GϟS# -QH.l8h`w?eV/w?eCBm37'!1Qm9vj )jjǏ164Ԗw߳x6T۠AY?CtI7HN/A>?TJʕ\2$!!/rq?Ύ;C"GժUw P+((={f |+$ tu>Kx*A\~fϞߡ9KdTJ_Ȩ(d2eKLMRZcbdĉ+WTKS~hтiVr% WǪHVVԮ\~~:{V)rKf8q mc/pϜzr9z-)͛iߤJEsMhǎ1vB7FL'/[իX5i:vvi)np͛kF"ХE ܉ۊL4(ݪ o}t4nзm[fYÜбruX|.\\| .qttk׮ٳ;vp-r9AAA<e*#ɘ{Ʃk8?4QC5}^~/4Ӷ-7m>wD>~3)J`\Ȍիsk@KKk)3bbXe uTQsե Sk++UJj hc\P@W+V0ݝ]$&%acm͢q>5_PkKbڵ;u SjUTrT*e9,޶w6UJlɒXl?1۝U֜YgÁ$$&RL""Ths.~(vBK*[F71/\#U_vl;r74&]˨s~(dk7AȚ7op!9r߿GKK {{{LBvpаIaݺu[~ɓ'YiiiQV-n;v,գugA…2pry _mmmݿ2M>>>M6mr} ҒYfg"\i=iZ>G/^PCtOĔZLl,GQYQSKؓ&2HǏy'8!1L/ΎĞ\.)%ĨM:/ߏgϲf|86m"H󇮎{0᫥JD9֭[iҤ 6666 |\\\bY IDATƍ*I؆$_!E"P"cӦM  ߀SN\]];AwRΜ9Û7ox>$BDZAܶ{ni޼y~"_ؿH+&iAAr]v` SSS<<&hAʉDZAk|E\1YgOf4>>qt=GΟ%K#U512+&&?aceE5זxZ*)fTݔh]1- Add$VVVy|k׮1i$T=~^|D|]J͚5qvvݻw\pK.8?سgin߾͎;>|7n;Qk׈S&щߟ7oR~}8z(cƌ!!!~Ae sss"uZZZL cnnt֍*T@n#/_رc*I۷{./ӧIJJvhAG߾, g;v 8Sݺ9q/rkSݺH$?ϵŞhA!=޽P&yL2[N?ǏY~Ǵhт}1tPJٲl2fҥK'$$I&k.T2dC _~ԩSxzzfX9rLl4io޼aΜ9tYy<7V~/BCKH%k<xݼCŊ24Tݻ24 9~22 uzߟd2[XиfM4ܹ)+w&2*" cceE-x)dhHǦM)J7o1j@^Ha[8jԠ@:ſpߟnؐ" g8 dV…ѽ;׭KR/χF{9){%I{A3_#Fp1.]2Kvڅ,--pvvfƍjI[VdZl8/^Ӵo^%re֭[g޼y* 4ԩS 6Lkԩ6m֭[ʙ權ek󂞞]'ѯ߾Ո\_CŊ\ٴI˗-320`9Lޓ'[ˏl>]܌իq[-ZR)S bҀ9z_jUK*ejb^ի*l9dZ{ڏߍcgYO<%2* U}geTBUp}* 9O//1X=i(*'q}XǔLKƏx%r׮ӤI< ɛS(nEoAH (ϑ\2͛7WhZ}}}jԨCp&YuƦM>9}r95s'..K.QN/_R@J*r|@ &׮]#::ƍ8I4,Zĵzj9m`wFq >ű ~_ӧjeRА6zLF4wv2i2׮ѣIHLdڵL^K%#>|i^G߾tnޜB<4W{L''G>l:x1r\-^rȹsq1sgn>|nn4i{,dhիgÁlCq-^}Çi#F`cmͫ:{6[bL*Q@H=sk֐ѥKqXQyj2_l|<[`_Waa} MSzUD ӧhRZf4$Jw(:;EBS)[YQpatutxr);>>::6Bi)W; |ٴ4㏬޻WTgd>T*uutpXQD߼y4i?=GzR͞M e4quYTc]uhZHATX2h"4h+ӬvLkqjD-#F`Vi눎cLMM1cÇ$Z)][|NuY1u`ernm*mh_L-iLF 24d\iP(>|tuvx$$&zU)K~R/ٙD"!:!Cޭy/t2s8$'7mYHZ_ۏq=47ajU:8w/oIp0rB031QYVJ};y%a^ߩY3 ̰ }R$d99qY]b /^qbׯsrlH%NLLT;8߼=/(~7 ,}' ܼ97vt0'O2W/5׭#kVOD^d 9R9to߿XP7UKr;0Petn)Snӧ^Xtʾ{ie\NIKlEi++>%1)mU=y'*AB[)Tdq1H^A&ۧǹnL]kcmͯ:ei|A<Hܤ˰aØ8q"GQ;_&3ǏiѢE> 9ŅGRKZ֯9u*[fv#F0`tGWggn>xԮ\͚\`&e;)wtr).]h^??VTlq!2R- yPZ֯Oe[[cc9w&VEAAdʊ/_s$ӧO%۷o#JҢ\r.f״-SW1c0uTw?*T7oSEϮ!C͐!C0aNNNpmBCCڵkҥ ׯgƌXXXPlY֮]իWiڴ)V)+R!y2 @;!!Xn߾ $/fE_ڵ+'Of͚|˗/cjj2L9^Z &ۯ53^|w}5޼Id/Macc:;3u "\6ÃWte!T(]Zck667X@׬Ɍa4bcm͛l{OH$ʔ(Af͒+e3H$l9իlNV $WnQk )_vŋSD *P0_Rn LYW#Jܼ9+&NTs޻ښ/_4P==r%[m(aaAb4Y60yr?sҢJ28erɫ  ܜ K-H2Sq]Fg:vL:wm۶Q+e;fzcwؑիWCs{&<>>߿?XMLL6msrq>| M4Q~ejZp}^|I4mk!y߲>|ΎVZ)k+$%%l]6υCpp0 J*4h@e?3э7N|nS-[d˖-Ltj #HɤLA*fpT*ršzonIO)eR"3$Iߧ|TYA{PT)޽ٟz%ҒH$dV(Wʕi gtҔ.]Z9sssZie5}I|`` ΝjIthݺuՔ<+TP *h9~2ᑑkEǏX)ߡBݿЯ_}c_\.Izrr%K˕w1.XP-:}O//lۦ669Nؿ?CC)_o޽s|Y8}~~@WG:d?~wOORmPZ}aUq l=|W6L9VٳIHLH{QYOHH- )71٩%lƨsђJ)_4R naWDnNWɚ{E-B(PN:m6^~MѢE;$A7ΝѣG KJ(}sx€JGWGGUMA,w6:].?u*NhS?7APw=G˖yR2ڭr3=SWk?v!;K,XP͞Mw0f |}w$RU?j^^3Ւ׮ڵ@ZNupt 3ߘoʯh/ߴIw{ 9kk6ϜI8O5X[wOmߞA?J7\+ԪU/dhszx>MX شi֭c ˗Spa rݿCVLȊZ '`̘tiw=JHL$I&C& _OPZ $E\|<=Qz=O]\el|RJ\|<_P9rnd2ֹ)h}==׮2D 058UC.sɓl!PbEZn֭[ p!_\vs1|pT}SItv))3#&6ECLƋPbd.s QQ}a!EfK3|$ِ]y Uvê>~<_{DYXvvsSvE|v>ZZԫZ5ן[K*NHݙx1g!&&K37. ܈c„ F𽈉?‚QFI4P@WWSNn>5jYfXR옷q#NNhÆ 5q"QQ*zEXur9 ~=|Æaа!Ӈ n83f`׾=.]b.صo]OJΣG8I&X:;Sct B^H$czbhV~wlڶe5w>`ayy¼n4fĕ+8 nݺnf͘<ј'5G#qoQC=ć]Arnnn8qkw8d2&MDPPk׮L$HI`ܠZ5˗%۷a<{Ю]7ѽz}(U{z%--6d-gt;VU"1n?zyL,OxF9s:úuct^< *qZٲ8T|Κ*Pix޻=߯a7.ݹX!>DBcoO?PԔӧ('&%Zp #"|I_С\{ڴa=b h,Yyϵӻ7!qv#zgW(~>EFAr˄ pvvݝ)7O[°aطocƌIB̚5/Ι5k:߼{ǜ?GJjJ̚5{q@!BfR9a!ưr69Vlj)ԝC'RN3FL(%Aϭwuz_}?RFFdffgD6r87~kݰ!!Ǐ81Q<]ȶbO =b…|Ԣ ®O,ZĶkOOq?Ѭ1yˉ'pն3N1?^! #gKzʔ.M֭!#~#gs[0dna mXzݚ5PXׯ0.(6>B LqK:u=_bEtu_/_ʗ)CpS/ŋ86k>U!… ,]'NeLMMgۮnȑ#ghii1m4Mk[ ?A4>/2q[5]KKq!$]B4@[{{v9?'Oҥuk'NC&c߳jX[Zr-"B^ :zRv.-P‚ގc6-!Ǐ.{"~֪qOxPMRi߬Y^fMN;Gtl ]opgg?9>>h0yk\$%aG:P_k[׏T,Wmmm޹Ó$tH{Mt&:7e̙ x} ]ZD򫸙glٿST 19wؾ'L`ox@cR7%19us'/ f~9=F`miɭ(_˛6HIM*-Kff&Wn&95w}N{{ˆ_"x?___.]Ç\2PV\spp V*D%!:Rd؂Z@ѳ)/ΎRFF U;s ҭZdtvRFF=Rm sug|տ?6m__5myy|T<3gO՟cZڱnmp;2J%MllݡڴɵWYOW=00}'$@c<|UrϏ8z,ʖOsEzӇfux /$Ctuu~}wYkƆ=zݓ֭[Q(ٳmmmqqqGzyOYCoܽG&kR;mps=]]Z5h@%$Pti2BٲꢧaE2&:'@_&&y9DEEr劬F!ǪYptuyxOFrl;G|]MPZ|Uge8MJoݾn͚,By"_ʗ)Ç{~[s쬭Y2iRYouLʚ?}::ohd bccZ*3"șkOYasEz䔔\dLV;ekڔ 'O/4mmm>|a见>-4 ҥ^;/5VykgoOإK\uKua\NAڔ~ɼB! S:uصp!3q"Xi W pHLJ2eؽpKG?! ( iڴ) .ޞ@n޼h!^[8={Xgj^o !)`2prp`8ǮÇ zUJ Kּfŋz.\Pkcmis۶l޷iKpm|mcX/O]7keW {8qh4:{@z"_rrL\gdͮ]l;xP=*U^ϳ¥W/6YAHڞSp~gooV7kf6ο6G̱fX={6_̙Äy+_ D%3zζ; ڌ:ng|zޞƏiܰ>ѽzV B*R7as'O{#! C/GGaƲ;GȢE ggg\\\С+ [gDWWV MY5s&3Ǝ埓'yH%33Zԫڃ0xM̨=8s 5*W+qLJG z e࠶v:h>l8ϡ};v?d\15TUÆٶqyUG9sd>MnAlߞ]J hyByo''ě`bl}oP $&&RN<==1be![pZURٜޢAQ[[-[ҹe|]SWGlM`d`@\V-,֖X[ZU*m!B"##Yf ބchh: oUB!mT* APi&ҰӓQFQYoh!B!{燗ׯ_Ą3tP:vxIBEbr\xq 2aV#=ڵtBIMMe׮]a222gҤI 4R9lW*(>$D !DSSU=oJ0@=}RJv0Di(۱W2u*}K5B?/_fٲeX(ʖ-ȑ#/_BSs璞R=XMRzYxKopܱE bcji/t`Phsa;KJj*[Ga{O 333{\PX[o6VAc͘ٳծNff&jʼn.n@ʕ[$%:s&ςr_|}cz9:rחB2e3q"Wn Jӗ/z<G//ǍCWG޽ G&dU-sΥQFr-<==%@ (=Ѣ|GϞZDMmm ڙ=nu >v +|S_Q`d`@3;;i;(edķCzr{7Q=zÃKlab_u9Ú]|P>%59>>4]ճfCzƶO[|v/t`cu2w8Z|h!xmMLL UTaĉaU Bx!bK6UOǎیу81Q'O(_ 䄥k=ߟ(bbdfF틉q\Lh^#wsgVcc[&_@ڤ2s27kFMu0OfʨQO ؾӗ/jNp]< t.Rdҥ$&'p [`E~O)P_upŸx'ރhkgm{Y[c°Ӡ֟,vjW/_BP V"!!KKK;v,_\%x;O'/\:::=q};K>{҂^ccʚA7l`75VUk{6`inGϞeΝq`RUP߾?׬9%%+WrkKW~ƈ([G (edsX{71lѴ(_ۑlܻ_|}kl)alhH 9~f4SKvU;wmcUQټoY}c::CtJZs||T^PW)#C;wx =[- c~~XUvݞ=;4i__mnDG[s33<_oN'&)D仧//2'dOOڜ训[Ф K֯'׭]ÇA~ݝ&g' 93fsU_(BJ%!!!~z[.9 *xޅ Wm/%&&r)&MĀ4](Jzg϶0.}}b[RtiabQQPe|՜22zsD2}R V-TPU}c1)]:[l?v?GΜ!ḅIzK eR \>@ԩQCGKKͿˌ޵;v`73}Xs (_y\ENxy1iVڅ>5*W]P\u !ĻիW144 :c ƥ+9 % 3&MPNM$JO1)ii Gvd$>Z%:66۶1E-%5Gsu5[{nZ&&'$Xø8 YT_P϶Ǐgd%ɘٳٶ$M"ŋYb˗/'::sss>S\\\{(ӱcGm' Y MZ2ޮU/wPPxjNB!x%''n:>#ܹ֭siذ!ܾ}E%G:kt ) < B!D8<XZZү_?Ù8q"/_&((}2E |Сi+ B!JJJR:2w\7nL`` nӓZjiLib=%H|8C M6.R*8GVbd,[̑3lPluN^`b Z63D(Ϸ_޸9;g-Z9''&s)R"J7c&o);NTKBxyƍcootҚ.[J;mwi#=}=F02iL;>>[WFB>K5kc͚7qK!sdȜBpV\ɢEueʔaĈ3FiFh?+ntyۑqCa} 2 :~;WXXN6.dCTՎ=K7xǪc?=v>ݒRO_;otܕcs [VlMz#!ؘp7q;!s*T@B‘ʮ]gÆ ddd`ooɓעGvaꐩL0T[=}䑸up# < ֏):L146dvP5E::OW|I1xx!*Wx(lٚB7fκ9unԩS1cOBx M6^z4i҄@nܸbBP +pJU+OKͺ58{l===:qsQʴKg,}Zrs) _CBB7FO_O+UVՕ(ݹ~:AAA]]X889E|5M{ ]v`޷^DJڱ}^{Qj%LLe8$Zjhkko>&33}QBYjXs(*BQxP(4jԈMC.] ͛xzzRZ5M){/굫Ӯ{;4{_wz퍏Z;v2gǮc?Ah[-_>s9 :6h MY6sYU2n c`s_~Kc]ܾr;(IBIe:WRWWWΝ;ұc"[MY&MpmٱrGϛv6Mjr*ǂaTʈ2dk7f4mה˧/sxa$wL_{—T[-L2;]}]'XT|%GY߿uyô)x!3uT233:uDeݺu矜={qqqCKo-N?ePoOnҶmM6yc9۵7]{z܇1}Kky&U3)W\Mh!uo?5]%RdϞ= \]]ӓu~K[$x *W`)|+l=@j۵SѿQ\d">KW!=B!4JO_@OIY>9GOgҍ|6< .cڵ,Y'OON:t(zBGGG% !(%?z4h+x\Y˹x"VDœ4pޫZOJbkGy?U[jtߙ+3bj_aծwf9)~XLJ`fn<3sC` B yʕ-[׳ƺ56?h6υrbTʈms,ɩ|!F="00 p pvvŅ:tĊBKBBDh ^pdJ)6@@+g6fݚ8vdך] Β0aEREu 233M՚U>Bg:tIJJOOOFI 4]xò,2ߌp5Mx|I&$D !d-Vڴt_;91Ǐǟ9ILH̶׿|oei.T{ZחtϞ]r}|}}ի䄋 ;vtyBڶmĉ~Ko'|IB0tplt+{Ys뵨ǬUgGTݸqӇ}w=P똨ʛ/ Q(JBBBP(lڴ4lmmdјoMvaΜ9.CIB}pGsoO*A;%%)9>xc؆.3زb }_97.@P_owׯ_ԔÇ₽BQ ӚBajf t^Oٮ]vF3'^,n$&2QSG0'*ר/V-9( IDATɅ.вS||!J C~QEDD^^^BHOB.=ٴl짭s|Y>k9?3GΐճWD,-U\ph!N^miھ)z\ ӗY*՜4aec>V7bfd8 sVc/GN^ܯrU r sL:})?2!J;wrJ-Zĭ[(S #F`ر4lP !(KA!a146o~'>6>_TU{SŪ 7׏KaάG|q̕WX>k9{-.2w ͒iKpY˱knGZvn鲄(ၥ%#<<'re۷h!Lz80P|6mjˠoѡwP?B'lݺB={V:==ljՊ5P%Oq5Jz*!fֹmW.)pprtB[hh(~~~KժUqww ++<Ͻu&Z_+D ?.(>m&AZ$D !Dg͚5xyyCqqqgϞM 38y?fǎ.]Ze 2'Z!%Bhh(TRWWW6mׯ_Wu.HBLҥLe\p-[ƃ`ĉ=kkkM'РtWBBDkҤ ΝtB3ٺuYKK GGG\\\ѣzzz.q\{zGz&QF ~tu-4SDE8m[MX{wFK Nqb"9eJfd,ݴ-Zf! BRΝooobbbR 'NՕ5kjb-e=5j`bl^rj*YO"ѷ'.!"OnW;ϵw}ccJ?ٳgzBL7OBB!rcV^BGGBϞ=Օd$7m2DOƃGڦFe|ٿڱ >H=[=k1qqԲ,wpիW ɿ~B!PB`ժU$$$`ii;cǎz.O"Q%[7sLM(]ta#FIB!qqq]ŋs)޽;C k׮hD!X-BòzHLLӓÇSR%MNx&s=~L%33TaŴihkk}[ۑ%$PܜvM1lȟk 5=&!1_ZxZz:Y_<~֍1i[Xڳz5O&19 hݰ!ܨS.t`016F8980--l׾O G6m:zty͟{T݇ þnv 9{*Z@5 :6o0ggUkGCБ#dffҹeK< Q"IB!1[ p pvvŅ:Hěqeڌd֭K++ccA OEʕ8Ρ0:^B@O]+W'IIX-ZW*tv:mZT([7Wp0X-}0i)iiԩQ*q'*Y˗ɬ/PkM9SSľP6٫Wo^s>'OhӨѱc_h(/Ga.q$%C- S-becgmM)##.]bǡCԭYkkn!#"؊-5ҖkUJZKXUjFQ"1"s{r钼s>s|Vƍu'$и.޸A5HKO] [k<=G!ī'IBQjY~=TZ@ C͛OJbݠK|ɩ2᜽zѴNdo=3G[aF̑#Q(r&0rLϝ/6ޓ'ckm͆3i\XTt4'\R%‚QW/[ǴC037{6?fۜ9۸1o͛Yy3ti{vGn"Wƺ ^TruթՂ\qE_~INش?cݢE %xB!Dq=\2[&44ŋ0a$ЯϓRRru9o<*澾h4)?p$͞Kn݊}vtB-OO:lɮ#GOJU{׃$.X[Z [[Cͨ(my\b"i:GXP'`bl̦s7['+y??ݣ\9*+owwm uTa}\B>'Z!(d2zCBBؼy3iii0{l>C x!]hf:G:uhZ~z Ç(%&06VN:u$[ԖWV~6&.Z{X1WyU;~ŋpC>ݯzd$ʴ4zy-kRkv + ǏINMŽcGc>DZt%IBQHܹs+V`_lmmׯ J*r__wOdJH4a̙w!ҥ]Ãⶶx':!)/Ҫ~}LY]Vs&&&&8?M`s#!9BGh<~Cb: Fbr2NZ; 6>2%JPS︝A Hh! Rؿ?!!!lڴt|||4izZ-zTtqaW_i֮eLz._aXZ2O*P(_cbp.^CO`ld5ŬXtsԃ/S&_v|:;:&F}b8:* g$ 9 ! CƎ!o۷oD hݺ5aaaߟӧOsI$.g!xQ؆{Qo1r$:w&ߟϭf>>h4mot}}֫VwͭUl<\]fsm_y;Pr<_ۛt:Һ%KRёCOi"IBP*Һukܘ8q",\(.\HioeZO:'%ݢET)_^nْ%v L2V!j5;t>4=Pt5ǏRO_^3:[dI/^̼5k? ޽\mnfFw-]Z&.1Oh4lF~Jţ8iHJIі[7LML/a;f.RJm]B=/6w +h44vK,!ěGs !+Wd/^̃ppp` 6jO{D0dtoق9ŬyJq͚WG:O׳xVJ9:Rif6׶'Ucٶmlb޽| zi5dff)$PeZ9einY4z4g`̙Z[|={2k̘\ 'ٳL7KIJIAF6mxO9sjNو31c?O=VÃeS0`T  f|̙:\}E7SgO,Qk4*A?IBPjj*[n%$${P(hѢt 3PڪeJX'$PٙF5kұiS-Ov O~L ֖8doOM31wjGFR[ogۗXkW#"051lYi0 tZ\ڰwsY)D5iӨNݹQ*:5oޢ]ZE[י3ҶQ#=3t*ߙi\^Y6mh˪;8w T)Zի[k562ɓ gӾ}ܺw[kk<ʕcӦ@ $Z!x\te˖/CJ. b.o6%Y122Stj<A]ߣ\980s‚~;ퟞ72-R0 ֖ ޝݻgv=ooy{g+gvrbl>ٮ!^?D !̶m aϞ=i{}LL`Ν?^myǑg,;tOm!CWB! $<<V^M||<...L0!CfD>MzRJI֪;n],-,^q4B$Z!xXf ,???8ěmxm>c668:=v,UP*Pr嗞kinQ'"$B!T*ٵk+V`ƍT*|||իֆQ6VV*٪{mv=Jl|<>/jv9籲Sx_Ttqr&_΃G;v?&8!_//|}h߭w9~Gv6ŊѺ~Lϋw {(.gQBWKh!⩫Wh",YBtt4 0CRzuC'k &ei {~޴ Z"\BRǵe~9c2ej&Q'VkXT*׷n)_g}m a lrUtO;Wg###R{XcLA-mcBdB"-55PZn'3fà … %'0O3f,n@˺u9|9G߾}8ٽ(=~=zpwn,]kɒ]gddDZebuڸ}>W#"hZN[=y2%Krxb"~o?c7gN‚;4iasl2jDOJ%8lwwfXpyEf$Z!Dte&NHٲe֭ϟg?:t,s*]&tj~jrtdرT)__w)BN2[!DoFHH{Ȉ-Z{gC"ێ;GZz:53\ܿ/_&19Y/oӨf;wN$@x85Ӗ9a߉~^6ۧ-!ɩ\N*yK/ti,,tJ+ :6k˖qU! 5IBz.\`ŊPL&L)_"Wcb(_#G(SnldD)GG׮R[kk?ypIʗ)wO>DVezV(TqCi''[9x2B@QI-Pc͚51~~~cb"7,,2ʜsmӤV-v9BL\mmu.7۾^f&&Ttq[l,?ehginϽ'q\Qɜh!Jxx8 ŅÄ́ y&aaatUhQ(dluNvˊ..r|g5E̼hPL;w2M^K%y8z˖%#H .: B,IB?~LHHjחKҶm[¸u:L!UUϿ.tt"\]^zd|h46jwNƼ'N|hn[hzݪ|ˣ\9vWCV3crmٵ۷ٴ?QIQɫx!opBBBXr%IIIT\@OJB~EFD0x4_ uv/i3t(hU/6x\@vw'4, #'{{>~̏&`gcѣi3t(M[ܼsJQU~C zJܙf.oZԭ%nZ t-ūbEfZHJsGtnR[+'"KvnRJoIBQ=zDhh(󘛛ӱcGhٲvriϬ]\!:&BAyմNNXԟfѣ>z[kkT΀NPT\ IDAT(>3gRJJ;9q#*ڞzgۜ9LGY 5oebli[YC-OO|U1cMML8ٳ<ՕTwϟϨ~`]XI))Truѣum!Dмl!DoͶmh׮Ac>JbڴiL4ɠ׃Zf߾},_דLժU۷/1O e]\hź CojުцFR ٌEZFPKĤhHJIZͨ(*vٳ1b"Ϥ'Z!kݻ,_nܸ:t VZ:>>L4^zammmB!tH-╻v+Wdܾ}{{{ϧ~J5 B! I-HMMe֭w^4 >>>| !B$BQ\’%KXx1̸qرcGٻw/Fx8~x^ŋL8eҭ[7.\vaaatSSAZ&>1СV> j II~Ľ{{NuRRPSP ޽]_RT\Q QxI-D`eeϵkr={֬Y\t~;;|Pw}]ΠA(S }L07nh{MLd !aT:DvN4# }Z5Bݺucɒ%L6^zg022o&MгgR7ZM^}6;wL2yjǬ] cffo͇~?$956/ٍ@VsCⓒp+] 3|i{@ ;/G"dܹT^ѣGiӦl7zhRRR?~`& 7|Ν; UVn'<<VZEbb"ү_?1b! FYPP\9 oW795lA}bӇI%޹CuЖrtda|ԡNmI/$&.N֖۵cȑyJbԩiޞ/dX:uڷoϸqr|~ll,֭c={sss:vH@@-[*HԹ3UʗԔ,޺[4^]n~][^j8|/OJ"p0%{,^[FD˦M2J4UK[ѧ];|03Çݽ9W\8{f@NáX1nDEhfϘAڿ9]ZMÆD?z}x1GΞez )SuNڿ-c۟ͤ|T1D>:~i''ncˁxS*$!u+]:cfTTuaaTt}?mp?&;w2},-,:uT>}(S*QǍԪU:{4jDْ%r1qq:dTx3̤'Z\9B1_~%k֬aĈl XLTR|嗯0JWtޝe˲t%+V 99*UȀprr*xsxh?Wwg޽:IG~C?y0O:VdL:õ--:jZof@~ڭ%[bϱcyJML;@ޢl9p $MÆ x=&iy&VlNN^ʯ;wҮIΚB`Y|{ٲٙ&Mٳ84(WmefGp)RRX/=aCݺQC٣DӰ!4lWFK_U#-=?_vuvvbqcL>i('-k,[U@9?' K>ccuF!|`kKJ93SS-D.'!]ҦM,{v1=z J[ƍСCLOj5#$$͛7 8ŋ¨xY[ZbblOrFR'ycbʒSS9p$##ILNFh˳+hl͊2-Nq-~ˊL1++d1=*W.WX5:-==O|Sjq#j@U s$BQ5@oߎ3+ٳPƌ#A^J߾}9Rؿ?!!!lڴt|||8q"={@Q QvrBТn]f>>ڄ VL\b"=»bE򌭲n߻kɒ򌄽s#a,HQ*stvO%&'輜ѣ|m3x:_OƍG}loƔ,cIIMEVgk[1+WKBE2'Z"lɔ/_#F|Izzv1/~(^?t Jźuzidd$AAATX֭[{nɓ' Z7%>)۷oxQŅ\`+/nݽ[ЮD^Ņ..Nl T絻w딯 ̽~V2eD 7ndBL\\8q"sƎi:g:)CJŕ[}--DIOEX"c]v%44M6qe~WYL 'piBCCqww'44˗cT*>>>|[` ! ^v혵j?\Idt4}'{{nFEIFMXbXoxWH175ǯ^ohN]L3gHJIaˁ,X%KIn:u([$o7h}XkSIZ|_|>ٙD.MjZ}i, BRep.X`5k073#?W QI-DץKڶm˚5kXv-os0\O… Yt)}'OO?%:: aèVPHL2X/y47',8~SvnWKss|}I ~'Ook^Cm,Lزbnf~3_K%$84/eɞ˦LN}c##~Hϙ9sʯӦvO<\a޽lػ7x3諯:~<ӖyKav^gVMçS;211Hgݑ`ޚ5:rDgM* -nnz3X++IFtG!0FCll0k׮ѨQ#111TR`ȹIK򢂋ZܵkTpqZ{8!!ӹ119{>+Vrψ{8~<*uT[YĄ4-ͨ(*vٳ1bD_O&I(Ҏ9ԩSٿ?9:DWښ:0uT<2aƆDLLLwߥQFx{{SbE J"7I"$ Eٳ3f vvvt*%%ݻwӺuk,-- ΝB Ez+Jō7زe [latIΨQ8v \t3fhSjUTBjըZ*޸g:U!B%k2j(4iٳ_:dï#G@n8tuyQQQ\t .pE.^ݻsKWժU$!B$Z97odРATZ bU+x]-[e˖ѡCzɩS(̼gBVtݻDž t={vZm@E!M'I(rRdْ@7F%0`&MJTRlR<:: .p[!B3zy! #Gw^pb%O!^GM6]v,Z/34cTT)_B!($EʴiӰwކE\vvvܹsԩfffzj3fvZ5jĈ#(_Ą3gV ΅N/4ӧȗa۷~,YR[v5Ο?_Yn$%%q ½{ϴݻٱcί={l۶ J=k,f͚Ezz:ÇS/cee%=B!̉Zz_TlY>c,X@yM4\~4_266fذa92˹UմkNᆱ7L=ƌJGδرc3-߾};Sܿ͛7#~aY&lܸQBgP(ZM!BIEWXlHcƌ֖;2}BR&,X ߒw}`,XU/&y!ƍ˗dExxxrE~!!!zLMMT^cǎCrrǽrz[ӱg׏'͗INNں#B!$ZzVVV@ իO%dmbbbBI&YSFFF >CpntY>nմiS7n\Ѯ];:u@֭ҥ #$$$ 4NNNann#>>>"ix{{OV /4 )))D !BBDBʊC'666|~wy///,X@zg޿_goR_ΏlSIDATV-6m9wCnR l 7!B(*DՋIP0b޽KXXƍpWh4yk]=?G]!B 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']) def start(update: Update, context: CallbackContext) -> int: """Start the conversation and ask user for input.""" 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 def regular_choice(update: Update, context: CallbackContext) -> int: """Ask the user for info about the selected predefined choice.""" text = update.message.text context.user_data['choice'] = text update.message.reply_text(f'Your {text.lower()}? Yes, I would love to hear about that!') return TYPING_REPLY def custom_choice(update: Update, context: CallbackContext) -> int: """Ask the user for a description of a custom category.""" update.message.reply_text( 'Alright, please send me the category first, for example "Most impressive skill"' ) return TYPING_CHOICE def received_information(update: Update, context: CallbackContext) -> 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'] 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 def done(update: Update, context: CallbackContext) -> int: """Display the gathered info and end the conversation.""" user_data = context.user_data if 'choice' in user_data: del user_data['choice'] 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 Updater and pass it your bot's token. updater = Updater("TOKEN") # Get the dispatcher to register handlers dispatcher = updater.dispatcher # 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)], ) dispatcher.add_handler(conv_handler) # Start the Bot updater.start_polling() # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/deeplinking.py000066400000000000000000000122561417656324400221230ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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 Updater class to handle the bot. First, a few handler functions are defined. Then, those functions are passed to the Dispatcher 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 ParseMode, InlineKeyboardMarkup, InlineKeyboardButton, Update from telegram.ext import ( Updater, CommandHandler, CallbackQueryHandler, Filters, CallbackContext, ) # Enable logging from telegram.utils import helpers logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) 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" def start(update: Update, context: CallbackContext) -> 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 update.message.reply_text(text) def deep_linked_level_1(update: Update, context: CallbackContext) -> 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) ) update.message.reply_text(text, reply_markup=keyboard) def deep_linked_level_2(update: Update, context: CallbackContext) -> 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]({url})." update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) def deep_linked_level_3(update: Update, context: CallbackContext) -> None: """Reached through the USING_ENTITIES payload""" 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)]] ), ) def deep_link_level_3_callback(update: Update, context: CallbackContext) -> None: """Answers CallbackQuery with deeplinking url.""" bot = context.bot url = helpers.create_deep_linked_url(bot.username, USING_KEYBOARD) update.callback_query.answer(url=url) def deep_linked_level_4(update: Update, context: CallbackContext) -> None: """Reached through the USING_KEYBOARD payload""" payload = context.args 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 Updater and pass it your bot's token. updater = Updater("TOKEN") # Get the dispatcher to register handlers dispatcher = updater.dispatcher # More info on what deep linking actually is (read this first if it's unclear to you): # https://core.telegram.org/bots#deep-linking # Register a deep-linking handler dispatcher.add_handler( CommandHandler("start", deep_linked_level_1, Filters.regex(CHECK_THIS_OUT)) ) # This one works with a textual link instead of an URL dispatcher.add_handler(CommandHandler("start", deep_linked_level_2, Filters.regex(SO_COOL))) # We can also pass on the deep-linking payload dispatcher.add_handler( CommandHandler("start", deep_linked_level_3, Filters.regex(USING_ENTITIES)) ) # Possible with inline keyboard buttons as well dispatcher.add_handler( CommandHandler("start", deep_linked_level_4, Filters.regex(USING_KEYBOARD)) ) # register callback handler for inline keyboard button dispatcher.add_handler( CallbackQueryHandler(deep_link_level_3_callback, pattern=KEYBOARD_CALLBACKDATA) ) # Make sure the deep-linking handlers occur *before* the normal /start handler. dispatcher.add_handler(CommandHandler("start", start)) # Start the Bot updater.start_polling() # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == "__main__": main() python-telegram-bot-13.11/examples/echobot.py000066400000000000000000000045361417656324400212570ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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 Dispatcher 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 Update, ForceReply from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackContext # Enable logging logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) # Define a few command handlers. These usually take the two arguments update and # context. def start(update: Update, context: CallbackContext) -> None: """Send a message when the command /start is issued.""" user = update.effective_user update.message.reply_markdown_v2( fr'Hi {user.mention_markdown_v2()}\!', reply_markup=ForceReply(selective=True), ) def help_command(update: Update, context: CallbackContext) -> None: """Send a message when the command /help is issued.""" update.message.reply_text('Help!') def echo(update: Update, context: CallbackContext) -> None: """Echo the user message.""" update.message.reply_text(update.message.text) def main() -> None: """Start the bot.""" # Create the Updater and pass it your bot's token. updater = Updater("TOKEN") # Get the dispatcher to register handlers dispatcher = updater.dispatcher # on different commands - answer in Telegram dispatcher.add_handler(CommandHandler("start", start)) dispatcher.add_handler(CommandHandler("help", help_command)) # on non command i.e message - echo the message on Telegram dispatcher.add_handler(MessageHandler(Filters.text & ~Filters.command, echo)) # Start the Bot updater.start_polling() # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/errorhandlerbot.py000066400000000000000000000066011417656324400230230ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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, ParseMode from telegram.ext import Updater, CallbackContext, CommandHandler logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) # The token you got from @botfather when you created the bot BOT_TOKEN = "TOKEN" # 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 def error_handler(update: object, context: CallbackContext) -> 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(msg="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 = ( f'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 context.bot.send_message(chat_id=DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML) def bad_command(update: Update, context: CallbackContext) -> None: """Raise an error to trigger the error handler.""" context.bot.wrong_method_name() # type: ignore[attr-defined] def start(update: Update, context: CallbackContext) -> None: """Displays info on how to trigger an error.""" 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 Updater and pass it your bot's token. updater = Updater(BOT_TOKEN) # Get the dispatcher to register handlers dispatcher = updater.dispatcher # Register the commands... dispatcher.add_handler(CommandHandler('start', start)) dispatcher.add_handler(CommandHandler('bad_command', bad_command)) # ...and the error handler dispatcher.add_error_handler(error_handler) # Start the Bot updater.start_polling() # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/inlinebot.py000066400000000000000000000061571417656324400216200ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # This program is dedicated to the public domain under the CC0 license. """ First, a few handler functions are defined. Then, those functions are passed to the Dispatcher 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 uuid import uuid4 from telegram import InlineQueryResultArticle, ParseMode, InputTextMessageContent, Update from telegram.ext import Updater, InlineQueryHandler, CommandHandler, CallbackContext from telegram.utils.helpers import escape_markdown # Enable logging logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) # Define a few command handlers. These usually take the two arguments update and # context. Error handlers also receive the raised TelegramError object in error. def start(update: Update, context: CallbackContext) -> None: """Send a message when the command /start is issued.""" update.message.reply_text('Hi!') def help_command(update: Update, context: CallbackContext) -> None: """Send a message when the command /help is issued.""" update.message.reply_text('Help!') def inlinequery(update: Update, context: CallbackContext) -> None: """Handle the inline query.""" query = update.inline_query.query if query == "": 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_markdown(query)}*", parse_mode=ParseMode.MARKDOWN ), ), InlineQueryResultArticle( id=str(uuid4()), title="Italic", input_message_content=InputTextMessageContent( f"_{escape_markdown(query)}_", parse_mode=ParseMode.MARKDOWN ), ), ] update.inline_query.answer(results) def main() -> None: """Run the bot.""" # Create the Updater and pass it your bot's token. updater = Updater("TOKEN") # Get the dispatcher to register handlers dispatcher = updater.dispatcher # on different commands - answer in Telegram dispatcher.add_handler(CommandHandler("start", start)) dispatcher.add_handler(CommandHandler("help", help_command)) # on non command i.e message - echo the message on Telegram dispatcher.add_handler(InlineQueryHandler(inlinequery)) # Start the Bot updater.start_polling() # Block until the user presses Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/inlinekeyboard.py000066400000000000000000000042651417656324400226320ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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://git.io/JOmFw. """ import logging from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, CallbackContext logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) def start(update: Update, context: CallbackContext) -> 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) update.message.reply_text('Please choose:', reply_markup=reply_markup) def button(update: Update, context: CallbackContext) -> 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 query.answer() query.edit_message_text(text=f"Selected option: {query.data}") def help_command(update: Update, context: CallbackContext) -> None: """Displays info on how to use the bot.""" update.message.reply_text("Use /start to test this bot.") def main() -> None: """Run the bot.""" # Create the Updater and pass it your bot's token. updater = Updater("TOKEN") updater.dispatcher.add_handler(CommandHandler('start', start)) updater.dispatcher.add_handler(CallbackQueryHandler(button)) updater.dispatcher.add_handler(CommandHandler('help', help_command)) # Start the Bot updater.start_polling() # Run the bot until the user presses Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/inlinekeyboard2.py000066400000000000000000000157031417656324400227130ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # This program is dedicated to the public domain under the CC0 license. """Simple inline keyboard bot with multiple CallbackQueryHandlers. This Bot uses the Updater class to handle the bot. First, a few callback functions are defined as callback query handler. Then, those functions are passed to the Dispatcher 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 ( Updater, CommandHandler, CallbackQueryHandler, ConversationHandler, CallbackContext, ) # Enable logging logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) # Stages FIRST, SECOND = range(2) # Callback data ONE, TWO, THREE, FOUR = range(4) def start(update: Update, context: CallbackContext) -> 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 update.message.reply_text("Start handler, Choose a route", reply_markup=reply_markup) # Tell ConversationHandler that we're in state `FIRST` now return FIRST def start_over(update: Update, context: CallbackContext) -> 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 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. query.edit_message_text(text="Start handler, Choose a route", reply_markup=reply_markup) return FIRST def one(update: Update, context: CallbackContext) -> int: """Show new choice of buttons""" query = update.callback_query query.answer() keyboard = [ [ InlineKeyboardButton("3", callback_data=str(THREE)), InlineKeyboardButton("4", callback_data=str(FOUR)), ] ] reply_markup = InlineKeyboardMarkup(keyboard) query.edit_message_text( text="First CallbackQueryHandler, Choose a route", reply_markup=reply_markup ) return FIRST def two(update: Update, context: CallbackContext) -> int: """Show new choice of buttons""" query = update.callback_query query.answer() keyboard = [ [ InlineKeyboardButton("1", callback_data=str(ONE)), InlineKeyboardButton("3", callback_data=str(THREE)), ] ] reply_markup = InlineKeyboardMarkup(keyboard) query.edit_message_text( text="Second CallbackQueryHandler, Choose a route", reply_markup=reply_markup ) return FIRST def three(update: Update, context: CallbackContext) -> int: """Show new choice of buttons""" query = update.callback_query 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) query.edit_message_text( text="Third CallbackQueryHandler. Do want to start over?", reply_markup=reply_markup ) # Transfer to conversation state `SECOND` return SECOND def four(update: Update, context: CallbackContext) -> int: """Show new choice of buttons""" query = update.callback_query query.answer() keyboard = [ [ InlineKeyboardButton("2", callback_data=str(TWO)), InlineKeyboardButton("3", callback_data=str(THREE)), ] ] reply_markup = InlineKeyboardMarkup(keyboard) query.edit_message_text( text="Fourth CallbackQueryHandler, Choose a route", reply_markup=reply_markup ) return FIRST def end(update: Update, context: CallbackContext) -> int: """Returns `ConversationHandler.END`, which tells the ConversationHandler that the conversation is over. """ query = update.callback_query query.answer() query.edit_message_text(text="See you next time!") return ConversationHandler.END def main() -> None: """Run the bot.""" # Create the Updater and pass it your bot's token. updater = Updater("TOKEN") # Get the dispatcher to register handlers dispatcher = updater.dispatcher # 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={ FIRST: [ CallbackQueryHandler(one, pattern='^' + str(ONE) + '$'), CallbackQueryHandler(two, pattern='^' + str(TWO) + '$'), CallbackQueryHandler(three, pattern='^' + str(THREE) + '$'), CallbackQueryHandler(four, pattern='^' + str(FOUR) + '$'), ], SECOND: [ CallbackQueryHandler(start_over, pattern='^' + str(ONE) + '$'), CallbackQueryHandler(end, pattern='^' + str(TWO) + '$'), ], }, fallbacks=[CommandHandler('start', start)], ) # Add ConversationHandler to dispatcher that will be used for handling updates dispatcher.add_handler(conv_handler) # Start the Bot updater.start_polling() # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/nestedconversationbot.png000066400000000000000000017274541417656324400244260ustar00rootroot00000000000000PNG  IHDR CsBITO pHYsttfx IDATxk}1"ZQ(=T^ A ""|b!?`)AJ! =zEqQIbXhJ[v@4ٝ'uj}SZt̼7y` !,kNL c !jM@rdZdWz,bPgdW,Kd|dd#& *+#ۿ,@ucQEId5^dLLJP[5 ) Nb0z-$& #uՓh1Y+!E/F:06Hu@ d mz-!& @d ym & @b'& @,b ! Z,@d & 1YhB,@d & 1Y@sy{"& 1Y$@L*#^bZ&& 1YX䓙}.^~۷o߱c=lڴiC/_.yx!>8??~zjj K/]N;?T?1Yu8p'wu뭷?333ӿ}zhm/rʮ] 1[nǏ 7ܰgϞ履~z7<_裏Rp?zѣG;D׻СC,>p1/b䉉!6믿nX{n`L?tm۶KHɓ'd;Ç;;V !!>3gΔu+Wk޽{:D+ypl4^oÇ\R0eY&& V+%2;^q>m۶__XXطo_YG9t?PX|&'':DuݻwWgsss~Ξ=< !UbwQGykhEwz/^,z'|rǎ#~u'xbĝ{}W_ !u]w}-[Nss͝>}ɓ.]wh9JzyDL !,1˲X@NZ ^矯gϞ=xSO=U6o߿zkff펲;??_X|W7m4ʶ>ogΣd!}_Q&uE₊֊ֵ-u}ZB`ŧ(- L&xzn=sȑ~zliie>O?t̙֭+ωG}tb}KJJ"Œ  #@`ҤI{wwxZ:wMJMMiH I&1,(k&br =cCSN/~WZϛ7w93+7FHJJҥ-K5]͍=2Y}7oުUy>VғO>e˖|qE+6w"뮻yX^}UVgݻ7lذ|7K, Oj׮}ꩧfggG߼cǎ3fTU5e{SRRfee?X޾}̈p_bu^9UV[ yyygώ2X/_ K~թSw 6,Ϧ/,؛9slܸ1}zAAAU 8D*2Y*bʼnUdҤIɐ!C@O:GrssKJJbUNҥKDXZZꈽ}z XzO?g}>";vlZb֯Ji&:/g͚Ue ,_<,\`+@O2%"*?ڰaÜ9sb! =::ꫯbuʞ$%%~e^nݺ==lٲz*"0`'~Ucǎ?pwlݺTC999ɩï{ѤI<@<<}ӡP(X7|sZZZt/X ::u̙3#+M6qg}vܹs=oT+W^ԯ_O>IVVVEEEӦMo9o}JKKÓ\>@%Y& PMI[2lM2u֯_B1]U5syj O;-ZD?f5ު OZf2bHg,@f̘cǎ]v:uo߾u֍~<'''}F~gql*W\ѼyO?:ujx㏯^:ⶱcǦű_~xoVIIIJ0zrrr裏6or5޳|4<7JMT,([6ج`0֮]яO:u׮]1sG2$:BP *S:un2/,**˗/?~| ;/N[n]xҸq={Fߙ{*P'?g٬u_Db2Y2ܹsaVVV7o߾}ƌmu 7$%Eλ`YyYgu):_bų>B=S\\~VZcǎzU`Æ =K,k:?&޹ e]v衇ƵX wuW˖-y䑽߹`#Gwq7x 'P5^IIɓ#2oC ?~|D>{7k,.d~Nc֭ZΓ'[4>3dʐdeeԁ+ cXkyow9묳bxP~ 8pʔ);ƍ駟Fmڴ+ݪ*kK.z~ܹs/^ztM]tUO>ٴiSxҼyݻe'O R1q-[Vg۵k&@/.W/uj'O -jͧ?uc,ڣKa: ppKJtjg֭~aD8dȐoBxݻwWA㏷nr!{ޟx]6c'?sD^6m4s̈pȐ!|0++矏MvgƬ_ F /l۶-<\n?ehջǎ{.ǻIu ׷o߼~)S|;**G!C$%Cm333333/^=K.eD+ss9q1l4/=Rd 6>@lY& _LRRRtڵM6|[n-[#򜜜/mذȑ#z衈'6lXc{\C>-*j˖-xwV o/Yf?gC|U0 Mgr z#yX"###6-99;Ht &ںq+y%<۠/E1KNNND]\Gm޼9E]ԤIp˖-O=TϊÝ;w'zhΝvF`M|db~x}e7n%+W.YmVZR㪳%oŪoW[pgaKtM]rʵ+Vϒ]J T#֭3gNx \dz~鈰/~:u\y{oDSOW&իW>}>?鞶iii#F:t |m۶[oYf{ʆ7iҤ$+++ w髯srrF~5-'>21a1a~yhu:a ' 8!5-5Vۈ@UVZ7yW93gO=+];dmwjylf>* @X& BPxҽ{-[c9m۶+WsrrbL6wy?Ý;w>v[ֶ̏m0##`zhթS窫:O0JJJF=eʔFUq ۱c|feeBvvv2م .]d=տ}ܫmشaLoZ_|55\(ںq֍[QF{t0|¢O&.3ݏ9<\C[߿nպ`f?Wm۴m ,x6lrMkש_%tY_}I—|_޲a^J~_u [FS@n7fOC},:ܲ~ˏ?giO(܈Cs)N:E/={5kV~---k#/nժUl6lx- 6l̘1g.7/;vlwӧ'M6ݴiS4mڴ<''kl?i綝/=Rx6| nxhy'}Te6o[>5"krN?_?ڲagsǽ56弙UKWdۯG.XZ^sOߧjd5k̝;7"|_~JN.--waQQѣ>zw8*&##gyyĉ VqɉH6l_'Mt5ʏi^'g_yv+9~^ݧS?&\~W5>#3"kX_k^`iIiO ]͍1rG篾q"4z[n̫v˫Ju?0N×.]hѢ8 8(x“Դo>Qdckc];X& &YUJ Tq>,??UV1bs[RJD?}rORRFҩ/O}ܨ_^2ٺ Nm?}mNũO~xÛh6۴|[Ϻum@Y& @B!8x, '|r׮]0 =q"ִiO?}ĉy~~~IIIrrrBZӦMf͚#V\9.];O/y-*9өDcsgB0luhV:S ]U7oΊwtu邥7 !:pbzQ:czsMx)~]/))i~ޗm^9W-]6U`nz%D:wuٽ󦭚6mմv\&=ՉL|"=:}4i4P/Nt ;8+6W_}'#yX"##b3" tM^xaD>mڴ T{^&[ZZZPPP^T*)S']t\i;v:th@ c,@9*~Oφ')& Όp5C˹I6KsgRr^n^-ڴHd/k_N>n?7#3c#;ї]e7q綝?~rW ixyVSzװ:SR+h,YhѢvCSN6,zl ͽ+*6sz٫WYfE>`q"ֶm2󤤤*nw.q~x{ƌ7!zPV OFdjתos9DPgO9qW۾e{Drʕ!ÏnڪSc{Ձw^"_~$ڵk׹sʛ4iRgѣYf}; (sMjJJJzzz՗)u͙3'<  ̬0??-@y?gÓkG?$&kԔ~C2~xdV5](^&]ɱen\pҥK+9yOvzꩧF֭ӉT̚5köm/S~yyyP(<9[hQvZjjjtGhy'?<t6dxiIit}b2|e'0z}m ksύݽ{wGZjܸq,fծ1\NO>9:| 4fZvc=cwqC ߿֭l(ɹ뮻6m}Ȉi͍H۰aX?餓ԩsΈ<''2Yhw_=of̘tSY< {?7=wч >pyth2M(Zҏ'}<_}-2Y+ \xq@͝;wڵIƍ{#$"ɉ2@ 0jԨ'kܹsν;vثWݻwҥE 5kL:uĉ|M7r8WYYY1_vN;wމ{サ;---gUnݺp`^^^˖-c88p>}%c/qWsEZzc(}cxb4p~yZVMT,vʙc巉@Md,PsE4hPrrr hܸq^>裈<77[oMJJY6mz?cqO Xp… z@ аavڵjժAj*,,ܰaÒ%KVZ ]v}c#sK,YhQxvꩧe;v1cFc{Vضm[ BNhђyK“^Czu}􂂂$Jfff+fZwѹ ]\\~5{ ^oį^-_/$2&%%䄟 aا>>ח>mYg*5kYg_Vqa㻝 IDATbe_&4iRD2xԠA}N>="˻q. &_>~G_ifϞ7TrT08p7xǤ[-\pٲeIzzI'^{`YYY8 ^ZRRSdݧx_>oŻ]9n-qqV_ />y777bL5T.]ڵk 0 Ng]}ݺuηlҤILKKohܸqeMOOϟnݺ2cԿmF1^}ٳgϞ@`ƍ}ٿ?믿....s9qk\sMaaaDxGTlZqqqd6m+Xnzy-BAؠA?W^)##Sb+ԧS?]0gAxҭ_}&Ljzֿ_'>qLcҽjJD[Ϻ5zl01f%w\RvƲL VeguҥK.---+ѽzիWlgo߾}jI& apqq%K/^r 6l޼yǎIII 4h޼yFFQGեK͛WM#FpZ9tc"99/TC9 NB᥿4!e%$w׭[n7>r㗟|9S^c=Gg`U=a0Ż럐JpBP233/|@MzG}щ.@ϛ9/<һK~'ZRRR]rk3=yO<1U߰mܸcF$ @$%PBDW8IWςaFG6"!} `,P233]@_}nxN?>Q}ʣaӆxM[5ŕ\+tgO#nzu8 TϽ+d~*"V?7Tiie楥[{teJCU&LZ0g'Gu;א^_wjB1Z~2]ŕ[ƷRTXTMjd?LT8=uS%c/9Pnݰ5:lܢqef6lڰ|m[p咕1^ɥJfewe@;W .333!W9:oߩ1d#=2:|oG)nפBo~\(*勖^zmoj\2YA^&;.NJw.󛇴r%|۷l/σɿ;ҒW`0Xn?>:,-)~d#o.+C.R7dwM7=~Eyֈn#~}W.YVW~vϢ#xwk%/`^P"ٵxD5%t@|vhvTe_-yxXFfį'&%W2فnٰ%"LJN:#;|D#=Ж-4iZ\TmӶwќ|:2Yc'|]gv e^MMK9g~:t 6LMK-]RpmZr%+_,>sϹꜽQz7tImjשc weB换7ϦM^/}6o;'ǥ ,|~y/g9=]mܢq3vhQa[7n5׬Xbقew>[#Vey##KI&$Po&;!b E_T5dt܅ .?=駷>~kL^vee^-U<3ޞQQe_%+~|ߓ2b/;2"/Qxw=pyjWC{t>̓>|2nZo=*npPJ䫆D7 >0Q}*ǠʳgC/t/_^~`Ff1=9a }ԩ_ B/?÷>EE{?))I&movg^~f;7?7sޚք_j}X=:O*g<$-=qҽא^G褤}/}6wq'~R.qUTX43gYf[͊ҽߟڢmڶnFE T@`/% 8Dfj,eDLeUF-_.1iަ߾V ֮\!֍[wn߹xwrrrZzZnii WZZvumٰ`{AqQq HIMIKO۠n 5oԤeԔʟUTXq]ӚhRvz|mm]vVM@UTXfnZpwZԮ۰n& jڠIl dXk1x @ff}셷kӳ|6|l կU}6Zk=m#&HْZڪաZkUoT?}Zk;]%Agq +׾ԻIMθD!dP( pxnsE\>@5d,1 T99{~ǿ4jQg%P=Y& `/BQaQx2u,RBPjMk7I& νD-dO ‹X0<vð:$PmY& @eEXŖ [^{^zC>@uf,K.Dx 'OT:KItB/Nt 8أk1ITLB`0-H/CP(@ $"քE,*2Y 6">Q $dea8@Y& @Y(PcE|g,de@)Zc"֊pW>a,qgA,/{.i|7LA,U@QAqiqTZ+n*VEOP"ZZ9(Ȯl ɜ9O$$$Lߏ/'&?(ׇHHdj2YjD>YGwL,5%O6I@B(  %5 Ȃ`  $$M5-9 t>dqX0@P& @mP1|PRcuHIX "DK,@S& @m;$lPc{L (d2Y8L (d2Y8L (d2Y8L (d2Y8L (d2Y8L (d@j,zL0 @5Kh I,4P5>ģL 1L aDJΟKrs#@0eY@ +@,@' @,@O2Y??<' qM,@b' F,@' D,@"' C,@' A,@' 5 PrsC@  cTM@X3ɱ^Z??<  Pd}-d/`0+@bS& P%*ȇ @K@p@nnHowHJJJFp0&@E(@Q& PxL' q!5 P0$HJJ я3eq@,@P& Rc 8~u V(%zL,@P& eq@,@P& eq@,@P& eq@,@P& eq@,@P& eq@,@P& eq@,@P& āX/[sFtz߾]s/ά?.-Çɩ}`o6cgG4W3f۶]LHJJ:_zE9j2YJ/ňkBl4?D_zi-{O-*K&M,QPPoƊh׺2YNɱ^^rի]k@ڶjU[LND+5 $u=쯿>$|d ^8k6X5j_RrHemrrO9R8Ԙ1 XOorz2325n2;mV8խ[jJJ @E)b8$ UX\\`RR/^(yԔ]EE_5KjO8|/~Q._mǎ'" []EEvXaWZh[6kvŹ<83#W2@RnʈWBiڸg'WÇYIk6l>9}c-]ɱ^Aۜ2/)ի7̉"u2Y4*΂A7ܰob@}עEK=>Il:u{jȐ m„&Mx/>{9$'n.͚ WvNvvq7hS`/Q^{ȑXfgJ(jЦUy2r0zذ{xO>eP& ՠ~ZZfse@\ c2EK2(&Ro2Y v|`۶ j:N,Tdp`oPǥzHmsr"U&efvŊ=Wn@~9sKM֩GV;?oۖlժ7nܲeΝŁ@ ~M߲AۢER[w3?h-h1W}[(KmԼItXZ4m6kܸZ˥K׬ٰeKAaa hؠ>Mк!;6$e{fQݺ 0!&Rl? S:iָ#F~7 >>Ȗ[w=9;ԦMqO1ݻO;tS~{7ߔNweV:˾[]EEy_7.%9#Gwz?\:EQ0 d۵n_&$Qf 髭z2K#5#K,vm{UD_}7gu{|)ؾs^՟m_r+rTR& գmV{LEVfo}&7ot MV_pM=0iRn:+k8W2O:t=޷o4 [x!MM?OXtɊ']~͛#y۶___lָqw ꥦ<LJO.,3##`IBN>>D9|s, 332Nѣ Ӿr뭟mWz6hP^5z*{ @-HMHhX2Zisĉx驧^x?p׮w[~ܸZ((8-\X M5JJJ*..zm._^ɻzjݦM%ooݾk-IĚ }*/Vw\_7HO 733fD??dѢpԐ!O.+￿C‹̨_^5 .Zn>E-_:!qJ,@Q0+M5j٬Y vikAY%%%Yۻes?O+ +'uT_tԩ_n]ti}mڨQ lﯾzz刭yO󟗟}vۤQVݝխ۲Uk7ߘ3ϟ駏0< 7lYU}ݷw!ʵk7SENƣ?iBo/sSza{ZtO,\$$7iS9?1cj=kK<=ڵj`7߼>ǍoYz)W]5wF )?d|<"1k32`@֭#YY'qb zM7}hQZ2٫wwG]L￳_9_8 Iz=tX\\|=.-Ͳe.2{Ç'ڵлg;))I)?ujHmF3"6' /Ԝu6sMv_z[|?Z?-8/ <9/.;e]ݶcGx wRRчVtXUL .˶q}ssK=:u΂[^iƝ޷oɻGvi}LW[W?D۹_|Ǧ$'Wv[裹C\dѢ-[6nذk[GHJJJOKkCdm4/^\nYYIIIwK/|k<8LvG}׵C* i۶'O G \[emݾƇ !y6n\jJJ]}ォׯ۶j㏗Pv|ј1믟s≧S΄)NEqL,mر#ƎWYg ?$l޴iYOgԐ!!a C;wVzuG|i}$''VBl)c_ @ ;++Lv~fefn߱j+5@?ΘqȑK{+'#>;~nӦ! ,(y7n\v˗x ~y'7󇮿v*>ߚ;sCŧ^2YOjsW #|[@HV^HRPXX-۶mh&3Y232ڵjtʐ|ݦMUyСh $ ڨ5CO9o\:QPGVQ3IZzWsNwyǏût+?*{IC'W|=أ!;FYZiӎ>s}fƌ]EE},[z1RS GQ~x>󣏪6 T.yy==C@ P ;߿ݫvO?b͚M7اO7nN}{{Iii?? >\3x7 -\_8=޾zo^wQn5d;,)))TPXe5rK.[*-_ 3f3L=onn4oˆy OvTn>8$xqAaaZzU +}$>ݻv/LQ~Jrr7HOOMI}{dfdy*.]zz B&O~x/ܵtҥ}z~f~%gߞ;3␫.4l]W\Q?.Y1ܹ;˃ /-(,zٲC:vf2O,@' ~%]ڷfB~`0dŊO-rV7lٲu]EE^>g5dHx 3gZ,N^;dH Kξp yį~رiiO[bEļS6,m#ydL pydX=O>[ϙÏ?j g׶UeVwMxᅻG,InӦIf͆rJl~םw^pꩃoe!W]O;fۈf7HOfZLr Z z :aĈ'_~Y%III))Ww^xK/m۱=^q9ҪyrԳ܉o23g>0iRokMJJn$ʭeeE7nd2YDOb/~y˗z.yffFFH~IoQ->IzZڈΪթM>ڤQKMVrp`۶O,(G˹1.O\'18{X/@33$ e WsϣӦEqz>'uTԦMJrrFX~} I]uyO/f} =zzٲ> $f]\:{1gNH>]u~-[F+5%%<,ܵ+e #iiQNE $׼0&PG6aB&٬FZ[뢁j.b,uD?O :yrH2w.fvÅf*ƙᖭ[ܤ 6r2!$}Pkޞ7ϏmѴiQ[wf>vɆ-[&jȱ*/niѴixvÆ`0&k6l2YOj+Ǎ Ϗu ٧Wbo/7[N_L)ӧo۱tҭS8fût ?۲ni۪UxXPXjݺh6nʊ? (HL}GYI@D@HQtX@QW,ւeueˮ"(XV VT HL 2ofI&d<\uOvW?pQVV0)1;LKMK%vf`A"gx#'/﷏GMXkI^ݍ"yڵp8k5dI,5S+L`e,P/ndx1צM͗VاOƍ˄ LǓ?5kJf6irXC'ؒP88 jwR˹s#65e,@`,>YAdzgGQe-ꦤ\r晑KB@ e8:))5Q~Y62LLHHNJz}HKM"kl"wĄ*\z{>}&<Tن_믑yƍk %guϳ\g6iy:u2`@ܦϏ[6k0KIN>^>L9Vܫe*Ԙ{QCn@IIIP2_Λw\0OO9)&@ PhxP>c|L M /8&5Um;+箁}D. 8l⒒ѓ'GҫWe&#==jy˖ ء,em8˹k 춒_/X0+>dCnnd،ޕF.1{vBBB$ ^5hP *O8oبG'Y΍:yk7l(?9i۪yJ/[22ܷozz:)){r߶mf8 ۿyQ{o-rݺ.~<*,*z9vnoty(*驽{'^-[F/e&b,@ի[^{Ek7l۽=v9fE ;''Ɔ̮4h\mk\]zQӲYoOjݢEd'2zt;iS#r"|n]9Qu{W^ %ŻE3سFpٲ^hѸu #O3gc)g'~N`pJn_uAzwVZqxp8yZÇowHZj?Gw<̖»/<)1 Ysȣ&Mv(a;e9?x 7T#dyr޼^zpQVӧo=4^*tg%%G7<~83{|p̤čyy,>kKS._lc/xՠA;8񗘐ׁ|m]p;n7 1B[eX .^\Μr-+~G3xW#73;,!!iϼmO?׬ IDATyo0|;5hP8w|02;q?of?heIIkcrX& yN1jG}v/L*ٻUk碞~9o^EV!fұ;~Z&f̞#$'/n:;;F>į6EjNq~Wx\Կeg]˖͘=;h…}.ua7hP\R*;{…ӿ`m|꫏?찪n޽ߜ1#h]V4lX7%$}/k,%RT\|%Yb5ÛjddĻ^!'|1/Op[W6HK;c/;C:v,sԲYzܛo۪y¢N\?s_~)+WխޢI͛wwߣ9ݫ߀;W|I["$'%ҫU֭Z:Dջ['O=yӯ(+k=ʂp86`_W`3سd\aZYdZqcI(T&lVnݪ ).))֯W/-5j#lZ&LS'#=-[ s~ /\bE >==vtQ#4m0)1q>"F 6o.$7;G4s mռyǽ>k#7L^?:K>9trƱN~j|DF :))uRRF9eeM˙tU^ݺ7޻U.{Aa5#tٳg/\h_֬Yyoꦤ4HKkѼQV͛٢ޭ[ߦM;H{ҟzŻ4_&:edY&,% :*7?tm23 \&d̘?t>;P($6J B[L@M*ux vnTfl=[W*ޱc$vtāƣ%q'^պvf}[lYwn`0]?4ʄ pg,@-9e&T-O=K'm23vY& P[W:i٬ٙvܦϚ5˄W{nRbb\PX& P[NJJ]W>/hOKjdj3J1U 횽paaV[W&KRSR %)gzUnT:ܯM'!!!^*\s_6mB2:aǥe̯6${po=XFU }̶,`wWTX4qĄoJww-$$$qS>g+˄o߭}\n.^|᭷F_<\Rbbj=dحM0uҕt|mէz_}Up-, f}}[x 5t /q)L״~1ٻLT(zg˄|a\vY& n-~Ywu/Nu`;,`w 2БCR*") Y\:9wާJB.W2{Uv~n~֢p8P7n4jШy̶{uث-v7_-X2w99[ ) 6lw;XQnYo~47;7 5Hl١[:8AZ (ev;Ɣ Z]C%l_L"oc^JMOݯ~{trxp`VͪO|mwߛޜOBr {v>N: U6?7~EwLi˦-?ˣ^e/QG%$uQx~]Z @ ?"E>}(z¡x OF];*뇬L^<>{٨F}':]|ւeŒ& 6lg>~-x-R8 SэIMO=syi1di'2w ?>ꞋYtv3/?.ZevN==KKծEט5+^M/= "3k@%w/WKjRS'ūE8sǘ2E(EWG@Y.]>}JJJ*uo82kaU<{SOmm]R 2~ ?wsO|?pQr#}$IFx5s_/t;[#86T9pdum>~a&߭\rx7k,򴸨xu . k6X&}h$۳O3/;M{XhY>)3uEwl^Uod{w-s.ذiy懯?8^w':Mba,2БC`cPԤFsr'%'޷u}[:W -?ĤĊ<=6mӏ&eNuܳsEVn^S঳oZ5aӆxG="(Nr{<~}[Wfsn0wB *[7 W,Yqϟ?aF9WѴt27ލ:;5-jMba,oK't 8`SצG>:R`m۶o;!'4m4e9|ح*U}˿YKdtO~Y{%G]r2G+y?oz榪۲i^_{rS/ﲨ{xlzk<]zk_4bP՚"!cLdȡ`0Ʊ ^XLȮ$[ۭX_̓S|nM=[61뇬2G?^ݴeӪL&@ LxJ^7V&\a/8fʔGz)&MV&'~ oΘQXTz>ᄿ]p~m]`ז-~V:}{֫ZI ?p8\-kN/R?%IĤs>'2{ gG+uqg*ܧ_}K/mvqgp3^M@\yeYqck;vN{̘mm  x 2}zʔ\~y.$7o3eJκwP(Lnsǘ2Ʌ7_P=/_UZ? `}6sNuƔUxgp +=#]" W \w_/,.)ןqBuHOW9t荣Fm*(Ƚ9yy_{ފKSv=眩^닊o5jM7m-,LKM=2Yvq?~c :;f15a}ϹyղUV$4{4lR>82-TaKTI瞝K/@M\#O^-֯s/>fΟ_E9U{??6o+/Oo, 5jTGDL]ܳw>[&D]Z5 78#?鬛>yzfe6GQV\^]#ÒE,´`0X{u+jUTbBB:w>س]٢E;sctFzz Xuׯ„⒒ oү>5yu>Z۶=CcV"OL>ۓ*\m~G?*ٿ :z2aY(< >ᇓ>lиqgAzdj\e3X:i|elG͗[rerܫe|݊u5E}j޴eXnUٱyA~AM43o8Z]i/}^8诳UKSNlk=cڝ|G9~O89BA}3,]坴}{-gWl_tQ#puQ'"!`GyBPOJ!o _)%kaxzWGZ{m̋7h @.?7?U'NԼ&k@͘Oxc6&%9?9Kj̔)?=cLxƱF} efo?xƍ#T0!2?c w]&ܾ=ףS<,`״bɊwǿ[:lyy''31aU|K?mӞywPpka<)# F`k,c++!1Snڮ~zUCMHz/kШ t /皦 tEQ^]<[ ˾򙘐0SDH5z'? ^PkzB%y;/9%zv9\: IFo{gpo.Q )uSjխ&332_[]vȐ&2:yƍ۽?v{{i JP>dZsoNjvSk>]_׽QF1a IDATFU#N1;]5֊XƆ᢭Ey:-QB "M2|h~_yeGӦQ^˯@gɉ2`@E]FEVTM`4q%%!I}鎓?y+~'Ϻ⬦-V3oze[kUsc9os(2s(5`}II czߩݽ7XMi޸qdXX-xsƌpM{vR Y7ǼY:iܢqūO HHL8#of`=[ˣ%$$t9˕]ڲF?Gmy'kRMv=5_te,cW=d5Q6;;{fU䤤*<+;'g?G=:v4jg,US=tҰiӇ>ےv߸wki/MVUӮcsGO,c++뇬01)q{d ٔB7$޽ `c6;WLmIR @u-A#ƫvO὇/Esf4ɈKۻuRl-Z&b;aBB¶v .@$d Ϙ= /Ytʕǻ(+K{QML]Ƶ_ZAg^vfTPzuG<>?\yrX&&'Ыu洙e9OH̢E3/;0t!AZUZVIqQqԕݎVc lܛo4u7?.5$kժyƍk @T .flR:9sկ>Qnٴ%2ԴȰ$Q:WdX\T6o 2)ѬMn̏<Ț5oUv%Ӧo|$n5HfT;dEϝI8+^}*% MSӣlGQ踳KLJ'>:1 UvZ8~ї9Ogވ 3dkԤp8~I ]GIII5_ oj`ң/Y+Tam;UVmX!2owX6ln\1Q5nǼ?2yK}ݾԴOtΧs"'lOB?8oO-!od'}sJZd-rˤ˾WZzz٧]VM4ȨWnRbb0 tŊ*Cl-, @\X& /'_.?nۜۖXcLdخcbZG="Å_/ ñw.㼿L6y3ʌ[Njj;ѣ#!'7&'%|xiܠA| 5 ;;%LxhB$nʠg-ow=$IjZj0ܜ9{ev¬W38gu?aԣq@ Mfm45=5 n-VoXte@ 6glq>yTh?8m=pFu9?i邥̼;븊`3drW|_e4tX6@`岕s>9o\{v촞}㍜2a F]}0Kxi٬Y;4t<㙗9_ʹf/8k-_hy]􏋆cz={/6w}g".ӆc iM0[R۷oFzz͗}3j`;DJw- W؝{L'Nr5μ+{֫nՏeHn{ᶋo89r2d7Ncnr;N#ût2qy}3ͫC,)n,6fXO: OiԼQ83F_IO+-9%eXrJu-_~_ZRVmqg7aބ=Zd-?NrRoܳدƞr)RvN+֮-*.֏j{ --2%PeNXx**R}ϗN0&;u }1oÚ ~G:nGwՊHLJvS2^:kue'$%Wk M2\ ~{hE~Z2dwq.O}*rn)ܲiKjzjjz-n3ػ~3rnrx <3n3HLH8G)ӧn̙ٳ>5kmUgw\poGx7 =r䩽{?ظ*)ou;|7v䝻vN`eF3v<;###[.`VLHx_*+m\4uȳz_uY o<%M\27E.g_Lxr ŭߴiNդIZxeB"^$֭{_n}\ڕ/΅;}l0y֬?!{~衛n-@L&sWȾ#U.)1[n碮ߴiQG>y1G1l̘lA7nN8l\܂K/] !wQz>8󣮮۸ѣsnݮ9@ L(Dd(aƿ7~{ONZ4QW\1}޼";k͆ #>83++K/]U/-?zw;%%еkn ;wv: :AVfֈ'Gd^u1 p#Kqo/^zioX]{Zep?[f^oݣ+? [ Pz:~2Mj<2e!9֦斪*s5w_uUń=Ìs-] JHl hմ7޸G>=7ת^}C]ر㞉2YL0u8p8S& @5ن=0>O~eW?hޒ%Q B[S=r VTaw?/ܕmZ;䬬P(s VĪU#P|^4?yw#GZp_լVK.kMKOq0I(W.|PBc/Q{+7@ɦIAk(R~[X%) f,xva|Opڴ~SZ"{aştBGƅ.c^aPdwӋ+WN5k7nܹkWńzklҤ]V)x;%es-ڼukʕ5jtQGq% ÿ)Sf.Xl͚-۶KQe&isǗ-)/qN%\"q'U& pP9da2٢Bd(͔tR& @Ί4TDǐ1ppOL099< E}2YQR>oר[سK1*;]))RdCE5@ nse>=rXt}d8d_*(U& PP'Lu2YQ}d M*(LE'LE2YW}dD*(*'LV2YU(}d\d / @)LN.M@ {ePԔdJe%@\ k`rr^o e%2Y@,@ @c:LP& p8j#2Y8i V|/ XOX`dJe%2Y@,@ LP& P((dJe%2Y@,@ LP& P((b(pV8\&u”u|rqes))봹s-Y|u7nݾ}Wff (_b*UUIz5j}6kV|Xٖ ǗSGپcGVVVa|ٲe$Olmݾ=r[n)P$˿6?/]֓Ov!ֹ wԩQ>8)sj]-smii6okժ_cϰL(tܑG^tuV\a9E+Vd{vIJgrO3gfwEyن>]!6oZ3֯aw۴h[Hdf'OuJ+W'`Xgn/ү|}2ffeM5kdܵ돿㯿;~| xaǎ?]V`08P( 9s|~1: vYYY䫯: á~ئy=z\tP(֡=e@!Xf=/j_MmX|.C)o:PP?x EģubL,A%XJp8$1c~mii9_Y&͆͟3d&%&*im IDAT{rnmѸ1͚5SZeʔٹkW֭k6lkժyKMI)@ `0ky/wm g6(d6d$ @aٖv㏿7v.HR:tҩmԩQ8Al͚^|1RʕGvX~Y&'ffeMLZҦNX1!ִ~Q}N:aݺٖ/qS$1fcj9z-]: >y$UL<8 iE?>۶1jԕ=դI{3v{6,EXe) 3+dI1͚}=hPĘnvOFΟzkXti-`as/6.:ȍ!|ن۶y1[իTt}{Ϸ㮻Vݚ5cbLȃdž7eJaF \ZD"&2v|oy=? {[ߑC)$8&Kd!cM)fE~QBY&ʔyǶ1z„֭wo2ebuqƌ'6^g/IlVRL(O۬YȘD)r%-=G>p8|ģ6_?&Hw+$@PyR 6k˜D)r߈V6sv8@tYYY`0XIд~wܑms׮[8(oٚ5ϼFa뮘RƋ.j۲e~obLؿLJ K6|{ډ1V(p睑ޕYy(Nq薮^fVR/IkWAv+S&LUm?ٳ.Yl͚u7nݾ}]@l\\ń*ըZN͚M;Ic5;a2P*K6myklٕ TxX6o~j6m[@/&N_g/Zb-۷j+7>6͛wn߾C۶>)~CZ5m .Zbڵl޺uGFF8. ULHUzCi٤I֭OKNN(W.%TfV3駩g/^r֭PFժ5hоuKzjJ[?/_qcfVVխ{lgkw 'd{ϔ,/UM3g…._l7nܴe;p(P|jSE'u_bB<\lys/^b5kmڴy˖;w?+UP;1!jڴ}֧{l!>4r!9Crwӧ=\t{c^չsd &1pԨ/r 1SԦΙsb )4i?wM"ڶlyQG}۶q+֮RJjj *%$f=ת^=OPRm{u̘W?hޒ%Q/ؘts@ Pfn]zU=-- 2vnڲ%fWR1˖=9|_jlڲU6jp{*e\yx׮n1j1͚m۾/iܔ)֭ʔk'L*TSwo\^mi;vk5j[8VՉFgd =Zzu /jҤG 9䓟#6ǡQeee}/N=m8oɒ'N7rd*U.x[k {w=\a֭'mhŊ~#G˴}RRS\e~q"xOg-\y56矣'LxxCyŽ.4[k}3uj#:5GuҧgϺ5k^-_8%3+ݯ~lK?a'ロXʭ]vy>ת^}֭<=-=})_ɓXÕSSW >@ _;>y-۶ku[ڶ|(dfe ~Gݸjė-{3U7<>q6O_lܮ'_}#rh[Φ]Vo۴~@ pɾ?nܛ>v䨕T.>;xyCsowU\9k&Or){~.[Cʕ{.=물4uΜ[>o^*?н{ٸ>pԨ2s۷rWff#xܿ]&;o|ek鮚ժp=Wu\US Isz*N fm=X1?Mgv?NcO>]l5nʔ?l˱`@>d@ΝϾ1W\1sBVjmپoWr$̞+駂g>ǂ4; xd6,`Om8;|xǞ=s$/U~#GԽ{^dG x$CTU*]q7Ȑ!|WZzu=vѽI6ߴGѧOzFFA28c1W\&@ 15=ժ}v|no V8뿙:5PW4vU5Eg߾_sAd}ۅj_ʄBݺtg4(5?͜yׯٰ!;DURtw/e6mr7䓂lgu|~5iYܲnƼo}W?V;4pԨk}4/涴.w̙Ϋ_>[NqORԧ\~ّfG9:pJIMZsyAŞZv{̌Z.>m-7Sf@ڎlYaò5k,[15xV[wzϞ׮N>昦WLHHXf͔ٳ.^U=ᨣ,/_?͜uL(cm֬vbbVV֪u͛7mܽLz۵hnԪբqFXBp8y֥W2g+u׭O?餓*&$Eⷻ2AC> ;ү߄CzcVVu=G]Ri-4ZRzF_V8cƾ>..[v]w8|x|ٲy/_ן޳g²--ӭN=;jݚ5OKNږeMwJJԋ4gyaܹ{ᄎS+UpǷjڴbBBͿ-Xïfܹ57ziW_sZMJL>>j;viSfnתU~W6&%&>~ݺt)<O>=s x㍇>OrPr3jl&M:tRr )Wִ%+W2z`su#DȦMzy' gfe}_~yҥw 338saӖ-ZzzR ƛ/$׵M[ _{mƍ{R2%.^|G]_zYqe=_vK;))sV Ӻrʩmԯ];n,=hў;22v[aEZի_toR5UzM8_8pZ+?9Bzl/&NT&{R& 7SF;m[ISջ~./8jTɮXh<̑}P#[0SFNtI@ cΟB8y֫~8r>wII}{啩sDCswuWwUӦo=䕝;_ѻwmVצ\c_Ҿn.ڡC쳋V6k rk_ZqD>*fk6l" }gpBԻUoٷo-<yW^yg( ۵jݰaᇑ0!e_{瞻/<{Ѣlë:wG~;QB=6Sŧޢq֭Ojݺ[7vldMgyǶmϟ`0ؾuok4(v%ԨZ51rPR;}vyqƃݻm22ŋW&¨QQ7^tKW.;L(taǎN:'|/"/>>痮^9oިѧ֠ARʕvoac_fVV;j9'n~*W\wyqڨۼQ;ΝTߋ[4nM GUIJ.+++#|oE/[Ӓsyy'6Ewl۶yFy VR]__չ~w8}Cts[ZZ&MuV;>∜ ^fٴiSNo7o9}5Qz ;|#C<{?ٱmk2Р:{-2wn8nm7oӼV}aF] }$zk }!j9ӭN;7rI3gmz9?>饜~`=@d_3s$#?[Q}X>Hչ4oɒݸ^vKݗ,\{lGFg[?:vraY#?$r޼Q^}V9[RW~;22rs1c"K@SNpesaݺ_ wĈƳ[ ;vK`_]7eO!ҟ˖&VMZ4nܢq}Fu}u[]eF| ׿ g9$[\2w^ye|Ӗ-6n,xەwUnUlM{_ E.-X,>1lʿ6gɹIvm˖7"1rIҳΚ1jT7Ɇ ع3ޛ7m(CգՃ繌r bM{\vVr.O<`gd<>lXYÆ`k`^|1M"KWN/CjժSF?(se@ts/ن 5>䐘)]9<Ê? ,#5hM7i>9_bǍgʊh--i_<{ /W|Q+׭+$E~RO#Gv_Yu\{󍩩wA+֮}w#'1Z#jTP#s-Z|y.7ɽu{_J*L:~ʔlø2e>P\W&VrG?a\ 2C xgϦ2C 8R^ر}v;Ook;'(\y֮Uot>:rlPGٴip…E}.1Ln_E֯u#==raO+GGٳ\||^w{W8nGί=:5ju^^Zaݺ`0rrp}שQۡC5lm>|Ӗ-wع3۰UӦ~znnSNP|~o6(_=rxeιXq9D7m2s;l̘ab*7_rI2->u@iG~qyNeWv|cjj񇉹Fr*=4r`Ң>ut͚uҬrŊWnjiתU)Ͳ7.r^FK:+ٴYڍ2%W][?))?)KOa[ZZ .(0T&elh%Tń/_~ ry}zsgd\tQ^Z䑑ձSfͺk>i 2jڴjJn62` IDAT{vr9ܘ#痟sNԮރۊk93r(0WRʽ/EX^ߢ/]%&@tk֯֩Yf6GG şԚ1u"痜yfٸyeQ_N KO~!rXvZ,0+3|fr}bժQ;wLIIM_Wkl$r8k|l);sr۸^9f_m#O:F B{5~7 }=络rJ1')+-A1ujԈFօ2Y 6ET)$ٙEsg5iL_:tI޳sQO=Kp8<~ʔyǶms_y@)s+C<ݒP\4Zpz$%cCjՊ.\<[E^{Ǘ-[(E8MP(?j׎vڴa0l1PE}5kРnɬ?]y×"Q;9(ۺ}{R ş4;}}@`)UW֭۫+`yO<|ؤ^}VU @AnD@EQuսwѢVV-Z8jq-%dHȺ?{Bnx9{=ܳAYreaإ{]|s1:_պ6m]ܰysAaaGǎ{4B4/?XgA}Vạ+ky^;7o΍.ԧO'a=oOB4:8Io/oo.vc>v챭RS9aCFFtSzzZ,+Lqʕ%%·Ŭ3ΓT^i8?nذ)/okAA8y5Y! Vuܭ[X]թS6*vƮSq…p8ޫs5l+wƍ%]TmzmkAU뽻v0DlܸzܭͭjEIi\~-۶9זƍۊ? q ` o D"Θ׍uG\0|C$'mȽbjH1֮֡C5\^=WY)S={ւ 33$y /YkW5kbsrs-Z,@ 0_׿jᦼp8 bRgDgNUˈ:kI*~Ӧ SN9s?_'M˳;eʗ~;k+VDeż@$^@,[BH }w 8Ov⒒&O~oM>l't!$69U-%ss[WszZZYo_Fl+*z^7nʜ9=KzuUZU7u۶춇w4f/\X7TPXشIKk7nٶU^(UGFi8<^׿>1Cim|Kڞeb=Œ? q=(G"Hm ZHI!Wf}k}ԶUGs:9V[ b[4kVuۺ6EBpg[W4X1{BQqq|n:y4 -o)){r:ܣѴi')#4gYY%Ͻ֣/.'gX/~@$ Pߵl$Tֺ-B~'%זggꪫyDl-[yiii|n:w4 NnxSw Iva;$ffz7?D=pڔ]y1^$YjD0D"=4\iizWڧM{\ߘ0ayܱ~_zÆB}!fJ\Y[&͜yw斷AۯϞ]'=-Ujjƍ%$l_vkwVFQ9 \WTN\x($;_씛o^]m[үzu!uԦM$$$l./Ȉ۴{/<{u0xuu}ߙ<. ^.źg/ L@-ꔞ]\vm'ZvE]tъ|O'_}]e>Uٰyw_9qs^PXX۶Ŭ$'WsC3x(Dtƌo(zsz/~s?W̶#UjJl;g۴jU7ԶF!St5]xar6͛Ǭ{ydu-5Y3CyՏ䮏ؔw7|1 ԦM?Um~ɒ7:I6%9yԍ7^wu9;#;;b0Y!2sfm@,7@߳gtqƍ9.}ڴ쳯;u99NG}7mDnx9cdžBxY}ڴYeYUsv9:m[ AVbֳ׭ӵk<'D"W2U-X<دGB<`0X#@ө]6-[F,^aZ]y韾’w߽cn6o҉ӧyzW%+W]bEt1i-ZTmѥ}EqïocdkENbب ; cEO!V_ֱ.6y˖$.,S z'.Ȉػw!jcP}PXϣ Ԯ}tiӲetϔ*Hzv]titi%ToZPKVaIii8*H2' 56}wr&yq~A%4puWda9ãEEJܻ=4ݢE236޽9 4ÿ?lyv4md.]3+)-BÇ*\mʊ׭sW=q1{˜:u4*iNu~ݻWmmYu)f]xIb%>=v쒕+wւK7{nUkN:1۶]zӡW^.'34m$f_[}|ᦼziiiU_1Ws]vEŬ7^.Ԯě??^T\| quާkܥKJ6Y}/~Q:#Ll(燌J6pCAaaf(5en8mzi8\Wܮ4jhD"z+RIM31ky@|x!CsrsZA|֬j6lo]?CٹWOf̨f:i&w^rIth#MO<tqѣZS609묘KfμW~٘{#;lBSb>_~;'72_W1mݑٳN<찡Eܰ[fMGE7w^֭wx9`=wǗ~{襗nxw*5uaҋH$krn :tb$RNjbF+*.~/9GsלŋnX滼@ _#'7_W{bYYN-SLm-R y@|xvE?H$4(=TOg,_^W84'%so`7):{[.{ǎiseȓD">Ԥqol`n͚\zc„!]W_ i]fͯ_xȑ/s>[ܡO׮m[ߴ,)-`H$Χ9&s :TRg6{< >{݉E/_lЅ=qbHヒp!_<ܼ?nѢ73DLRBaPO|z?o˽ܹRAa>Ϙ7@.ZywoӲMԟ3fڵ}ڴN:^ژw뺜{A_yq?I%@i8<}&O~㏗ee}e'Z%e]sމ';y&D/qe{ w_vA}Tᶢ3g4onj-!-5 /|o^Zy/;&隷Z{f2g`۶JY76`GW_E/}kԷo⒒Nsi7wҤ'oeUlԦMgւeoOxbn}v䤤;>^z}vV&-۶,8mګ/̌٪w׮\uUG:hPk֔SOڅ't!ݶU2mEE֬Yj-Z4o;#151 +Фq~ԦMW]X/Smx&N3!wرLxni8fÆeYY23,^~Ӧ1>[^sb~>k ]׽c39!C۾uWp8kݺ˗p}7y֬ B0ۯ~5oҹK ޘ0>}F6tР{n%%W1k/o-(,ܭ1~,++z)'7Qӟ=mn+,lYQ~ir^yenw|:cFSUN!nb-.Z<;;z?~}{q#G>6ztuK.\8 ;7JHX3oٲy D6l|G:p8\X\sRp%]ta@yiG}G}t)oڔ(3sҥ,*{.>*)-^zwҤ>|ܯ_zZZ$Y~}9{v̻/hL7G6m`5So^z)sжm5;S y@|xvvСG:+;['NVAa{`S}tw\ߖ? :pםZG IDAT}v ؜W6iqm̟>w^rIVӟ'͜mĈ~=z4JHXbů;.xw3~L- gۣBvii͛6 Ey7o.D׊׽W_Xao?>&I3gn?w_ŋ23'<ܰ22e[˫Ӗz!MH@# F e>6Hl%?yvWelzZ'?_5uW9⨣vَ!ef\Ll v7w^ۄUk<;$=gmڤIMٰmj_z`>={N{~1d+s\97UX&^x9ļ(,*pK-X 7)=;f%%Yk.\|yFVֆ͛ûX7$'JFo۶|z)Iv}Gx`e6޼e˲22\.'FaUzl}S>*::?q3F)GmUm@ кEqO>ocԗ/Y2{­ tjC%vY^~Qf˗ee٭$KGuW~zyW,tiqIIՎ%W

]^U˂ݺ8'na^$'%C7[Yj_Q7ޘ\+\:bJlh:;N{Gl0qŻ^/Sq~䤤O# iʊ_Y ;ƻ? B/?}_;6JHKf}O?*ޥGcG)s);d#GV-0Y' ,yKʃ+S0uhʼ 3/wė'!X5 3~ww^rIHGܣQ?n ףiC^z}uC˲/1#CX7O?,++'77h֬>ܯsϚuUW)soMG" S~ Sl-بѡpСxbyۊk/_eN/]ڷ?C<ؓwoOfN:u՚5+ ug ]7?Ě>c ٭9]'ocvժpx̙N7]2yII{>l6xpX?W?LGǎ*t)}_|1yGL-5Է8lPQ\R׿f̛s;^u1/[Vx'6jԺܭ[{뭗ƍ[re ug}'GG_ۊy=eΜ}w_aCP(4oGu'5_y癱cعv!8sO8:$Hdy}z߱Szzn>~RW>`i[Ymt# ;ڷnÖ'^}q׭۹޳S=N.4i̓n!T2OaPc8PP5U -j2aW*/L6zi? 6Lmԩ'xciTbW^/~ je%'7wʕWg_qP0ܸq[رϞ[J%+W.\|56oޒ=QBBƍ7mڶUui߾]V{cfK~-ZnS^^aQQ͚ӦMΝ]S) /w#)t>teYY7oé͚ui߾w.+DXpmZ׽{5TcZ~e˖ggټe˶pQBBRbb͚mj6m:oߩ]˽ւ222֮~*)-M4nܢyvZuJO٩SUyrrsgdd^n]Nnւ폇Vii۷թSvj6 ?dd,^`۶vii{,/|2>sLWT-K,&=;;TTMdK@q@C&LJF!b{]Cӎ>hӲeG۞zVxy;keخ/^3|Zޤ a{9@}*TYyx~~iuWtɓ}?Ə :͝]ػw'f̛wGnj^|){RF=@@`0D: .ML=4xht= z 瞛|գɍ]q%<1]~HPn:c޼5%Ty}Ik:ٻN<W' |mڜケ[YnѢs﹧}|~! W?L^zvT+`-g}Ѭ bn Gp]^O,x?jL_P+̙5 Kp=54de^F$@ *[K|<ـ  _إ+V{=.`ԡ=zu{`agf2?yMW+#^~1{ 6KOKǣ!q=ץbPgR`˙- ־]L}啇qRRyl7i+9pUyߢ7:I]Z#GVo.¢W>$ًO9eoKmxѯ>p-j{jY9ZVML=4xht= F"uP}y塇/曯vڞh(ff~oLСm۳~S=򠃒*e5z!\1r*Ig{w,d*K,TFQ7Y|ŧ3f|dI8ւu99;{ݺǎ}zؤ{չs-Z4OIIHHVXaٳ.!#]g̣Bx tq'qI,%ǡn& B%5JH8vȐc [ .](3s=k{.`/zutxւ,nڶjb4ƍzvr@^[qy6ir=wήV3'OM5JJNJ3j{ PmPLLYqXoxO=Z,P5CH`0DjPWyOif6oٲ[wJO`=szP(4oA}upxъ,l+2W^~}NnnmpRbb&MZlٱm]اϑϞ`"LKBݺ֭#B=&LhTԎ55 <]& Pda0Yz@,@= L& Pda0Yz@,@= LhT^nъeM_+^@,5ɓ5LϞs|V@`ژS̙lYFv-p8 $'JMMOKܾ}vֿgzJIN@ D"N})pmrnݺ5s(B=z6`A9mkd;v?)~1V@<dʫ}o¾p%K_n>?qGM{OgxwҤ2n:`/&LjӦu睹[H6HdfL9[n),*A; zwQ^lV0O׮[7NL,.)ݺu͆ W/\D<00YH$rCm͍^:n> B힗?c޼/ӿ=9,0Y٧3f)6mdQ#:j7OI9vȐc yફ6}Wo~G_}ga֤Y%e۾mٶeu0Y߾rt;#wU/>srs,_^@twmټL;5B=os/e|dw*5L_~] ? iYKO>̅kVټam@  %7MnѺEZzZzvҧKz#qbmO {aP--Sir0'e;ϿhWϛ6oGQbzAGԲMx^h/oؤY[жC=h۱m0sā0Y%+WtgP(T+@%g<DzeUaߒ3ϟ1ͧ S.='ؼa7&VC:#N9ӎj֢YM OdZ6n\RZZZ+@ܸqtI"PcD*WTن7L|c7&&%'yQ~k{(#L%3ke]D"[ 6n\\R(!e-5 #w֜ܒFZ6ma"'78 6mҤu|Ѵ6o`ƍZHNJ*)-ݰyH$Ժe=YRZqH$<%%')lݔ [5o޲y8px]NΖPmVRR-!LjO]P=W>o^b.]ڥV SL>}Ƽy?ddnݺj0Ҿ޽8pQGѣ:3g[,+LyJ޽wu99e[չs̙3m/^z-[7kӲen݆?Ï9 1o^aqqbyw'M)]jUi8jZjjΝu~P߾u}6+/E^[iҷ[!'?9êf:m⒒2~=zVmEE̟]ү_JhQ@:kڍߘ0a_N;wz8c=ݚf-X:cƜŋ v;kw؀#:cm}Cfc& 3)LyuٱE Ey6ސ ?sTH$ދ}=G>rЃj{J F"ڞe|(@WqښdO;'LZx_R]C-,*{=9f̒++}N+N?C*ւzcWYS/?\2}޼C.$wqQe -"")`cwwcڮk׮(vb b7( 4(HwO3{g3 0}9 \ u_8ѽ];luڵ%E;wL…oSc+EikgxɃ)*(H.Aqҥ;w[ <{Vҥ֮Ts̘WKXJLwuh(ǓhiNTN JA{^+>}f'ʕ &L>}^^?3:eРӦ4m*^LK"oK l9l޽&ƕN4}zPXx+\IxqTw/^6xL= xPikh9s[7o \tUUo {n IDAT"o6xLև,k@&] P@8\eMh1!;l'eUq ƍ3L>? /X6n8=[K~ۻ]2\꙰YgQNNJl6\8/+/+5 o7[K?c=p~jq"?,rqpɾ =hҤZi_H[Ȗcm\9ys[;;q;v$13e͚C/ݴ\&mԌ[~[&a8qI%eesB#Ѿ}aÎg]I@>) );޽˰ :/}?clrz c˖߾I4č}}4iyꪪ^*ӾK+,tbZVִ?~TBCEDKvON-[*z*IB##{GJff՗1klY% >>O4I jWWmY[0IpEvI{D\$ o>}o))&B-կ4aڒw RQKOLdI s[ouAF=Tb8:[8nmR}oƆ'|'ǭJyPWN[} (vu^mH;S^ѱf vkӴ=lmArz~~YYeC$oO^˗v@WHM}yyԃ9\U>\H+Nr~R͛SK]]㒒nK*,s5p?EDz2)-vʑWu45-\(hj:hheHGSf'FF J;۷7F-~ۥ0enٳ;Ϟ5j?Gf))*D}[O=>37AgZUg5w^z%{ ]մ dЎ6׷oV뗖E%&>}Q`ppTbb?g&TUT@RwIUuա3J3VCSKi'-T00Y6di_W>ЎôiS >1%E"IiiWM;…;fª=cOOc}}蓠KF-֞1l~LJ\~Q7H%S-ͧOԺM>۶Qd)ٓtR/foݚɠwip׮Oqcm5fIMuu e؛ʧz֭t?H_f+&\\&>gܴvc˖/_mpjl9sc37^ݹS.Q442r#5#~fdܿTt},?7{xP;Z7=jX_?g̠Mq552r_-󽪂z P r HE ZiX'V7n֘:T[prͷdd#GѥVxǮ]9r޽iYY2ذ[{:AaFAQm4]}mSWU?ݝi#x{3o5q"xkpVO/=pOʢ2w.?~&Rgo)jj֬?uD=QH66&ӧϕ;hsx^%6V]֪aU8ySzNeXNK۰0;u;ܢ-xzN8vU°&'/>uZK/z*S00Yhie̙+-+|5eܐ/c׮}&54YikffZ96)a==f]غ6"a!]WrQl-,vF_(j<>;o827?.CYaѤI `ʕ;߽[:kŬ#I7SZS/ߞ;7{hV''zZPSkm5aݻ9ZRr@8|ܦiӐ "*̗b/_nmnNZ\ZdWdP7:4od5[fj˖Pս/@'%~K>RRbb#ssGAnfnbdbLXLlxlr\rqAq U:̵ezvZv/RR#ύؤ܂9- OK RRTR}=~'js w}[>}S. ߯ujɓzE*> iie%Rw_C4ox~fdxݿO;D `# N_/jPE:v͚Qc|S23iǶvv2ӓ||<?hPX^Sc?{U&w)7ZY[?9rÔt^όffsy<j}P׮#zd,fݗ/+,bU&D4th6<3%Ġa([C#?Fr9܊vnmMkL/E%Y__Z\߆Gė/Zvru:k-D="*'='6|R Kj:wŚtҚkC/=9 e|=%1zb̾cGN;uAIYI_ڏ>~y%&,&)&)G vڲsKR,Ȓ;?[PȑqBp^jV9o8RW۱/I3 =rjQI hif6qhjb :&ۻ}V֟I׮h)jDAL *Im8.KsW5$j #Fl;szV<||yA"M5T^ml=/I4eYQC]&H?mFR] y kR~YX<=zaovhLΐo$S+Og0ձe ˍMi <]T[]UP$_sd;e}YsGVv߱=$xw^a^r2rB< 9Ə5BYg$%5T\oŕ_ƩB*^s}kwm'?讥O2 a~^{!*R_%˗/W^kmJ*E&F&~{Mh\x3cv!M6FfF > _LÇn;kao1kì"R@ W׮9~ٳEEYo^/-sN:fK㩛7Iڹukjlpxx|>mӆ Qܺ55LIb-<͛Iw`.u>~;Rqɔ)N 8{5E%%w_4/H.T5jtÃylA@KEQGdQ3GPWUr Xwo6eˏ0Y#=2L%¢qc:3*Fcm2ܽ&ƪ(IT,Ë޶{-_(Ѭ;sbÉ .yJ'&&-hS*\_ϳrJ9M}=\ߝvpY,)46|)q=¥Em'W v=R(("$UV={s SSiee{xp.]&&'= ڳpD{E^7i of&{rɛof̐t _MZ 4%!@!ͯ@TTΚs277eJOJKiS3B##%+>99(,ZޣKa۴)Ri*nPXXbJڰőns Ɩ~"I:?+-+K.]54.l|~oH H6ǯ|9Lhbm7P^d[O:u&p7Ms欚1y}? :9I.AabhH[KJjmc#f=hHMuu'[[i00 ҳK`PUV;v얓'IC/6MIL̼ͭ҉?}HK<ffqII^bj`uf88T?Rm^gܡ_}`դ֮%Si#E6[pĔL.R}70Yj3p#ks{]~p߆E**MM r2r']e[ui^k_II{OIyv{~Otaȧ18$F& W PG3nBH&ۺkk_k7?p\r߻N6\-@0Yj+Mx@\o͡C?zoۦd}(Ϧ&&zRtJ[OJK&{jT_PSo#=1o힞.W~ɓIV:իepšy:U:W[;jU\1:lI # uH3SjZ~;lz=$յgVw&khjh`BȷDKO+?;T nA]C]{{KKfl64GZȀSi'= mx7O&MM훵hָYc}c}u-u RRퟟC̗5rZys!VM߲3>|s 矏fKjդt}*((Ӳm473qYY-X[L & xRJdKʎ\J*.2IںCQȄ ¸<^\RRlRRRZZV^^AQ>STuBϟrN0,MMUKHuQ11(cbhUkPcD;iW=;Uutk+UuUE% vy:ccZKO8StfJu"#:0M\C~Vz>mSR}A+nwHYEukkj16,V|;wbQ~Qh6b&FObX,V>L._mk|@abf񸼻w-W\PLrJm۽mI*-.,>翞|x\ލc7o_mIQ-n6<|C."bVR~<>?wj=zH!rn|1R23󋊨+O +Ei0ٷၟ?wjJDHṍ dĔjHOOMEtQ,7OMElϟ?}D5>=? h*cf7m8=5*6I4y(5 74j,HMPwEq9Wk97ߜxetj+M̀7${5aф][@ cGX`R/}_2 q%i0bVZ1j(Q,usOS7z};Y<ډ.L6<΢LɌG;*>L6*Wpi!zɬ5L0PSGS\stIK&ҊI򬂢Ym>4]IQzMȁn"Zwf뙋{..]]V$jjVRQ6s؄E,,.>{lKuQG"LD!RLC,@-a^& IDATU u؋w;}zbJx:-(A_Q ~fa9\nM!pv~~6l ۼ0*1#'ZldVHή YN~W]gmRChO UqFzz0Y %Q::5 CCj,Ts/-[MZ'*O 3[\=xm4%BNF+&MM֟]߮g;ґ,ȋ#+|H]K&;ff]{vh呕U jj^wňo! ]wϝ=YZZ9<_-6ˢ"U bG7;ǣUQEEEzY ]ɮ3g.ݻ7'0 l6[ͮױDC)@-q8U\ UQRNbXK .>k۽mGfe/qk3\N_3t~58e .&_\~N _j]KOk[-h"ڡ .֭ab.< /WuғSE>,*:ЦUQA5>J@̊ 2S2oSށ0?_$5{u5lu4A*(MD~ZJLxd;B9%#T6 b-(,vTYI3f\غgy^y`npkabXj:EE  Œ**b^459wh:#m'O>W];6eUFiӽ )pJ+y c%E4p'.dkajeE6.ɆT))*IIH]<%!%)&堩4Ԅ)L^ԑ*mғ5iD-TIKOtҖ+-f~-@.464ݷyʔ2/qq-h0'BEV~;Am()ܢ ت WI "bv^w3tǥK_S5uм9Ui{/,.f-Q+h ?~'C9͗N2_?m oОLNQtf  Co"'@ձ7߬4%6)6iӴMv^XwQ~XMkەTKSKK]l#n-b7fܝ:q蹣IuEbcM-Mw~_vgPS6)Ze Y^կ+&ģO-+)M喪jq!*qiIi4 & /ZZY-4i׹sԡ_ ֦Y5c O_Gi5ֆ<И5j>//R}ѣY,VE%'?/ennm#"TV0qUV^ވKIJ.\4i}fA}"NQ]*.@ƂUjkIK&pJ _=G\P&=K]KM]M:Ost ?@Āzk.j>i@jUK+6:ԕ2)Lֹs\x)L%N'>"^bl_ɇQ _PliClşgj] ,0)l_*032'drn /VD\ )_bJkKWFŔL+:$+'*Xd '̏q t98?֤ UͭNSUV%dj招/^pFjJI-'=L&/ܵФDSTTh.j]kk֖Z&̪ۚ YHK']C]'W'cbbs#If ZL[Q! > !L@451 EMQRTlnfFGɲʘzrlDԺŋx>>IzMӦ"Ϗ]uH3b~8}gɒ;|?y&)آN0R23i-q~2dK-f斖|3 =8x0͊?,ֻo>s/J_=n<~1ԑt [A#?D[vj)j;8RidښT #aۅ ǎOԹQQ/ M L Dv_յkJ)v K3o "mb6vvԄ/EE5t3v}|#zpx8dʔ~~o 97~510ؿ9C|iai)jөES###==IR4P\Z*,y6O˰433vlS[T,,aӴ'sb5)NѺ~~Z4nL[MJFjjbR-4t-; K?$=E<_vI)*J0٬uuF>]]^zutQ/ڳzZTjQIYIָ}hq!UJʒbm̅!B*V`֦NN0YQQ/[}M[7֗t)>ǀ r$^= zsW#{4:(bjzSin433eP.Ȧ'fw*m9xdj켼_nWSQ3v535o|)EkϟݯcG)V+,o VR%'|>m/Wй3mt |ڹXPSt}OH~(jqqΜZG2PP{ buB腍7K-;QK-V񛵞}mNFhݚ)W :w(/OS!%;ۋJONt4Ԙ/sbÉ+"$!'~kAƃV\y؍/qH@bC7cv/P"uQl6mgOswO㓓e\e\۶552OfXoB&bQIɛ7/^$g f#Fcz3sso>.jčԺ=gOstɰ?32-\xWXXZVV]ȋ]P'oܐ.2۷ ?R{f+iS4)-/f~|訨@yJ/JV^e d4:3 [=miw^>OU\XL-kWqYQ+lרI#-=-R1.#Ć/֥Kߝ&)|}U%3C=yYy;`?kEH {N܌MjN R~gѸU&'=Zxe\eY#GR6䟲}^^?&O,F3 {aéݻ닟I-0:-+߼yqIILg<DI rKK]}\~OJ@ z$#*N{1Kax6LI ިo+M:dm絧O\*?~*sϯڐL*x•aݻZXHWWj=42-URVxnڡST:؀>}صk΍MJ6kV8Y9NosQ9.>xp%MІDQY+$ZW>Pڶuqpt:)dO?a6 ޠD`A@hLf$ա_'"PHŦ,œ+eCf[[*1a\/ yRw%V[ :: )Lٞ.515/+Z~BUU; 4kì׶pϻeJ_=/|WW:2~u؍EedҎk{vaׯJ023(@C'fS zh׮D{j*0wJ窫n3vh퇯\hb4ڡsN0ӛ:xp-usF̡/e{~"`_C};vޮ]+X7iB[?zӷoLE{7Ȅf;e%%kssj=6)̭[LV-(iӔ5kKKUmfݻ7ǖ-S9%$۶vhܹLVpܙ6q8fO^f>Ifu{J>~qt_vm?yM 4 [qWV_E;[QrBU:IIaI-.,kmlHo<=bȩ0OEmҢg#9QUK,-̣6kl8螶ml L eб\u֎9;h/u=ۮnԮC6wlcV@4pe r' 򬨤䀏QͻqIYt-'O|:4gfL5jTVh}+SRkõ393l-,2gªUmܘ%]y}}w?/tyhd U<-fIXI,_~Mꐒ˙a֨/>~:# }m#.˫(j߲ɎrOEv~,f"}~h1rnŴѥ=mĠw܂z8;3im/$d#Wڏp}:vdc(!~TO׭}|Y@]ȉіZ//aN@Zȩb{Nz0YNGGFW v!L KwE&kl/cǞ)*):vt3+3%6تoOԟ[Z745x: :@VCmU' 㠠AA:cۧOUMLIYkϩC[c}~˖v'QGo }Ĉ;4odԬǁ_PPTɩ9ukי38W>;v윱c1YGj7on|͛Ҳ 3%ZZYIPM*]? ON&Ӳ۶ H;Qo䖹sK,kP׮ܡ۱U&l6{JJ@hhD\}˖ik3Qn>իzQIIٳΚ5o8#5G^^I__{0>UNX^d޶mԡׯ=z\ƍoz XA] =S{GGjd…i˭3gu+:൳f vu-6?FFz?xp͜|۽];o,.@P'dX,?@JMLy8hԤXRT/̷#f|blxA!Bνɗ--t tr3s+*ApJ9_na<./N޺kkUuGaUؿt?5XMC<%?ʬ燍͍k+z aQgXvc6nr NUWUťSVNZԃKK_{uG8\.e+;whj2o{۶K*/WZVvʕW8ZYرmӦ 5 (,.N6<6͓ ckq#hAQѮsv;޾O.6Mk ğ?#C##?C6ߒ)SD崚5j4o_좣ym׮n^TRB*.-]{ Ի}{ۦM9\nrz/|ɣ;ٳiӘ1o80r)C1Cʡ#z .)+[{c535UUVˋMJ]HO/=;T|+F쩦,7ܜ1c|t]Ǐ&Lԥ^\4j[Pu>5%;dhnw^?(fuUի;wj0Tn5_ A|m̊J+)*HM mwٲjn@br*@R.}\hE4䇹9m=kBUB&HJSŘ^czɪk;w MgX~u׾BCGID\DpDK r&,A=G3+6<#j} -I 'ĄŐlۑ$ P& 5O@JJ/inllѸA9qqelز[quɎ IDATe˴С6ɪjbN[s/ :*mk6}‡oF{WJIQΝm(AsƌyxO.7wQn*+׏ux9-]--]|R 5ƚgƖQЈjQ]Kݴ)6N60ٸ8 Ǘ{Rmӝ\Ha >ٞIwG9C4Kˬllj@/$[ڌ\㵧O޹sC##$jܻwT^m7iGKۯl5q՝;54j yfMD=Z{ ::U_jl߾<Cȳj~ Z>IuRRTj5@0Yhc-=Аwt pahnUYKؕ+)sI))*J:eL>aWWV=H׆<>t28s=mg(tvݿS'WTW?ߗoWSQbC0߂WwT|)7Ng϶hC}|#>Y,LPW<7noܸ!'[۷ ܹ%+,.?PS{tWWf 9sc9Ժ+{5 sZzZIfNͣ: f ){N҇>L־/?|""*4Ü%bYp9= M[5Mxj\&Ʉ͕VD.KgA,ܡ^Wv-ZQz$uxz7kV uuO[~ʟchkhH#GٿRL776c;/CH@Z=zfFC""Jʄ+,kմ}m(D5&|~}U ]67>aVn>z{o™9%Yٳ{,1խ` 5yE޸㯿4*Oo &a9;//#'G6W?7n,ssRWh;jҤ* l3]4z~k߾+VhjVzΑwNE1^?@ h8ʣKItťjӋZ,-.}xt S,j,KL yRQ1бjeE;]IEɱ/ׄCBd;)Z0ys.,'#:RYeKCgCR,*Lɔ hVmUjqE_[{Ǐ_cPXXϟйA**(ȶCf͎^|ӷo~9:DUeefvhٲGv͛W%<\;{s7^Q`XR*Z K6[ѮyېC;<=I={V% R,kx;I$-`H]AAvYDi7Ī**ux{[kWkAq+֭zR"P,aBH30f|>199?f~Xl-[l?u=z;hy7,!\ۣ߭??n-lؠA]wj= 9iƏxx?pҥogB||N3wɕ+bg}iO_|o Z/1qp^W{ ))tC_s6i(S6jOBCO9%た,?$̢}˖e'>>QF1מ7oɪUAQ@OƜ~hpC f+K^ԫWb~9 Tu*ћϾ:x=g9OA>kG;^f[p. >oUʁwlfS|p'.W{.p鑗_|ŎM;wd?E-j9φ7ln+‚}e}X) 9ww?\u_+)$UZ] ZV20Yj/U%1}e׮]y۷ڵ{ޜ܂5JIIKMmݢEztRNJ^] 6رamvݿ?7//>14hѬqii͚༲lܶ-kmv;p@RSRZ4k*---5ToqeAgCyyee߲egvC d4o~|-p(/5k߸1{߾Ĵ۷nѢKrmvNHq}{8Nlaa3_ևmtܥw>b[FB||6-#|頑~]3eS^;o'<蟺zjԮer޼IMTb=DZjjZjjLNެYzf19uԴQQP+@5ڶ/jѼS:xk Mmu|#Vp7׽;ϸgF{>w<&v=k$_N=;O;o)w;[{! ^y;t5;?٥L|HrC7}W7/7/y KzÎg}5ϘO=tP񂂂>jMO?O^ɼmaPG\f"_4Ct'ŋcU TCKWsAo&ѣcUJ>}o .֗*~8kObUI1qƌ{f7nָzޫۀnw=~r}vmݵsoyo߄tqɚI1ןv}xctzQuYc)Qwwǂ75>!cǵ?^z{vJJD=7mlܬ>¡I 6|!ܮL}7M7d$aqqq.=PSۀnhFe _5+ׄ4{6e ^vOzmRPIaV~p%]>emg-;+r;M67 SSL8Gs>*WuMsK [[~x>EӴ{w,f鿼1Cu L A=X_9V U։"w%$n.Pl\qKʿ=7OKOmݱS!I~Z>xEj3LIqqqzv ;ަS&ťIhvS]TI1GC_.-!iҦ9I&;]]^:`;/˒Ǡz;ԳN}w#5mTj@!L#tvd<9szmz ]vYL\;g9^v'$"'>/K2!9%e=0پF8C=Îwӹh)]s3yhC;T'>q֧N:2U?O3)Oy|BiV+_3?w=|ɰ {쟏J+Q-R=SM=c6ix냷~HPkN@e ]e_qqq9;3׭[jUC;KL˽S^  r5Ȉ1#4j{/piξ 7{C+kzl߽Y"?0Ac1EбGǰe }ooׅ0نMv٩]{N9&ʚ f~2^~gvo}2ο+'\ylϠۀnwͼ끟>p8pwlޱ)=Oy߽(VP!YzWrsrK߹E쒟l ~zAַYْ\0Y꺀˹(Kłg\8V@MOOsϘ#}@qѼPn~RI*^ؾq{ǶzCν{X^}C!oUyC/]ކo6~g+,)u9pʗz]{>37t15Jm ۲ &Kɭ?HaѴ}w_|'KLJIjڸYF-RY|G+>Yq#կv=p΀g(%v7^~߸fcHcvЭ}7M60/7>YƂ %8%]FZ__\ٹ]8oWھ?:&^Cz :oP=Ka'+^}Յںh$mF3G^ Dـ/}*EPS,udńRE&w6jTUu0YJW.uY] co}u[7lݹyg}9;&阆MHhq|*-FOAAmnޑ}@| 9%Yz:׸YXWG ;#;/7/)%q%F/'{gvJFM# .[7lݱy]{ssrLR&)M5i,acKwÇ4NiؤaYS}{`߁)bH,Pkj@Jj8@]֨A?Oxgĺ37h~Yƺj'>>>MzzXBuX/yFr0\Kв}˖[VYO_?~oڨQjU@ %L(,, Z]EyA >nk.Y \& :=..tZaZ(sB||RSRқ5;uS:w>wРfb[!EBd+T.OIuwl8uAǪ@>_V](TA?)$VӀ/} *EX՟[ /[>5!LʠRV}N"j d4+%O>\& Qpn:;MX@mVŲU@uԩM@0zz;Yf/PMt[joBt#@% c` 4f/@- T.:$33ӽCu%=N2.Pw^ZL-aT{]eTuA< ve4{^ VP$D_k4{j%^ &P$)nDzu;Vuf/}dZn|P) A@ PyD0Y*SнT:ꈠ7~f/@ D0Y*+u*8Bj.^0Y{-f/5.Z"A|A:u:2ư@>.)=v f/PEc]@@t/] Q ;EFf/d.T& @% zt*uM@ JuPMT۷A8=v4{(B%ƺj<+RT.j@@劏u@mYq)5f/Q&L q#@e6Z@Rd1ec@- @ҸXP@I܀5f/%0YϽUf/PU"333% >@D0Y)Q}tAo 2(]m@@:"dE,PU233c]@u@- @HX{> 7c@5 @$4{ Lp#@ԸU*0Y@,P233 :X A& @Eҹ%*0Y,ĺ f/Pqdja@,2Īj%5B/D0Y*Ľ!OܘP hK,ec;@Lm*(1T E7eƺIr}@`@KXc@h{;@ \fffg=yYh 5Nh] }f/'LigʧFSyT@$ c]P;%ƺj@pD%r\~~͛lٲwCիWaÆ͚5KOOOIIuT#w޸q]rrr SRRRSS[hѸqc!wuSAA֭[nݚ[XXX^! 6uuo߾&M5o>>Q]޽{EWU x@dffv˘7{M,P~;2b`ɒ%g}jժlҤIO8Ν;w޽k׮,*((/ϟhѢ/r׮]%홒rwOٳg׮]իR?x…K.sssKڳI&E0tҳg.]$&ZdWU6m|˖-[~}Iͷ6mtرK.ݻwݻw&M\j-nݺ9s̟?a46mڷoaÆ}ٍ7~,zEZ),,,~)(C6ƍo߾cǎ'tRnݺu֠Ah;w|7%Kݻ7t޽{2dȑѯ_dG}x⯾jϞ=%활\SӥK=zt޽V@m"2ZJc̘1K.ɛ7o̙_*hpӧOy p#/SSS?v޲e˙g+zD|ZZZ֭;vؽ{nݺIIbSN_ΥRV jz%WOvrp'zꩃ :"< (#ty=9tDԩS6 oɒ%\sM|̘1dӦM/?񏬬r^~SN9eРA]tQ-_zw,"ڶm;gΜ۷Ww9rd);mM7i7lذa{7G^}Վ;{ŋ}w]v-lEOc8qrOXż.F+((?SOYǟ|4hРA"ڗׯ̙3Q@\\3<ѣs=WٲM6{ÇG||Yg5nܸCI~~СCMVR#jժK/ȣ> h=J4jԨ[#֮]׿u;w, ]v=Ӯʖ-[V H̛@iCSrsssrrh.,=>>O[2}rrrٳe˖UVLNN.Zt}Yg%''f*wiڵǏ_|yg_|g̘gF~l:A "]Qi kY&YYY>ٳ+e]p… 322.Mh {_ɰ5X+===FnݺuAglժUna{P(տ~)S̙3"}e˖-[[ndzuP<5kUdNN{7a„!CL:u޽233,kAW׬_@Z|_\$Yj?g}]š1cݻc]E\vv/~/IܹsÎUς =|IT5IL8qܸq|:j=@5 UmժU]tQEdkcN}zlkXtȑ#7eTOvKZRl?cƌٰaC}ףFZfM7oرc8P&O[CyT єu5xvQ;w=z/KKoi&hd߾}{K&Q~;]UylW\qŔ)Sʗ5 8.*333%?{5״h"&g7&LW)))C߿N4hPXXx;vlܸo]r%Kjk[o_7xoțqƽӡC={СCjjjbbC7nܸf͚+W.]4++r Uvvk~<=zddds1[nݢEΝ6:?gΜXG}4dȐLB\\\fffNvcdVuY^^~ 7|sf͎ϷcǎM6}w;w,}_~y˖-O䓒v8묳^zvZjU׿o~]NJJ*OknnԩSCǎ҅P=zK?p۶mf 7nQUu!rsso喰i=zoۣGiii:t:tgϞ~׮]A.X뮻"$'O &<#GBijRv%@5m۶3g &$$L:sΉp&M >|'N\|9s^J) /,hI'v֭cǎ-)IsϽ#LLHHݻw޽/_3gNtҥKVK/6mڴ\ɑLuС0ٖ-[F /\[`;3l'));#w֭[n~oA0jm۶}駥3wJ{(((;KJС߷ogkӦUW]uUWm޼W_ {"Ȟ={†^|'xbYg kȑG믿&;zJ)#..nʔ)+W +&$$?4h 7:O?}2"}og]I1Tw~' /0I7nȗRm7n͡Gy$$@^xGmIIM3fee7.???tSV_{x[ ݻC=7jԨ2*7|MsVbl֬YE9ٳg-##[nynX,_qRdHOO_ҩSMw}wNNN+2eJazD/|o9$@n?,G9vu}\INN~G[vmأB'Vua۶m+>RRl>ءCn۷nJNN~]>k۶w9>}TLJ{G:*"55~KZ ?yo6UTZP}kAiii]w]gNHH(G$/^:㏏5&@$6mڪU@|MtUPP0a„M[~N: "===))oq}-[V)[zɓnKy̟йsPww}]޽{7f&Mz gee͜9E~o&< j(дiӛnK SN 펦M6~N0iҤ:t {H6mF.LvA#QM2/ OJJ9sYgUk\2d^|XW/III ݔw=X Pbdj 6[.hpȑ/x۰ac=vĉ8(C) NsKyxÆ g͚2M61LJŘ3gNOWXXáN?{7>׿tҠem۶&L1cF@2yG*8 ˣvرjժ#FTE5ިتUf͚vӤIrE*'ɉu֭ɓnZpG}z:(33g;*J<嫯 ݻw+2eʡCBG1jԨCz+VTYtvС )x㍠ 2$<;][y&M bKyꩧB/?]~m۶ ܿs=Wz饗*8 H,T\ R3fσ׉h IDATӧϙgY;|?񏡃[ٸqc9N0ٌ*:WDVXXxm=Ϯ& }g}6UD3θKnz'\ @)+ՏzU@  ]iJn֬Y믇7l~=Dbʔ)U}g}v׮]C9rdU;wnȑ#;=n66rӦM 鮻+=ʖ-[tҠ믿|&&&;6tÇ7?xMyFcQmPn(ٳW_ n|wCd[nÆ _o߾*MQ-Zh…wyg&@$''N>}Ϟ=/|ΔEZ*Y;?>K!D$%RڗDETkR-^uo۫Z.R$R[Km%b)R D-Lw眙LfΜIޯ<I&W>+WJ]?iҤ+$?z&-..^j8Vg͚EN:ejӧ|qJ_%o(Wy}8صkװ0|ؾݻk.qqݻwoLȑ#dJa޽%%%/֫WO,((X|#Ug]Vm"y+a֭V7~7qW^,Y5j$\r+Vx_#ׯ_7DDDX(_۸qd^rҤL{T=\hh Kc `md*/]?( %<™-ZNj7OwC㑑:t}:usΙG|||S={t:UIIIMgϞRq|Ĉ~gqiӦ,y#P%5k|W˗߾}?ϡ8rrr֮]|2rA^^^۷wp͛D\+#  |-b#DEEY9 )_KNNZ!C1ٱ.,; ز )Sk֬z2rÇ+ XQ*w%Ȣuܹsϟ?|>p۶mۨQ#↡gϞݱcsٳGGmBB.͉߿:W^v!B{쑌'$$1$p966%;VFȑ#4h z6լY5j}ӧ%$+?͚5spdF#YAW~d\be䌌㩩VD<րǔ qSNAAAO@`PiCoz_RRxbWcΝ;KO`0;ׁ$6KL&SRR (*浠G (yUVčlEEE8ޢE WՒXo!;ydq|yyyv&UyyyS/**;vo|J;q8(K8kNNa-~GLA:I:sPfTVMLLǿK.)=7n/K:F^2~֭#G\h4*\ѣ.]( l1yd///A0##cӦMN$вeKy'hϟ4t:AݻKHNNto-`pd&M2~ƍ3gTB;lذHAгh,l,J(((5jҥKeʭedd<,Ks֭k׮GLԩSV>_fRPvOÇ?~\!4oDD,[?0ٰaC(}vaaaSSS%z^0+WtSv)q000s?t>4fƌxRRҹs \~]\6r*CVϝ;Wɓ' 6eʔ/*\ٳ`˖-&M$~ _^),U۴i#UdJNNgNj<طo_f|TJY\|Y2" 4_tC7kL==[Z=gK'>}g}W/\dbʰTT,+?^^^3srr$V>RTF[y `-׺uk+!P%L;=ڵM&ӧ~|*+,,L2~53@uӸqyi4Ϝ93;;[Ġ{InpմiS品ƌ#/YD2Effd/ sNt UVgv5 @?ׯ_q \q0 G񃂂l:F3m4q|Uۋت PU/Xc;w4hƝYU4Qlxxx;}h4f&IPf 222$\vˣOCVϘ1^駓'O9>SYz g:]R~͟?/KYYsFƍ7o3ϼk 4P L//;Wh4f͚ɛk9RYfDDDeeeI=zxu0a„k޻w<7L0v|*C o߾D~zA|Ϟ=z^8˗(U>8d'u5dbJohz4 1L ,Xx=ARUU ݻw_hQbbbIIsL&Ӗ-[n:tɓ'7lP D򣒯#޸qC ?ryR3T+3Zj&[PPp}pvǟ9dgժUV͛7hBL:uԵkeyի'{™:HHH y׭3 ֭۰aèQ?:53f}m:u=*c2.O}mnݖ/_^i ?~'|"=\Zҷ0G2FlBBdzO?4h gyrol߮{43F߱cGZZZllc j*o߾k׮]LҥKRr gbj)СC-[;;v~fIIɊ+֭[7vq(!MX\5vد֭[e˖%&&:8d???Kj׮]3ԩSʍN:q[n{$^nu2v;wҥ\Ҿ1a 5v@Ue˖w}w֭,--]jG=q:uțd*))wLI*?2_F qP<-N;w|h4>}cǎiCBBt:]E;,mT^=GGl}ϟo2v23@ug-^8,,“z޳g%K+Rp=xڵUFW^yE_bì>>rm6>}8xÇ=&ԩ`/RThҥ=zXhdzqR>+?ႈkk׮}.8Gm\vvH){,}I:?DZ-}6l`4S\\,ȑ#4h ~\ZZ*98la0m&&$$XJ]v2w+WPƻ?zK=m׻woqhW^)))o-}3/^T ~>>>7n N4[nDDy0--M<~VVHu' P y?Ov_]_$`<֭[ $t5-}:']iƌ_YRcǎ=߇*K|#kZKVKl ۮD2SNݺusmfW28^ԭ[w֬Ycǎ]xJ(..￿;voӠAcwmYYd%+02V~T?6jHL6''Ǽ`8} qqq*m۶V3077Jܞ3`sJUTT`ߘCI#xīv/_aSwJ֭p& VY{PT 6;wm T+W|wqj׮-/((P8->>Yf`XpcJ~aX*kWV~~~e\Al߄M6֭[+һΝ;L I(ceKMߕ022r |;v2xƦj:>>^?tЍ7_[vmoooq܃V1PJk7nt|jz]Lpy%%%7n/n ɒş:uꈃ5D\b255e\\\?Zhe~(--evv`NWAޞF:d\R7ݻwcƌq|d 4@6kԨ!uI>剃&$$DdTg& h⫯Zn]ll>LŲ6l(!իLO?ԾCCC%|aܹSLHH}AF1%%jZ'U$ws\߽{W2n x5J_paIIW+CM6]d߮];+-\… eeʏJOIIIQQ8$ "999/Oڷo~+V̛7|C|…  gG3f3Fߵkɓ'}f+d iӦXU߿V~jB_xFEEo.\t ʓlqΝ2//K-5I&}W[N֕ϩP...nݺuW' ?/T>7Yܸq#$$+UJKKK{ŋkžN:UҸ:_~9 @uʕ+]5`LL@9de5ZPhǯ\M۷oS8+حF*|y9H>ۢE s,wꫯZhܱc)n 1_0}tqketѣGɬwv۶m~UR9r M4_rE%o$2J7nxƛ7o~rMQ guaҥt6} IDATޒGSRRv*wJJʝ;wK#f{ԩڵksrrG`7o |2` ,/KVTgKZtqSNjsnZq4i"s~gXv*))14o|Zj%fޭWv`'xB<{{⠏dU*ճ>&z7٣ѭطo{QcTyڵ CewRd姸222$"޽Kd5o߸qcf*ly;ڇ*UZ(Ⱦb/Lxꩧ4i"-Z%\OlGV6Ll0cۼ4ȑ%/?w\FFڵkרQ#q֭[+Vp$ءk׮~~~h_y߾}`.]t:#y{{ڵkssseڪƮd2(LVVhܷWÆ [l)Km*Er H>U.""B)o&[X466V)*;;<YAޞO:}iiӦ7n޽{7q|РAAAArw@QQ8(,UV*h4FQ^{[|#G*5ō2U*ٳg׮]k~sqqZs矗9jg;"+&&&UTI`ttA&L;vlҥ '5J7 ' `Ĉ>t{L۷o|bcc-*+,,]vxRR݈#񢢢/d>|8k׮۷o=ݻ'֪Uo߾vi=+Mp6j6p*z+aZV3==ĉaì\&\rE?mal&+_z5??_L622JU\u0ZmJRR͛Ix,jzƌL~;''G|got_8 ZZ w֯_-ZH?޽{M4iNe???qhҤI7ot| ,$$$8> ϟrUڵǍ'yĉuԥKqߓ5k7dZl8>bYԖׯUVؕL\x 4p@q|Ŋv)Yܹs&M\ׯo~^?sy҇m۶HjjlTT`cUdp]vYT*d׿%^mRݪ-؎P]|Yrx=h4_%)S}S{\ĉ%;wgIOOw|w}vhСC 988Ǔwt?,]l̙32d=r{_5jTHH)ڦ[n:tp,p6j{+a޼yA o߾8I%f᭷({.]TxJ%U۶m-]ۢE ///Ν;AAA5jԨ0*C:O}'*Nlڵ;v'NȟkQQQJKKgΜ9k֬Gpk6]ĉw޼yZVhVVְaþAժ$))IIHHkA٧N~O>9j(KGÇ9sxBΞ=[߶m۪Ultԩx:u&OPQ3fP`"UXcW2^|cǎ9^l"O0A[o񆯯 xwyd?*ٳgF]vto-#/###+L*;XpҤIs),,e.n4f̘+8nٲeĈ Zbc-L&ӊ+lrȐ!f שYŋk֬i 6wʕzgff.ZhJ3fL'66oz6l/RzoF#q%nݺkgϞ裏Z:zC999ͪ3fݻ+{mԣGڵkmAO<_zM6:t޽2sA=n,99yȑsu;1Lk֬܃'&&fĈS6lx C#Ə>HrۡѣGn42VVx&sU\u:u|gޖNXjU~Rǧn PikИFԲex[]+M&h,..~۷^yܹ p3+>T=&7xk)… ??;'´?C~dWD۝8qێ`EAAAv\x___YrhԨQtt}XUT;vM.Yd6߾}{ܹ}خ];+?~?T*_! ())9tPI"VPP`gy$WI:W^3gC5*++kN8}K/Լygy!!!Om&(g=㲴-W^Ν;/xrr̙35k}||>{ҞvFq}Ϟ=~nݺժUh?t?|.GIQw޵~v)W~-[ڒ{wk׮ njjY-]{ɷz+==]|(>>~$ iӦR ;M\x?裏:tЯ_'|2**ʖ_Ϟ=K>C//Oj/ٳѣ5k.\7nҵR6mdyyyYb}7xHR9r9gϞ -E 6id$Wm۶KLLTvٳ,X0|jhZ\111k@6lذa\=sc.111%%h45YVվ/"Xh4:tСCs k߾}lllӦM#""U*d{nzz#GvyiK_j޼)[-\hрpr0q7xþkǍ'W*j޽aaa٥K%KL<9VZjU͚5[jոqPFoݺ~%+?-Λ7O=ܲerss74 _sΝ;7gΜ9sDGGj*22200Pӕ޹sի/_>u(&99YIHHwA{I8q}֯Wիǎ[ެYh駟~'FӴif͚^.^x… @>~ ߿g}fhu]d?/ιqƔׯM6uQTF1''رc6m:p͛7?֭[0 %%EI=`wLtq?[d2=zѣs -ЬYȇT*Ucǎر#--h~6m]Zv…Æ Z=:p~%$$k.88/7n8qb۶m۶m+߷I `ɒ%:5jdh-o&ghTTi̜9M [/»kv_VV6sLuk׮-^xAAAqqq5k4 >s^UdPBtt~Չ?vڹ:p:gټyÈN%%%^>SLѣڵk_yׯ[?G9r2|||^7|joTy[fffff㓺֕+WN}xyy 6UT!!!֭KLLw0Ν;wª>W_---֭[n~޿Jk Jlٲ5k:1c w!`aspW&%%%%%=:lN[o5l0g&,_rQFcJJJyORoo5jTxc呑'h,UlUTk׾{gRX{NZPP`]v9>#p92e+}_ٳ$ (DjJxoWeYz}AAAdu:݂ ^xebbccD!CDGG2Z~饗֬YӨQ#Yts[nDu ,ݻwǷmf٨˖-5kzlٲڵk[9ǫOnllڵkN&<Ȑ!C\25L&5v@mwS^먰y^~eeruYVxc%$$dڵ+44 j4+3VfviӦN:5 ps4@!aaa??޻wK/|ri$>^a&֭p˗hB1Z\CFZ611Q;t萜UuիW kL?-I&}wzySh4zꅆFEE5o<66m۶~~~Λd+Wܲe˃˧UV{0`yf͚M6-Z(;; t2cƌXf{oqu"_&J]?Exߞ>>AAALZPPp}A088ۑaU*d:}޽{3gZ?_uԩ[nmڴ|C9^{5A[Į]VVV&z{{7^ȑL'6m4..}:ur!Ke+;Y~~~v_n0ߟ/ܺui7իא!C5kf\yyy<fh*J׋sSN͚5sQc^@IO>~SΜ9e4_Rf͸Ν;?::ھy޽{=AБ2{޽+&)55uӦM{ɱtZHHȓO>SOu5^Uׯ㕪kPTܹ{UxWEնl{':vxܧ ~,*>v죕']**..ͽv͛7ܹSXXXZZju:]@@@PPPdddhh({)dqƥKn߾ O5֭Q^=W 7ϿsNQQQYYYfluԩ_~XXXÆ 50a޽{J٢s1Y7o|AIIo5 u40Lyyyw-++[nÆ 5kVn]W'ݡmF|W IDATpڵkGDD4lذJϿpBnnnAAAIIwڵ6lؤI 8CUdy˗]V~W{VZ/L݊<d}ܭl9$7U v4nY@e,-e(p>ul0LRb/@y4x [* ʠ,"x2&&UmYY5v ~-z?YgvQT^N@UC#T+^r\<x򒾥qe111 L4NOT%Tt #CoTd4w fij>Xc@uO|8uFb/@I4 x@UE?Y`6@F&Ib/ELlxUTl{wU}/||fHXH A@ . _<ŭֺ^o[Zo_jZڢ֥bŪDžRzbQQ)R)Ԁa K2<40If}ߜ;_̜|@;fRm @n J)͛IȋxPbR9d(vH$R܉FmU7{ 1YL[UJ=YrƇ,m&en7{yLyR'K; ~rLx9'([$& @[#%YJ࢞,ć)2䧵@^$KzWPrLL6' PnɒG>pqȥm&eBn6%Y dyr'Kmeś=({}9^b}](P>dr{/D͓nxn9 & @' Jr ɒ3~f@i´ͤlXq Q7dMΛ7/ǓV}C'(C->3":b ͼ=Pzc7n7{kRLaJJǐ(ObP wŤc~PEb<-xthW->Z5eWJ|¬'{귊\%O l[> dhwD—Jwz'=Yҗo J)xPBLEqGʜ,F䷄_<(Cno| w+߃o`3=YRH;!S>, wyW(GR##Z,FQwA\dpON$OHȍ?:Mޖ{o@6bbo2_IFۻ$ YɲED"/P඙u!& ғ%$IhY,=2$ Phl.tT{(P##dn2:@!K`dHI(ɺ @Ȅ,@I @1h5%Y"# @ih@JQuw"& czbz[m5":(و!1Yt) YJ,% %Yu(jb۠$ P,Rdd#PdRQ( 21YdB;1Yp"#qS#& BIPdI@HQu;&& )و۹:1Y$ Pdd@L Q(l)JnP>d EF6^.eFL(wJIFZʚ,@aJQu%& d J @yʑ,@aJQu d$ P\ܿb dEEȝВ @H$[ -BI(s Ɉ/4##J,/%Y4LI(b@RJIEh%YYx=@SJ,PjdTLhF6$ XhJ@iJ,Pd$ |Es\2̓cXVwc.]`-ded#& {`SUU;y}vuaZ6 ` %Y|mluoϞɿD?s>죺vR$ XJWqǏi||h4$ 1Y ֮Z;g=s洙(zو,Pdܩ[Tw!?uS(bJ@٪@1ֿ}EcX$77߰vچ//d'>Μ&!;֯[ugppD(LLhn59 1ߦ^zw/_>_׽w}whZG,&!7랞G%;lc_ù$ & >?{gW6pMhTI "& h4zg_:W{Wz5#%4#Qʒ,ξCO94F?oxX(lMLhwhۡ][| HSNjgЁ{ܻwYMo9̩[Tnͺn;xXEe{=ٴi;sf={Nj\xΝ{Vۭ1aUݫd%OPPd]9z/'XO$8ċOlK'?8y)~6?7_Smk/ݴOoH$zwN|wmUۧk÷<|%'^tEUݫҿ ?Ziæc]4gѭ:s[u]/=ҭn3OôtqW{}oOk~ݽ5rfd#J+CWGf<㌯O_ݢiSSw?!ߙ֞Ln]={ְYb̹~¥ f9?3,nm.:7YYR&1Y wbذlXtvtjٌ|򫎼jd1D"q{M6qlhܐr-׭lD"1c' @)"eȁC>35>}I}LRP][=pH$P߰o B]z[4=Ӟ=;lD&0āvd'N[{b"wYh΢dG0z\[;dOI Mb@Nn.N};se%X,vGr)u.H̙1{ӣo~~oK.*:L}fxH$/83Zu֣>Zԥi?KOj筯 Cw֪ K%YYP|~ ][T⬆xsHGy-jthtȁCFO=~v)xn<747t|>1-+;Uy摷1?tqW dZELȩ^_^_W⬻{+<`2|?ڻSBӦϺq5[/uQ>t7~Pۧv=v>~y j3ӿkn>`ko{}wܚ5ӯ>>tvkvauҨ! 4/T{ _{ϵ=ܤw)\76mܔ&%Ye. 6\_W^ sK{ޱO4m{/=dxH%h4zIW._fDkbqM-o .[nΌ9ҧ$ 1Y *;UZQ7{o~2!vU)OLin[Tv;w4bӦ`"6qM㋏\?R}Yժ} MU% %Y4<DqZpcaՏ8oulUkWe!l$ dj\ظ1^ۧ6|Mjw 98}Jq‘,wsoܛmxuk։ %Y & ?]3Opq5w5eR׮\Ͷ9_Boͅ(I%YY=P^]󠝃6l .b"t}Cĺ5P9 Pzdڏ,S?]s[Y.W8My$,@.9vڗ)>t{w ]l&Iv6M89"IJDBI9^z]p Ov{^٩2d3ɬf+;Ul=ٷgp0d%OP>d\XGo}4^ӻ3LvVe}Op7_jmfYf洙}OhD?0𣅟~i7-^y(+J%& IOڻXǿqeԧ9C ?Z„Z;̋`ZӻEeʓ/;9qØě[4)䝘,^6xUwvm^tECG Mgsz]U.yՍf\KOޝ~;!>?zп,@.mySw;?;vgcwLp}u꫏ˆt6i꣯^KǞwn{01g/K/fJLl'(sb@H$˗-}뿸}gǼ97z)|߸;ww;xȯe+4#Q1Y?ws3;77o8?ep~^06R9C>г=wkuH$_-ˆ{^;v;/uYC}u_݋/H$cweꎝ;675Yf+խY&SP4d , 8alr%[/5eûSMgX,v}wq̓w>7k_/|Œ/r3%@I0=PDz,nvǏ>lRۧvܟǝp ϓw΄/*.EI`b=v>Sz?yE'TTV>qӮ<-=+;Vvi?8m5OuGۿc~^_uڕxo@1 -& %YBfe%wM]n:tqٷg߁}wˠu߱{VսqWó'?8y)~6O?gq…'Tuitj>ῶ!t >M8ޘ3cN<O:b~qA Pds?  HMOL$D"Eu2kuV֯\n}$]}e.{ܻw34~<Ϳ&KWXո9!֩KzZ~; ){dR3":b- |lJF[,dBLHbJ䝘,$|䝒,@2$ P\d)|ZhIVF=SJE"9( 1Y( J$wAR@SHFI("bPdQro|@) $$ %Y|%CLJVl'/=Y4T{](|h=R=ƢѨ,@PS""& %%Y AI ' 91Y(Jۤ' /1Y(JAӃz@R>H$Jz@$I ,PdEQ%Yt%@LUƁ,@(=Y؉@QRȀ,Pd(dLO(^bPdBDBI Mz@b$I,Pdh(!=Y@FJmNO(.bPe d' 1Y(hJMO(bPdrCO( bPB#DBI=OL Ql'(z@@KJcΣ Y; h5ZWqk{^-HE\PՊXZ7(RPE+(( dx|2e2LzΉdFx @sk~ɷ,!q3!BH KI @$ @d xJTZ @C$+# P= uUn87QW$D$AS҂^ِ,4DJT,$, & I$ @4 qK2TF$P&d d!1YuJԜ,@IdSd dBF4JԐ,:%YjNL 1Y & dR,@ Hb)@L 1Y & ҂^-sZd4H21Y.-_Tf8I'uB P@s^z.\Z PfOv*=%-#-됬-۶l׹]3ki=d:h8z 5DѠW{k[|&7DEz` lݡu$T=CiIxryc;@@C´> q~+:P~83Ƿh"uaOIE__ٵ'EaSajܤqv-E[5oܤq埾c3$SZ @dh2n.3,).ߑ9o ܿ]b}y7yz᩵)"A/[FJ4Xڧko~旗<;/O  idh+&"7V|b%I ,1Y"%Yqoإc3'yhdhXᰒ,TUMoݭq~4@%& @7#RJw~N:7ޘeaP'81v^0@Ô JPsvffe&hiIi(4>ɾ[) G"JLԶ&w)ϿUڰjÖ[vmUwMjߪcݎv3jjş-lŒ^m%%ߞffe6k٬E;ԭ?<1ݻ5=#ίOm+).ٹmg<#3#EvVP)@ڶ+vؽoFW.Y9og??.{j !/z^ݲ]˪A7OX0kAсp[ߺ_˿g:g~OFco;eѣO9:9k4b[q3!%Y,tIpOs=79k M6w?}^KbEw^ywڋ/Z<O>c݌D-Vɣ'ǖdG+KZ$ UXP?,3lٮN%[myާlN=uv Vަ~rۈA#jX2Mݙ~]ve</(xՈwKڕ+iG>3zOMs=7o޴f;x^_ڽ}sn߼6qYbJX%5I;,s4׍w]1+RM6撮[E_Vcɿ_=:=U} Və8kbNZz]1YR^܌lHIO[ܬ 6>઻iMXǮNZr=ן||Wkzպ׽ qtgGEEbm*--{>}2̬߱k@ dHmJP=g߲nK(*-)-:PTpWޮm+)ݏ~o?~GdnZ{R\T;zTבYKPhena$3mL{'& @ Sj[tժ*.׿8ggF"AV;!v7oIqIFdΎ{t32_6qڋ^6'"& @R8cz{"&M;4[Eű.? \uίKߥ|j[FJp?y&iiqqyw\jG]9*W Y z+&H]YMtlӦcZhQ}舋MVt(9K#),(\pIcNUmox{}O;򙑑b@u,$e7_v7Rp7⛥,p·?Y[wN6qZ6a}$v~q?sPަ ߑ_f~x赇3 `2K(@Bd5ڧk>]tf(Zמx_-,(,KV\{og'vh4}k6o߲`w߲~K _dey>59 zXfު}&fd+'& @jP$ԭ-nm?0tœ9-_4#]p5|O}|g?Y^V`qG:^JKFlIJg5~d+1YR,UVxN!c߉w}t{j]~~fUM+eǖq:KFHJP`A ӿ=*-)G.7o{AwNZI6 ٵ'<;';i;Tlӟh3u,uZܒl4U$ ~M|˺-SLU>yziiia$4+>ʶvOErWM&@(a [2Uw]ռUFLyh=ѹGvkѦEӜ3"H(Z[RBOJKK IDAThܣڧnpKәl}hޔ dʫ(@n}ˆ/֋o ՠ0IS(܋o8̬򞞿3J l}fD91c~?ևt"A/e)@`f-ϘxzFܒl.xEUPM歚ǝo۸V_N;CO>b-JP{ڱ% T}ۆm㇌:3{Z߄XƝY&9 T~{Fꞏ(+81Y%Y:;,]s 5){ [m1[d'r ]> }苤Py׍(Qс;/_%y%%& @]$FdNiҴI{ʾ|u#i*DZ=?a2רp8|ǣwzqO v =wo6&y+$& @P^I6+]pOaJR;ާnN2(ѽ/{)G=۔7!;LVEL)@ YŪf-Z;lٮeN뜄U9pbh^I2ud5[ zdR8Vr`߁8v޴yӜ6a;LKOKf6q/OzyۆmI^l6ovjtKG Q\TH>1Y7#R:io;srBӼMyHZU,3ԭS켰K;ĿMɎ{`ւo.@ %YH9{ xzf}*sw..=zr=\S3mL$wNJ$,$FlؕkĠoO{;a6oZ7~ݏW]{~=KV 6TpCFi?9$=Wޓ#Jw~\]VƩz*t s^S{@dHJ؁}6~qyK\}XǮʊܽs>q{W޲nKњkG\KK*'_xXG;7w:{A=M8moJ^gמw^y瞫9y//;Z6 B'OOYlM%/D"}|? 3+stWko=I #& @-Rz#<7?iո$=3}#w[ky؛^W sH$21qhO\luy\ϧKvlqǷ>uzOꝖV0tBIqI_ :UyU%YH21Yh$6LRjd%Y,U' YDQ%YmiA/oata+PGs; ‘WM&E^T) WpXI%& @BQ,$,00v' ,bT,44JPwP5zpҎFJ1YLOJ,ա' ,AbT,KpXI&1YOO~d.FdP:NLғz@I>1Y@ORZܯh4$ u,' )l7*&& @BQ"& @"BJZdH0=YTLLӓLIR,BO&%YH]b=Yk~FQ%YH iA/@ &p8\ W^I6#& PȆd D^Z$ ,@$ IZ Pw-.LUD]T,3bPܒ,H `JP/+JP_7#R"$,{b)OI1YԦ$ DZ P}qK2P/E^jR%-ِ,kjdaH%J` %YhdR,4piA/?w\Sg@2EP+*UֺۺuV:k]vjjZPqQ:P-* H~W9 !@XsO3&Yjd(U= 4Al {Q5MJ &Y`2Y(h"@C,-l,5@Y&Y` ePT$ L-5MҎ&Y`d0$ D,dl {t?{Nt666=<*x(mg3&Yjd1'tޟJ;zZmCI9Ym +fi>WDdDK7)jc+mU>U}իV[oв5G-'+3KRޟWN]yf^NJ%m(J( 4RBƢ5 r+)!)l45WVε;Au=ݭ6deͫ6?Ă*{բu N;k3_y9A66y<(J۷S(dkd^؟dV`}a[XsBdDזܿy485d(ݮ'~ (!hyA,("\7g4_"__σc{_^SIeۻi㗤yhHwX}1WbaI֥Kmm\VrAHKM{O߼}ջQw824&Yjd(t:? d@<`dK3Vk5y҇.C?~~*G `FNl/<4PtPP& J3$%^SnU+ rɿOJU*[k9<}T|Hi4MMJd@~|(yB,fΕ7 &Yl {BIׄo7 48tO(΅GvްH.< (A,e:d FݽqF56NwMiޮ;{GM ( {B/kP$'ɶ WW8Id@)vdבT<}&{V$(ddu:yG,("P's\+Xa)/Rds'+ObX'%e %'1g%J6MF*m`MQ& JԤc j޵tqؖ0VkP$R(4+LFGwH0Lի_߿KP^:~Z(fdkdd@- Ahp(ɮmׯ^IH7n۸@GJO}:ɷG c&%$t:ײnܬ?4隸Gq q )/R232AQ8:.㥴?&,٭˷6 |xֿAxKޕ ~@jYY7.HxQA_YYё?wpް]{'6Y&]Q::UxB 棴ԴQɩ>U}^RwLKMqFJRgEj[Jʋ{7%>KtNN*Z1A=}Ĕ,[;[rnTtvs;4Y ٣d5CIO:(.uvsnգ?vp暙66>̵׉}'F]l޵y]_R_B^*Nw};GVREFvkԦ') G.\xjlNU~&훴cq'>) fX\xƽ3VsԼk特sԕ\Dmᚗ ݉}'>++u;\>qYvս HhTS^zOZ/ /U˾2Bք'ىcq',R%feؑG.{'V͹~{ tr۟]'׸mc__ii-nmuvRqg=091y;ݩ_Qcg Ta{ǷqK˵ޢލ5.V珟r boⓤ +{7n׸}mzQ٫o޿x(tru0t&Cst;;|N^i[a-5"_&hV £:qj.0դOg8,$l]Bc O\Άsr\YI_ϯӴN<%>K5Wq*W}ut%=߯ 6vmk=D+Hغ~lj(T(VLvqØÏo?zGS?֤k~pƑGN[yV IDAT1M.)!iހyoT\N6SjRU"}gSoxz.] }qPĿ Q]Jr z)ݸ77jܶg~j1ε;AuѴdݺn&CmAtgWoX2ݿy,s֗-_vAoRvϏ{>(ٸ mvWǸGqيS(ddybXF+~[迪%,D\%ֽ[;qHKM ͯ2YMfu[n1ssǷ?{nfeiI+^3ן?|&-ڴ㠎*[wa}i.Ng.Aʹ.}lN9WNkBtd3zJe8:9+P##"g!]Y}fm:^z`:>6.Xѯb1,zo}#*U'w{y?ۢ&YAFCߨj\($+¯W˯$]>"e?hFortrHGwd'ffdhvm:c>ȶCh&oHheUY~'S;M}r^X`*{akԅw#W+{1M0fLM35~^b}u{>rrn=X4rF&YP(,{#TӄSaDa+utrlӧti24yS:M1I6[Vf'>&A=>{Ƿo~lԴw݂ y} &Y5!kgx0asedݹv'xu/tukErvs=4ߺn6KCքUmpC&V.919cN|rcMݸ*)V~9IVO^>~ʩ+-f`u:1Ked3KrJ3]>lcMَ:*-ΑNLh=&lo,Pӄim\1WMzwߙzBhph^f p1M$ w &YPX(ˡD} Z6Xa% t:t4S&lxgÎw*1Mn?ֿ,؛]#e{ Z1Kě^ua L4fK|x`+U =yxt<,M3vM΂ DFD~;+e{~ܳ97jK_[_\o_nxǖ5f`W^?wK>|T΃}=ROoNZjZnOaͬ5kM{7 G/X=&Cp[Jߴ{|񷻽2c:ia[XUhp(VǶѪKԤTV=[Y|kmE]ost5+_ccc'.zꙫVNYi`g de3aC٫^^nҡIE ލxbhpkt:݂ e),ʖ/[EkVYB vv,m{7]K/zʦ閎YZau9?$#/}eS*MQ& JGq7L E!Eo罢32,r&QG~GuGgG}.չ?r~ݜuWN]εZmZjed[7wsץ̈́FWJ+5t9~z}.լ^"^^^iHjVU^^zGu񔵳~tO8ݼk܎$y_v\;|;ɤOD/&D4|ٕW:ȂBɘ+1&>dڐ3gjrjw,M&-=~.BT5-;S 0x`{MP'Wgw5Isbߓt/2EJvP=wZJWs\t~̕sdw|cۥ_-h`6h`ȴ!W\荏dYa@w/omy)UTyw/w|楛3z̈{gZ>'^^תgG3[LyNQ4_%]fާO{_V҈n[CcV]4rѭoI/~ڙjZZ~[8n?xq1w:}Ŕ7V[nαwb/9gjҿߺNPhEe9qƞ>ҕ]HdSR#Ft[߹~g\e`=3]ƈWmΆ, nmKte\|&R(mzinxSg(t..Xp.ܔSDFizWiZg1g˻up_WO}҂2Yg7/2919SZ7+4mENGߏ\f}T?.T(Ah߿}}'&hTl}AP٩{ꟽٞ ёѫ}e\Xio#]0l)8Eo6#ێ *{q:jLdK ߏxU~u+/_/A|\37Ex|qdݜ7ߠkSW c5s&=#lвY"aҲIGR [5eڣۏDW:{ȅ{ss#&>K4 aҷ>{G{rkHY`}ʜ!$pXf\ʺH0nnκLM4_RMٔ6ʑG?^v$t3VKs[g?3$WMh';>wqvwϾ?}AlSͦP(&}8Oq9n;k; 0sLҴob[hm/~aedWֿx5s(aӇI;ItUkGj5 تl?ivl6_b?Avl6w/U*֋>Yei24n+XaI6ev~Sz tͬ޳?.]nU_M5a܂a ?vruZת&Y[h++e󿛻.j+_uUVˊ_̼Ø_4uޫccIVݟTK6l3s}K/&r5zPhEei(mu]SuQ}L.D}L1 hRxb߉+H'0ۯ{c@wֽZx(Ke )D~pi]BpGc=ݗ,U^u+&Z!S ae˗ [pZdD3WEl_m۟ݠUUSTyFtU}{-8ޛ~}f}]! 57liefuԂ *|oɱ}@A,(R(Ł-D+_18L3-5s{-kHC.9 ?yn6DV z;(W4j>x 䯚5aK[MtergEOQh`']enДA|5~ WֽZprǗe?=EOX:A;؋Ϭ̬K6JQsGyy(ҴSS\sam*[Q^];,  En &,ƒ7,ȮVZq}6K{C9nqm& *H8{]zW ( d@QC,(-BCEl]^Mdf瘖&~NN:Jj-ƝRysG*:EZv`z5nX{պ㠎FfeeY6U)4p@J>}%,W<(|{(>sx^f+DJ se{lT2O9A=_H rOQӯn?vvCjjb BѶO[i~\#Kސ$urujzie!&`ln'ǽEɂ ۬nf[nUi~X(&|[K/_/d4c4E,(߼5Ve~@{[llm:,#F$'&S)jiuDWɻ#;^nEPA6Hϰ4jⲉ]Ue֌LuF,((BhH( FmyxJsi5 ^ Z6fNx4oغf@ۮ3$F/w|Y 1=?IybxV.i]TRT)mV7jȂܽe?jn8y-f w1K$ײ^eaø N^[sXu:ML|1Wb# {Gv}QTv*Ӻx͹uZjyٞ_~BEl{r>Gn !S=X:܋-~wx|<14񀈊v|c;gfP$ eaPn\q7aتg+3+.A]Cg?~qf1#MfwF1ٸ>ꙫ4ɢn΢0)!i尿&vE]:~0Q(æJmY3+(]nAjM< O<-q̝@A,(^l {*J+{i'deHCV{pS 򮈌N;雟޿yAw|>M ?R)%m}K2bIl>LMRT?/忯REJd@C,(t:]XH(bclZjln8`1`&]jƪ_n7g]vhzZzlȴ!ZaxORY쳿7- ^#h TZ{ 56ڨlm̰R+m@&$ qʩ+n?*/ʛXU@wϕ"2d%'&3 WmUjRJ*+W QF[eRU*vඃbUƥ}5pX`T$>K=}|llm2Dy̕| @ C,()vP2eefvP jTI-ifbOObccܻ!+^WS=S :Xo[M_:Dt}\] |N_u)yn^) +׬~i IDATdlN___>qRHh%L?7Lykܶq^Δuc[j_l;R,ѝGmCgZ(T٩uiV(X0q(||qpAF=1KJ2 W e++:.2"R-q;O<lNIG)4$TtYi2lfl4dvuzE%_m?{G{i~y *&|GC1Wb^eaP\s94^,–[Dy}ZcI&N|{)"̣[z|4oޭmz7Pk$2YPi24~?$ xw yhp:v0Eʹs6Kd[ծl*[߼rH˘?hlr%i4bMA02"T?S9*C|W)IO:qhճ/w|٣4?g8ML@N+{kQ/'i}J֖=Z۾f}u:ʩ+wEX+_d. X,-i%UպU[tk!nZjaRy-[koAkVh7&[i8,^[ʞ @ɣP(h%e %vV*'%: $[J%^^G΅3禚 Ͳqv|9M]ё朠NQ3yznܤN;D_^~VHE͜~s"#"s_6h}k9ۇ9ԡ4?$Jci%eH8aQ8,0_٤CijۏLGS;MO l5m+{Bgef4?!.+1oz讣ٟڨT=wKVfiCք}eݨ բ(1umWo?;XeBP[EG*j |3|GҼÀ50 {Uc?}FMdL4{#RR _ZMj]Hfl˨9JxlRI6S?}ӈ#.jӻMA=l~ƽ'$MOΆo^\f,ٚvjyV<}﨓RtaǼ2fUiƖE]ʟAQ(aӇ^vu)=6;dK:1˚ m^yN9y𥥦8&ޝeߒזHs;i+WZNiҮrfw14=Nu!j.ւ MģL4a!a$pXV) tQTz_*u[{5|.AP/f֚~}jRk’ ɉɖM.Xm̕ G+ )/RN^1yE?~>ޔfkl[C-ܽ;({3:.N~7ߌ:CWIט^ǫ=\=\]-R}9O1m4z0-m[1,m?ybۉ}|=oe/ jWVL^,?\z%e`d/È =f֚g=a:t:d@Ib['uc00(0o,ى}'DyXHO*OE쉈{'n׮v ``amHeK9"EzI Y&DXi*TTR&wN8aQZQVupd>8!.AZ~n2^;{mJ)cW/)>):2ƞ(v8 8৏~~NNVw_su{E#95jӨ^zUjWtruR(􄸄޻q/:2ꙫŔKY4ݨipz땯T^>yxАPælCt[hTcO [ [m-_I&ԮTG2YYYduػwߍu⍬̬0$kI eD94äjݪW߻ >6h@v?e:cMΎOUA ~2cMvr!%t ~3VX1e,La@|%Gv]kӗ;];/H4e/+(ʠr] TY`[]ߊ{(-#-ta9XޢFg3OhغᢟYyyh4uڇ(6$|El@,(,k"+48Tt b^bW٩>`O -s)492YA:8c 6zzmWOVN[ @1E,(m(%GrbrQ%K͹eփ,Ft[sٸ~hв>{kA Zh`gmpjeNVϕ_.aȇ..qPn8i`vZ {mz˝w,ERQݥY#?I)š/}5ϣG_GYG g6gck3~|^ƥLhک7ys6G._G(4R2YPryD1Lj6W˯ 8,P>}vynkvNNOwW҇tuz.stM6W{^?{á >00yhk! ʱWe >Q* /bNL}rIGE#\4lݰn1KiT*Ly_ͫBդ _&'f}m/m1gc$.e]f{ﯽZJ-;ѾEs?//(4IKP zNN$+Bԣۏ >5({4Rї'⯈Wovv[4SQݝݜE25ִSS'W's7p~}<~`;:96jӨKP.A]8vէNIOkգ^eEJB\cEBhׯ]A;<{ ¥o5ߚ5vjڲGKBOӝ ?צ.#VYQvͻ5Ѩk?IKZjm=nK FJc/Gu?{j֦f㚭{>a[zj]?s̿'uØ_O5u UE:MX,nsEcfXP0bddyFk;DU2YJZ&[LSԏ>NOR(ezyXԤػ))66e˗RMa4VP(drt:*MQXY맿&>U}+HNL"EAertv,U֥e/+]{عcM;3s/GS# Q)bk"4"IVB &" b1M )$.YH4vjH{S{>yϧ;uݹo;ov|'\9ݩorֳyo~Od,@d`1Y@L o?=\98|?>䯟 1Yv$ P*1YXezחJgo=/Hfd12]Ng_kKgΞ PNL '//>r?įcSAL 7^zc#? w)&& w?{~wg~L$U$X,djX竫_}o季_c[U~ |ȫ}4͵ӏ?}yeɃO=Դ`tɌl$ & 0+~opbw{LII` O/ۢdC?}W|,lo~ɇW SJtիWiTn?wwp߅~?WF$ddj}*j_G-{+),tA7,Uv5Kfd0պ@ud & LJI`;btdGj,lTAI`GbdvGJ6J'E$ 0 1Y`J$ 0>zPdIVF`G](J#Hfd%Yt(MLؕ,d(Ld,YC=ȕ,dm(Ld&mdFEr$ ELؔ,@ 1Y`#J@%YY)uS1Y`%Y36JA)̍,LI`d(S=dIVF`\(Y=6JsE)dAL$ 1Y,@FZ Ed|u0KSp].jHI ;bP%Y@$4P۶JJ EŵhdFMD=#UQ]=,@%YYt)"mdE$ P1Y(,@d(JIdedE$ P61Y(,@NFI8]`{J\)TEL$ P>zpbɒ,@ٺ(ILr$ P>zdFQF=8,b0wJ4b0sJ\GVJdedEҔd8,Kfd%Yu($& 3$ *b0JGfEIVF,`JlJfd%YRP)%YNDL(pRb05%YG$K2Q`k}B2#(.zOI݉d,HIXdP=ʔ,.zHI(Ɍl$ κP%Y#& P`Tb0%YG%K2 yS`b=%Y&G,%3,@~d,,!ddGl$K2LyP V=.#N IDATmdV=fMId1YHS`V0Gɒ,0;J̐,DIy\$3,EYP`d@I P;%YGHɒ,3E0JdDLJ)>zL-md.zLJILP%Y%& @-dZ=8ƪ H~JdS(?laH,@<hd`< s] m$ ;x؄,1lO%Y>d(NJq[`>zťK ŋm+ UQ(b?z0v/ym50UeI<}XdmUdJ/`kn$PCٔdQblqу<"m$ уn,pzPU',@Fd%& @dlJГXOL*Bd ' ,Г$Ob$ 5=YUd,dUIv% NO ILBڶU(,QbTGOlU(,1Yj' R,abTJO$ P'=Yd,d$yn. %Y\'& @d! J/ ,@#& z0sJ\' & z0Sm*p,P91Yh=YU',@dda>dXCOV=F^8m#a R^OLId=MtkJ,$ ]2#(d(ҥ^xqPbkz)P/%Y؜,1dD(Y5TGIG"Ɍl$ P %Y؅,SP0:%Y]=%K2pR]J$ C`,J0>zJfd%YA=(d,DL(x"Yt(,@ޒFIE cJ01Y;rh9rf,3z[9˕ZP35`Q G 8z^ydCLI00d3d3.P6R# 4ei&Y(dkddCQ(hJe(M@Y`\LIJF }vȭȸԤԬ,AMͭmm+UTfjx4٠HYSJe,Cr[]۬~},*Qo5gg&Yjd~gQ{L+fruˢ{s7JţMiO-[>ѱx{x(|VŻT5M N_KO5ըհVs7^ڃ%dRsC{ޱxq1h&|ݡs榦w)]"I,=5}FiykN):z54*rpw761֨5i/c^{PrZ@D,R'uxzf(yȧ#F,Μ5 ru--"I29i N]}T#@Y^}IڟJm%$%jٰ֡OJu:,8WO^xbLDLIN ((P \\륇f,]z~fRY]33({}qxh5Ϳ>u2z@7,ؠ)w>ouinni/{z}\J#{}w}hs\bʨbxٻV,o9vXqЈQb, 3bY*نFC,eGJbC;_,84T*?ר^7Ɋ(-:t2Y~*{hΪUiE\?4"7¾ ر+@œ_'8t*+.܊zey:h4\&z[PNQ& ԌFyxtmۊe2Tٳe ൥P(h/]Xi>x`((PjJYdoȘW>|'EcTPy4P%%^T9@Sihni/dMSedWT(Qq[*$d(_N=S97<]! ](<o+(eKN535[ܻǂ˶o gͲ47g>h (( ml ? \L@)sZu֘15FϢm( {j5K= GIV$ @ĕBQLC Mzj44151$r2Y>R4?{_ǏԬ22&&ƿ̙#۔d ? ޿{Uyf ~|ѣరϟG'%ed A07dgWť~͚?&%'£^xgⓒrj;kڮ-xgoԪȨxq;wB5MjԨW6nnyOO0*0Y&Yʉ㻎7PA#{\?s]tg?U$QerǷ>UeLL9{4բK q06-;2)Aǃz74.*.[mjnZ٭rݦuwnެC3#)1WLD̵](,.:.-9MY[;UuroޠEfYYeBLDL{ED$$de( s+sg.VhQϫ]%y[ϟZO|\`W,SN'%9'._*#g6GѪN-ܸq(tTU?￿^&obeaqǧX #8,ٳG/\8{ZrZW،w{`ŋ?KG߸!AB.oF֍=zzu_Y!B|W}<5)3]k?`GCmV B,2iΜQeeMO>i|lŊ6H:v$$'n={$9-??jb֬Fz/ s$J%]T L:qѢ =Gc|ŋ]Apvp(2~hQН;x3uq|[++3SS[+B Mw#͛w9Ztm =>. Ӽu[27sERSv.y6k߿ ⚑+}9ܰKI}~\ش}SAk C5]E'>^߽''oQ-~w:GgMmJKrǜ윋G.^]I6WT\܄fN}pa@`My:wDhy~mWA;K;veW?ٳB1,^vڵ5ɾ݆ NlM+/]j5vlKl_\ o{n?|Wr<ݲo.: :{vvNO5JqC,߾]ݩSՋr{wysi߾!}O?l :_j4SYީ>>ڬ[5nxVVuW^IVvvt\ܽ]r֭5wO9R;xyyկ;wn[HA|%Q8wfQٹVj՜Jezffċ7>KLś6ٳazl.Zߟf7ps3R*ݽ">> 11}N}U5wvm~G-xzWP(?r IDAT/ݹ2)) <0}z}lIn<ʛt!obhۺWsω 0uTRIIS{N uյNMJ |3XtO{c/;G&u,-Cn<$QZ`̂nûq^7H8Fe7115yg;^O}7+[7|åIzJzxHkTx7SמZke]2nYElBF| 66uqvuP(Y/^QniۯњoԬݨMvVvAwӒQ 3xWvWx@3~Р_}}aaw1.*.n9>}ʽ{5*1}Nnޠ)2&fK31%Ez}ԬY(,-vf陙!C껻oF9y.]>WR4q7D_wOב\ܹ[˖7QlQד;v뛣VK.ٲaÌ KP=zرMܹSըQM^t5Sg`NZx7lm'h4 7o.X?0Pz4G?6/X>;66_O0aj܍˷oĉWP]p=TiIc47fy /޺U1@YGY$ @y/Jk'1L662^Z|;g>>h79rWmo\Ys͍}+=P(z#G[o32r,U֑mG2F)8ae[{=rH#o8h4>şktJU*|arBkm֡/ƶJ4XZrZ?lH·~3X2ZCZzQRγi5հVeD<_''&͍oo^?^U'7]wGqkM&&C?:|jͳTYmvZgoXcc 2YeѲYz~w6ۯ}sVJJM{RuΜ 1/3U***Zc՜0a A=y/Vܢś 1AoPP+\iz ߿aayϿ">>8,,JvvXSnӷ] ݶm=5U[gCv?mZrZhdL7:6oͦ{|ʨavN={~">~ۡCp"Ks4thD EۦM^_^-='6Οo9jS$ m<=WTvuq~:I3olmqv-Z$m562ݶmmΜxqdLLeMT$G\&cDv013>z߰_ZP+7~ypg!gWǿ3UVzP}ENinW߷Bzȵ%SFL?X(]>W,]von + gxzO.͍M??|[*Kˁ{g#=?ggڍ_>XY aܮ :8R2YFUjTqߏ jjnZYzWĔ+'?t^1Ax٢IsW%{#=dbjyH;%JOؼx 6 89VYOkmOTD(zjտC3gDyBr3Gs7n':au ^ՏUM\ yի|gErꉋmۦSI%^y1{Ŋ_;n]=w/m{_5cZWM&cF:Bѳ {x4|7{\۷,535xk׊u{7Lr߻WT\ehnz,UZl?~Q P(w/4Tڥr?4./޾-ͻU ~i7kvu ;lXyX;:;7 0cRG,LȭM,,k+=֪]v'w֪YFƅ|JTb\92gniU6M ֮[nիբqT2婽-nZ kpS5%/ٳz92WTU㠎ge>+|DI-=r v226ϭ{.rSs~̡Æo6HΕ*H9y#Z|4?)ƍxSQhfaUz5t]κӠNuBu۱ߦr:nϬTRM3snJOI߷~_LMOQ^|~A&\#zpsBC_dСf06!azRSMӧ:g΢?֩I:(*Ղ륹ޟ֦r;VMbn&l ndohժ ($ @/Jhoae!{r=aBL•WzOے?$JOkNq(Rco7eDH%7R(>o>Xx7޸w4/TӨf**uٿ|zMz\O<=5}qh8ɽ'Yvpk>/&YAn% GlQtNQhnioiM?QF>'yQ& >epiVZ|_]@thԩ6z>slLJ-<kK߿F7 k,EfۭeKQhgmcӷ()eB<<:ZE. رC|ϛX[Z~8x딮( j֬RIyňWCGʝn\,R(KNjxY{wjztMK cbli =(]M/F#->{~VgVHҒ+ͻ yHgiܺklmu WLD̑G;Si֡zOֳg JGHjcE7k5ժW:};yߙ+٫g;|f_~rĥ m[?\6lP lw.e)q_|,hDH2{(Pv͟8V#ZLۯ~eT/+y=]Wk9kWi~ɓW3Aڮ &Mfe:ۯ=[Kì"W0եa\bNLCBN\:fŎp AV< MW!vMvoUsaM%-͇tYNK)ʢtR_'{y+[6wEɽ'TY:m*'|MvA׫rm&&}=\N^^P(#cuZ*=%}q޲2%[1,§?}Z"66͕yssl:RBU֝^@NU]eeh[EʼnB{gSe>E{qΠA@C,Vw.1%EO:Rg#o?|XՐ-̤S2b#KiFi kRlK`^ADTN)i x40xpѹ7¢e4ȐcҢP(h%u25\=F ɁucгgtZdh?}* 9Fmx7N0;+;mKIq{gޣ{|U|t|PLWG7|Cr p4?X{wf展UkVn\WК[KNj4~3_#cJenڢL@6wԬYiC6o@]9}Z6ݧ~ *9S{3  Elj?p` Si:G~gL̤ӠNԾSZn}i޼Ss-W(AǃdsN^4i5!6莣ZpyQUЏz2wAh۷[KN6zk5}Q.-Jz<_ԞS]W/  jj*KÈuZЃs O۶ڴ)>VC#"y/tMkT6sG5_g=Zmd,S*ӼSRYQXO63$miؼACΐR]@f70h %t661.#{H /h@JnӺZP,dǰdRŐc5N}#}Wj;EI߱}*;zݠҰjͪZl'RvrإFMW>x냉't er 5\Y=>O[YI,UV|tP!Q& S(Kg0R|Qo]M+ʲ]~,RX%C z<*-goc# S~߿kRS=f+4MRj!!%!YG%cJjaX ɑfMC LIV$ @Q}\ʶJMJv_݃oKêhBý{Q>b1m4 |B}xs^(LbQcᮺΦoJ?7~}|wH(ƭjyz5[0ldxORcСkvue>=Z kUVekO"#,ʚ.]uG=MH(--'֭|Tojsmڷ/9--oRZA]GVxAw~^<++Bub߫ )&YOJSWEwj͵&]v>Q~lzJE+KCꆾPF(j7ݪgcgO.- w(iӧM\4VHw EYswsTY>EAzVkn^LDLZr4t `d_HOc5*$d=ZVk٨QWͫ:9eY3SSG[$-Cm]{O>|NԹs~Wի||D4ooc#<}޽>GFĔ^Ϣdʎ%u M&}EIa݌}DwilfzYQdn@8Tvrb'i1ʬGJds6"$CCOA;(5s6;ڍ_T6@vxF#n WgfdnY3Sҏl;rd{'úSͯ5VHȭ֔QP>TI))MW[!1P^$~/5|y4 PJݺI;vp'D)>[+qo]ÕO=!˶m3@ 1/_VVؽ @P$ k";+_'Daݵ_y.2<'JKNV x2UVEFˮ{YG7͎I}Q[YKCu:#P)ɋ&;UY {qZC6~1.*N}-vLa63.esQ575E\s.4L]if_+W=xU%Ҫ.ﭣ9j-[7UVVgTeeKa 33CJl@,TP@PR|(V~oJK)xbrBruCR*#E?3񻉲 ՞5{DᨙоblRԻT* +".zݳy99Z^,jろ[o?ޡ;59jl@G,j'D١h۰aՠA5\lLMs_;w<4iոqۢ|[Vz{\71R*z{H:|Μ.pN=wwϺukVurdggmiildy7:S͍[P15R* 9(Q4NTY~QeQC,ns&L%?#l籄I*zz,)\X/ Ns7̝̈́੖fst٫gkQ^%F,םl_K33Yi4KY-gcd>jܹpѣKMsqt+v%zQJOe0I&O|B׾>}zn].OOV}M=1@$ F:Tɭ_pl~݇22FY`ae1h?)w{Efra 313r;3sj/3]殺 fX/n'|3awCOu_'oksa?{l‚ ڜod,66:L\\ r2Y;;kkeRRW]ކʟU>>Mn..fԹs~%_%jhnn..yCUVֺ={81o{G%FVK&-Z⋆kFCVHW1@ Ljd.Zr_`d BYBeܰOmy:?#vǼWBnM,M^VvVyd~SSs EFk7=~VٻA2wZE6~zF*L+[oI '@ O lSeSӓReo;YBrT IDATCgX!5mz}]J &Ɵ .ޝRIfZ6jTuu9iٻ_dAeKp 0( 4z (9{OpM-cq*wM'I),TU6wldhBJ&lV9~}OX1QBn/\:GGS-PjaaEY6keYޗ*jbn~ʪTFz!C۰!-##o">ر2=3sݞ= gcK@JZk{8ma+uͣbncɏפNCNQ~=C,^zjgDs޵o=! }M%V&&Y,Q uE7>_^=y2QYsFLFN՜LL2aEhC¥km*56bĴwC}Wm!!6C>Vj^>+Yd֪P(L>,ʲ]ޤnݢ, {'>+&YAmm+vc˭!~p\bbޣnn;u2ܔmǑ##AlϚe&YA\\dϟfr0(F4:;|F>Dzw@RRU^=y5ySU'K sdh(Ġwll,23KwQFM4u(ߵj׼wh봑Rհk=h4zF>LKNM<[հxw~>^hc|r6(/eiҲQիE)l:sl޲Q#=V|FzlNI**nȚ- =xhV!::}(#e9 }w`kkT.yXӦncr P\h5/JzQ>4h4'(ueD=JMJo =P(nuܤyPZm1ʅGJãۏ|R~DGGF[5zKTF-%8Fʣ+x֑w6?tM4OIL)D9~6 K׷ޒWoٳҼ{ժz,||L8޺% 7h,3e jӮ4 tI+jocdȾ(-xÐ3Twq恷omq:?]&Y^sI=F(ʚu<7pZU;zqj7Jv;zv'qayHg.0Kw^AoܟP=z^بEY?;Z:UtοuOQdfa6ydiXouK~?Pe6(E;uз5 0ylbOExw?pe˴?x% ,, ?If- :\ :iPkKKU"R㓒_BѺIiS]W;zk1eڵ۷e 1F9V?*.x UVPee:'._p~3#GΟСnh1ks+'mni. 揚7tmx7vvGi/_k(;oZX|d]mGEO5-!6A_svօTͩU/w/=sL 2YʏyG}n>w. 0Pmԫ~U(nm<|\S3T*-GE,̤aН;I/:z4)_B1}(iӢdd^n..Ųii}EA7(Hu¢o<|(͵QI6_k%%';Gb#cM[X[ۧ*U:W}'GFj3k2)aL{ւ яцdP* 6dba݊uiy};dsS{ڮkǖ [F?:es@ 6,c=3fO?egw_8zϛ6e:Ks;w&4Z?kC?\yވy$ߜuVͪUsDSs1RSǽB.f<@ Pt]zisupեM!wݵko;N~+ѣuº_~~' ZGE;?GiOھ}}D(ˢMx{.4[+@ji-[i,m95F6 IW^Q9g~mV_~1W՚Q.5ٹgy6ޘp8>|_kL{_׻Tb-뷌>tʖL)z+^lÕP\^6qYϹ +T9vNkGGBQM~ޟQHOOd#_}Uơ5^q٤qQ<'FD3%\v)kZ/jiҸ4oZww_~g:I07eZ:ЎC̘۲aPwcթSuZ|yzѢCʖ}b"t1j@jj[o=k2E^?9e#GٳTsw.>r=3>ߟ|]k'O>O[|mO>ɯV#n:Vl 8眨˗6U._\|KO:gƭ[I^Z6~'O>wĈWoܺO<'Nr ?:>}jרu j n}&_ԩGe_ハ $G۶is#=[lFuZ"c}#J%zШC 8tKSzϮr#8U?KloqF%u/xE+О{ԯ=:䮍@xX00j@ޏrR]uߓw|{{qLr& ;&;l|Χz{xoN|s-_f݇Ϙ2#PvͺKjrJk&GjؾcDx,+q+s/ߺcG{qǓo1rt)_쑡.oih뮆uduX={~>oeC(zjTclԬZ/~72m߹7n7gռaÊݿ?g-Xɜ9RSqݺ[p8jר1s2KJmvyfv.j.կ|e-;Sn:4];Tvb'sl?5uرo>HJWąB3ǰaQGuK/|)hQRm;v gFןުU`&&>z㍗v[)ӧ9cFvڝ|rr6oQ?OqGO} $Y 'o|quֵN9F^H^ [4ɋ3lI{߭ﴞ:Vڡ5KtގYn|c7pvMiC<5j2榵Sa;$MJ=>wވwΫ~\Q,tUƭGwۤnhZ˅&=0ik8[njU;RkJuKpiKe kn󣎮Yfom۳>:tݽG>oމ\pVv˖]fMN{2ۤ{ǎ7 ח_lWg /@^R\g:ujOt,v;tu)jY8M<-\^̎_7?>{m% tէ_}F2uXZyIJ{vDfvTIw:wTfP֯\~,v߬_s\|%"H00z<utM@ {ug^vڌ=õתYf2ܳnź)Yygq>Z\\p^._y®]/3h8pӦ}1e@7?]+ormNM7zy0㷿6qⰾ}V8aϾ}Sr .]bEֻ)w?p\^R\3coF8gaPp$/ f.8 {\#R^S8%c}kӳ8;ѴMӻ_;9cTi@:Ou:<>+罍bFhqjo\Te$9l\0s7ӾY⬓d{\7OQ;o^wpqo%[o;gƜ!$`0x7]tE;\e{}̙y \ʹoad ;D:m9/tZ˖y٤F*>-CbmƏoTNVU,Wa/Svš'3kJO<{ +]Tӓ9 X}~em4jgTBBN6oKgذ/Y: ?gNF$<~͖+S :ۀÇ}̘ "I}q׮uYKV/[,U=녻rt->峍[5>rWJ^/\aq#rUVOzV^۝R/J,}280wY^3_eJ_ ѝ%`𚻯{do\e"޽}ǧIJ0vɓhHqX>ŪUOMz粉<]w$6kU馸F޿fV8㩧F 'o߽oH;w`P,iETzEG(Ҍ D8: `0wx~{vQQ]~_l]+'~6ZjGwOK\X^}ıʔ/s wÿ5k,ϼ̗y!QߙّP*aܿw67mO_s5G1p:IyܧTb~#Mq93]n7sEJ[k a4@ɐ[bJ jQT>we!.ݥ^uE>9sRzIfsU]TZgf ի3yg?4ҥJ]|7 ܩu눡`0#V*!!g<...+GTX;+׿La˖shӦO?ZJ'\}u8ŋ#֩s~.-5:f'Yx"*;/)Mzf~7~3ΩRbO߫Eg15 jK/E|NCP/8cv 3lhҤ~@ Pf7b e]OKO?iӌ˕)sC-Tn.zhҤ7gVGpm>>| 55WG|Ȏ)9ZݤBhFYQ3?95kfFhȩB%@``-.#yQV\E7sm._]꿟 S.[uNB};)M"*רw vug'_I,xsRI݈굫gl#na\(4O(O?z69Mzܕ,rL IDAT+֧ۅqy?zB鄱O9?g6]v{ MOK e"]  #}] 7l}R6lނ xقe֝[wV^9{\̘ۙ_۪Sv]۝SmjH[E^ .W\hIu2UhX ud(" ϝN{ /_nݦmۗ^&1ZJkjްi-[&UZ-]KVڼ}J7]c]۷pk.JN^y}kTҴ~S6-StvvH-ڰeK\(Ԡv'\V}33iV[t[wݵ7uj H(PR*IUNhpB mVLq]8޸zcu6ۜ%e} J*W\ꕒ&iTQFkWwMtKJd& _Yg#*6Ql׵]έcM,cҮ瞹?QKIK!I%%~}Ť 0Y(^}ȑLQoٸ}cqH,1`k\_Ub > 9-@ V̊uoںǕ+?33( =w ~%0D|/(yQr@ v(m}[6lI^<˿_qroM#ǝcPlH/ kė? Z,~8L,\xEvfBr_՚U %b(l$ ytː!uI)`IwX7@u\1+=e?.gw2.F(pdȑ' .Lҕ+THZ5!Cd`;t8Sv9I'_~ܲ;`\|\re*TPzPM_J$f2(A@IzMՋuǵINPi(B @1 4&&\ǭP2% 9 I@&& b _0$ Q|AXp\#@ u%I d"A,@c@$Y1G źx'I ;$I cp#$ PGdr*> Ekݷg_D ի֬~$ d}DqÇ~$$ ;d5IV,}b@}{-bًVrê )[RNCIJTT%nRuܠaˆ Ņb5$0Y}S3qƈJreNjRέvm*+6)8 +UTV k7hޠB @5F6 IȆp8ٹ0YH۸z_~>*k}w/{1k|ġL4|g)_&($y$O> M,Ptd>cwVim>k׭K^&6j(MI@' d @'L(~}CX7l>{Ͽw7jͪIS dhcz,Il-NmQi5**]`=;l۴mê ޵}W!w z]/ugĺ5IV @ p8|ܶ3Й9~wOL8_xs>(xWq2ʄBo>zh;f;n߈~)@a$ Eצl~D1l/{vﳳ^ k֫Y^l/fLT`2ܼn%k~/3cNԨ;#'_2nH(d%O<9ݭVZF7ߦ6ڐ J%nXvڝ4d쐽N}y i'tbn ς0{ c'D$UycPȢ$a@Ѳɋ#>7IGYfz5E eG}݌WRRo/Jٵlb:5b P$P_E~'ʔ/sϿ:Ͽ$ PЄEGT*רܬ]4. }F}s⛫Z]- I@,PY&Ra;bj_F8Ezzz~ȣW$Y%L(ZvnYr{)%N\|mOimٰ[ȵ̒d O,PGT~Nh?uۦm~0`4ޱ{ƭ[7nݽcw1pϚizs2}u)[RnܺmӶ])2~}?=R6lݸu/+$YG eS]ٹmًZwni=g-藟9RYfN~%g'J#_}¯&/J޴fӡ'׬WQF;t^mF7Y&X5j*}6m۾yѕF-ek̓?|ÌiNb9-===ya/.[l5Vmضi U?zwl~ZwkP2 56ǽf}C{Yk.ξW螱lֶY~K3^vz5î]ޛ5Lf綝9:zZ0`Ԁ7w4F~2w"U9:J ۤ N6gޟ5yѱ4opmW;P(6tK%I0 ;1L?"CN YJ~9cʌqǥlN9k63vG><2/9?qZ#ǿ4v1mW*B6pt1l_6ؼ@W#j윏\0.['S14IMk73aL=̦mFTV.^Yp:xh+:VtG/kgO=YN=bտ5Kq${ķ;W#ܓ$ Cnt9K3O}QG/S 0{W?/?)grvs^rgz+?å˔lN6M?+RvmYFYy~)X6>V/Yo{\6k,l#K-)6vlq[&͝P*0[4-[lna@ђP:aM'q(Pڣ[&Rip.IG]Jg73=-΁wf$[QNujܺqU@ʖ_Mj]qP MNibdg8wxߤIY,\`yD kڦ¯FW2===9B(jآaSmR7nRRn^yŢLfQ&/J2aQT%NhڦiV4SvrC}mZiLfQ~ʇn׵]!7 E$Y& 9F xo{Zui l9CoZ%Ja6VR'od B֫T%@ڥkwlMk7͘2fouQF#N _o{ g;黟m6M3W,^q9gsֿ}=Xl_֬WrY/yl1L6u[=ZJ{qvumWB̦O}yc7l.:t >96`0Nuxxuj7]\}{烿bъ, sg̍:`6<%R통7ݔ0F- q`z%M>-Oo9eٔFg7(VYOsUqP;srbġmթFg̓=uvڡ%$YC,P5nxoMG۷gߤ&M0e̓PBقKn=RFwxP\iP&|<ᾫ29\zZߏz W^]G$N;̓ nh+g&fɚ-DZ0sAM.~/OprKcnܚzM=C]{w "?Yۤ-n2-$;a˔ۤnfjUXbI޵9c=:y Q߿wVe%$Y"%gq3}?XNޱ;Ө+_9@i#QƳ_=odaBc{B2/iukӖ~4c)Mn}c& oy▓ڟq(yaG~uU6M#*+Xs?ޱ;lL=dԨk;n}4I{]%)q.[݊n? IDAT7Ea $j~4 H(jENvucLػ*;_,$ b@dG!VR.VAmZvj ׺uj;֎vƶV+.cmVw Dv #'d=K|{>sd{叮ѹ}kvvᣇ߿qnjkz|+ <-mhhx{oLFyy7{Cੇp}dTvy}M_}8-\tuܤ-8[y7D~T|ǦìW &نM)LHko?!/޾im_?OH#~d.:{jjWU W"h5 :kQtl 'sbw/fʘaŻG7^xpIq˗.Z\5)llգw;E#_={> d> .]_9?'7o`-r??ڷޑdl(vİ-/8?u|^_=8=ytⰢk?6wTp85iTK 'g^J *(Sc{±H/yvIO>:,n|mί/|=2Wk+0uBe٭HN>V3wvm |5> V8$ Δd7~'~d_ߋ.:t|y8mFD"yQΪyٜˤD<=qnmc&B_&7e攸25V*qa v=dj=zI^sokHd3}SQ쭹[n[kֶCچU99]v@j(:G!ߖ^2ٵFѸa}][~p=z(*)*Q{EKonźY&{@W^]4dThI'?z7=,y募zmm54[.p~QG'ɨF5wΈ2&nz6 ?rxp[+Uk:,SEͿ4_x\܆=,*)*Rx?F։}cؿ;ޱ{lRzOu\ڹihhرiƊ6nUU>S ҁ&YLLlSgOw񽋟^|7YlM W.yv__҆v.yyl;a-$ǖM-_W&7b4Yo/z!+X6٫r{e|mmv _/|틖,[o^oϮ=Ni*gjdғ2Y YgΚyo|csW>秝42^޾;p>pl;(xF շWޚaEyŬfN^_z׽8Jc/VaF'Lp?C6QpCߤ_?];+yꁧu+֥: ;M%#|O.(_p͗4w]_k] 8ݯw{ٻgcE"1 ڃK-m:<w:jT߂^hձݶL%~ItMd~3>sW$ - Úd22Y ҍ_E/YOvq֊G-&#7ƪ ~lظ򵱇_]zᴓ5}D)Ǖؼc]n[&yZ2$sg}Unξn -l^om+S]`lH,@ htG8z/uy\]ig~|קJ^$"F;5SZ:tLd{kѦ__zٲ9eJ_z楦ø2KW)|F4Ɩ?|tVivn9h`ekbN,ԟ!_~Ϯ=I&pNbc!_yxFviUt͕W|eS#ƍ|oay5ed2Z$:1sgZƊ.*͵^Vnl϶w *<:qXQ^Ł.yi>uH1'M;YxY׫=5aꄖgW5p^z|iQ}]7F`/c[$Mv}%6^+Ysr,td22Y ]pMl$2dg-v>zxpm/`mӼlNYܕ=']EEK^tMn[&%qo@ª~>ۑ2ӉnIӺ>d> LfpxH$ƖW6ZƊlvPhL阸IŻS&ZP6xKgxsE;P(uM󢒢!Hh48?󧵰OqüyW|犎 !~o?~#ҙ)i(ki]~999þ}>IG68qoaF=VS]jyӰlNY''Ϝ{R V/];X61٦r[?~4q^2줲.yvIp쩅ӥ7𛡓ϝe{^,@RdnM;bX'I^8~֭\׶=?\?8~?L7p͇ov>4,\~Z0`ԤQB5K'LpY[8_.i5`3?qO]|m<^ H!MG,vnٹbc|1>LxΉm>@nhz7(x}뱓dCPÿ.e~(odش#qI>$kv*UFyZ*dv H?rC>L|S0t@WVꉟ<8/TpYǷ02&nXn/t8|W&ΒwW/];ne^x꺻myy}]}𣊏: Y}z`PAp" VHd4d:e@_|$q>9]ΝwnzO7={wMz䵼6L?UMesZXW&[߿ۇ[TRT8 %\yҕ{O;i'M;}I5jy>& ,k߱yG7RBzU<~y]m][b@5$Ipd4_{vn!y6n~t킫.?L~{H+wW//u&ݷg_2 x=}_)陗١zOw^moDrd2Y ٵ羛;/?F۶OCCCyO}1H~nxۗ|{ӋdɳKnM~r28Ǟxl gp˿{9p ̐Ty'.t]rW]m]#om=٭lNY[|/j_/P裊]q87~RQ^͛ᩳ[6h4܂.:'}k6iiH7pX,@S& ںyĸ_QW[KjQ#v*+_8uF$'kڃCk)0#<#~}h =z&q?UW& ѫLjq#G:w U;>ZѪV%v6D"w7'$S& ֭\7;l :?3rp /T:c=mMp7(|i'OkaU/v龪}?"[玚4꺳 g~~^tEW6:2Y4@s~msꥫW/]zO S?E^s5WoV%cKnGѪUc ,D"|C.͙ɯS&*I1nWgmy~8oˆ-mX޷w]ŧB9yW'^S_W_* vޭ~27v߆Î퇿]:4Z9~d N$m ERs-+fnlky<jml([:6p>lB~P:4xZ&M9_M9%O9 O:_&P(4?ӫwmݵrO4mRֱֶ';:Urrs.G=ɉ\ë[^LIR& )3\qSgO݆ rݏ{赇FORO>}OڵNv{o} ͕ɖ)Krn^&;a'}'?xSsrsڿCew^[80g>_Ç6 9oaۖp`_+ '=;e\/3,_pݯw|i3rWZXi'k8dςrtRo_]rOk{1]|'>6>yo>vn~`|ٗ}بږ- [-ݒo &'7g?~O#&{Bn/OG'U[g,qk>`d2jdٻg~>غp 't][wŞ?Qu';G7Ɗ X;6h#:rꜩ3O9u6^S/.]tYR0`#ƍ[-I^w#/R^:ԐC̜2'|񖿺|,~z}ǎvYgΚ8mb$wfgzU`lȻd^Ϲ$d 3t2XhV/]n-lߴjgվ}F={=`耢#&1ף]ŗ@7\rP(ԳwςF+[3?jU;vnٹz~E 4yD ;6عeg~={쌄5{k>|mYzp8ףW>`Aӫ7jhhعeg|>$<nYjgU}G3teh [)(ԝdd(I,LST$X:M4R& K,MrSX#$ ЍERIDd h 2YH#dhNn$FFTB!M2YH=MRn@X#$ @H@I)$ @($ @k:t;MjdhY${$ @:t5!M$'-h@$ @)ΥIdTI IDATL:&Y:Vn@ i}"YE,D,tMte14ЩrSA`Y:P$ i :dِ&Y:A$ Si+)$ @S& Iad"D,LIMu5!MtH@$ @:P& -$ @P& $ @MuHSMjdHH@:$ @Q& 4rSH`lH,i .4Δ@(IL4rSR,IV,&JdX#$ @:&Y22YMd"et/dP]'IV,!E4є-h :tِ&Y2M$i k( ki ( ;i :t&Y5dH@$ @VMu05!MdH@$ @vS& @6$ @ W+@ hhT=!4M:]`YR$4Э(@A8$ @w@|ȆL lIuhMt[ddrS$F#phd 7HSM)l$ P(..I) M5WbII,H,Q& @"׮o$ rSN`YHO P$o4@:kzr|OOL׫eEpX,df͈y lLM@ y ,LI"p]v#Hdy|7B ,BYdQ\Ar+}å+mW[ ]iŶZ n"" #HG~?P%7o%hHD,zUkV!݄U&o$J`} & @eddHK)͓ n-ZO,@ݕ<a\$ \HlR4,TsdH$Y ( +E dꨲ`+H$"RU`7aBPB`JM^;@ \%Hblp(dI@aQ:E,@]3<$ Gp T 50Yʕ[VV$Y /W'KP`$W PNd꜊H\=H8O6FKAUU,~J̄Y-@ղ:@ݔY P}I*)s!0Y+dzBpu0Y:-fJYZ(n(dH.PH+-0Yꐌ GdTUll@g*D,uP6JʊsMZXD"QK1W Eǀ $LZ/xT,$Yѱ@8 %P7Hzlrwd@mD/#8 e& @;NR `yQAP?T )%nXrdUܿT#XvnH<4a:B @ TDdžlcm# $Ys@ 7P[0L.>@MT[.@MTmwmcPYĚ*I'C?g ml %3m8B{j kTH١d:PTIbd.@Xb m@a @9$L֙$ڭZOZ Pn+χCmP=YݬGu, .TvJbb/@]`&XҒ%K*32Uk׮eTT]<Zzf @b&XIC#JUr8)ixbgH5^*$4Qf*X@]\TȐ$ P!vj갋^b/XcIuDU=:(11UlJ]<;hPX#,PNQg25IR|̋$D('[xU^55^b/I$L3tNȈ$yjw)"N @*&+IJ=J.@#>{  FGPGY䲋PGD}K#!e @T,L6jɱ$:%222'V~ Z.b/Y @1FV,x)Zb/W0Yϸ C`O# ,vH.P,`rʮ[f# @]#& (r^HIq.D"d*.%!u^#N#N,EQ_q PUIdR.Uh>,P^(֊bI,TH3<vZ˨} \_P.;@g,PZA̪ZL,@Yr|,E,I@dɒ4w@YcaQ:nQebj(^C,TT$j' /u\v/z!,Yk׮z$k'b&:eg(8(?@F"8>j$ EEP PY,d:W0٨@=~PD""eR.@-`(2dEz>F1R6e*R(ATPY<,SatdTP X0Y*+Lvɒ%+{\GSԲ]<b/Pdw'C)$,*@MdaHD,0Y#e TO{]dIFKʇy"eJPE TT0Y)]TR)#HY,PQQT>RD@@aP)f wU Lj*~O5d"HY(0%Kj}tclDQ, @R$e>YBuk׮ӧ}Ѻu_۹s:[nz֭[NNNھ}?UlժUB?ybFFF֭0 X⥗^6m… { 4bĈGQQQaaaJvvv˖-n<֯___Ѱaý+͛7ϩ!''gMZl^bO?M>>={w}✜ߚC9C֭[z*P%)Duƍ_iӦ͝;(-[ww!'ܹO=kܸqƍxY8.((+QQj޼y2Ԇ M|g1uQp|zai&q۶mIi,1o޼y6G1E 4h֬Y:((()~| n։'&=jٲ~*ߤIVZu8p`] g͚{ˣ<{'|U[<̄}W~ߖ\~ƍKxP(駟|7wܙ+[ND$y뭷ŋ'6BVVV piuܹok^&-/?G5vp8>CpBo\fM7y{}Qܹ^zkVZUرcJ~3HIxo=͜9O>dF+9TVlI(oq֭`}Qd)"ԩS˗Wr˗/_SO& 1~xEݼy|U6lX'j¯zʔ)PAGyȑ#9昄#2 6\}s̩ ;w3gΜ9svqARTT'"v8;wʕ+&fǎk׮]v~ZRi۶ywy&wb˖-[lYt~6mW:k?rD| ndWTT8ƅnݺnx7+3H8?7o~饗&7j(gtN;IƏ?xM/9ri_ֶmۨH$nݺ&-&y'|ܹs„ /+$ 2H$NblYQ5ג%KI&]}Օ mڵ_|1c*$ 3uܹk֬I\oFvs޽۵k ),V۷曣G>cW7V:+$\~СC瞚f͚o}Ȑ!Y֭[?3Li~~m߾o'\$ٚ~8+$ P u2 Rࢋ.ڸqcׯYˣ ?_222rss\STT4z_=5kӦMT1D+Wᄅ.x*l`ݺugu#K?]lٲpx֭[|%Kϟoݺ5]V>7.m۞p  8蠃Zn]rv8WXQs2{.]S{t^paT=??K.IDqJFFFYQP,Yk׮Ȱk/; ZwςCyGqDz B?[oUիוW^YѡZhQѷۨQ Pq4i>lذ`W^ݮ]["???j O`OSYA;#.袬̌p8?oٲ~X~ʕ+-ZI۷o꫷mvꩧV_z饕' ^,??w :kyQFuܹsGydI`̙ӧO>}O?ppk2eJY1׿|~[Uqq_|[oM{Wo+{999$ ;wܶmۏ?X8z…q.Yd„ q;.ƍǏvm(\~=^8Ѵܹsz`?~|5ܒ *~Giʺ$m>唝]6lȑ#5ZV5Maj;vs=Zcg[>SN9۷O>W^g _Ax6,{z}mp &M&XXʜ*ۣ+Wcǎ1/ٳ*+ue ,۷o衇?t(7nK:3jԨk3ӧO>}nw}7y#ֈD"qdC,h^}3}s^zkOyO?4wǹg}m۶_-((xߗse1իWx_PP /L80׿;N:gC=tהuOǠAڴiSYu{lܸq)%ʺu.䒘Iկodeeׯ_~y'|2XK.n(4xPӦMO8N8!GjF*[mX֩S'>{WDo guV0oР!C~ԭ)b޿z衇>#dw;?~/-۷.L6DɖT5'Q1c֬Y|A<'ɖuq=S]vYH$"1mҤI]wU$(m۶KcKf͚k6f"Fv^|s9Iu믟1cMʩcǎݻw5ŪU>ҕN8!YCӦMCM21)..֯_*[bETqĈJ--''xc=/+裏~˟$[Zff޲n-Z=:??yO??bj(//+x뭷 õi7ᩧ*((Ht;wo~sƆ N8kMq}'p3; T_d L<9=f̘*ij^BB<̘QwqGQQQ0M6Eбc͕jv6ĉgϞ׫WOTbPbY@"Rjͳf͊*|xRFjnK/ӧOގ*壏>va۷O'wygaaa޳g;SFDl믏of\VVm֤IK~RgYzu&Zl٣>oO>is@yt[v-[=zfֿ~)عsO?=k+"X8qΝ;yyy999+pxݺu ̻GVᄅ(=4L4a„۷'x)'RJUI_rH;ٵkםwiBPz'%Lvٲe .,]9*?2Pedd7n1_0aB^͛ ݻwO'-z饗O0~o)1#Fh޼y>o޼SD-Z8bt47:v0wܱk׮`3,\U$YhQT{U@"L-fbi̧4np駟/6~g4n8XPPoF333۵kULCWXQ֭[b:uW_})-[6y`I&7xcJ,YR-P]_>XNkQ[T%z}g(3a˗/ȯzTePDEY&233׿OBEo)gD"ѣ;uv=p`hÆ 6,f~Hs')//[,S|Wov޼y믿>SWC[nݶm[T.@9&d?,nڴ)T3fx ٰa#F/Bؾ}ʪU:a;v옊YRe˖wߝI|xeꔼ_1_fSM׮];u`=///Ns3ץKߵk׬"ɂJs=)tĉ1GkR:u5oW-K(H{P!=ի,Λ7/T|P#PyYq05kTf견\5Ph̘1{7gΜ͸uW^y%Xoܸ矟IΝNV  ӧOڵk׫W`=??2.Zhٲe+G}te;I&ߧ&/Bqqq~饗榹+ֆH$NQ{wI:X2eJ5شiSgk5k3LѤYϟ @K >8 ͞={iʄw}7X?ꨣ*9r~̊u!jժ,`&۱cW7v &ԩSO>YPuz}ꫯƬ_xi ,~~mccztH%KsJkԨ'梨b./X૯J[H^ sssO?D?szӦMIuDo~o&Y8``Nь'O޵kW~i%ͽ+]XX;TI?PSD-Pg tk߾}ٹHo[ݹw?c~Wrz;X1cFT}Q5k^yˮ]֮][RdCرc9s|.棶CЩyg}6u7C5D|`=//u:thz~~~bF"`# N<ĘūVZ5j(X_Z\\曘O 6lX&MCr999mڴI3UnÆ EEEQ-Ztԩ*Ik6X>}9sR1]YJviҥK~w /b0:K,[zzO6O?UO> 4iDzhzaV3lQQŋyyך5k0nݺżs„ IO@޶m۬YN:u-s@֯_niɒ%injbѢEQO:,1tЬQg>} Ο?ե+M4F겞={h"X={vҏL']fff߾}O>NOn@-\0X֭[\k9sf8dȐ`ػwc9&XOŽ7oO<0~Ӗ PC *33 q{o]8x`RtAbAAA}:tf͚5pa˘Ο??`@mM B\sMffd|Wofr'3gή]c93СC'{g}r1 >4we6lXxo&&OU9srr 233?`}3А!Cby[o59d6;`TO 1z4hP`sM'I7v`qΜ9|Ar'cL;k]{EEEi& U`ȑ7<0jԨ'3t钔;w`_6mڴQF+pa͛O?-[wXZ-K.'|r~w'daGm; .Y߼y9#|؜9s;,@LeEL2C_=x'&@(ԫW߫Ç7o}8 #G,p8?ϛ6mJgW50YF:C8A IDATz;;X{ShѢ`cǎM4IPkdee~+ŋ_tE\pܹsUbӦM6l{f C|ŋ+4ɓ#Cʩs1Qg«>{e}c4i?ܩ.\޽{N<5kQFqQF_>~뭷iӦJZJΝ;z]w2E$y~ip/Q\\裏{lْ9aP5z:ĹfÆ ?M`ڵ1-ZH-[ ǹdׯ_nݺ3۱cDz DW^s%em۶żHPt;ȈsG}tg\veeOlٲu[-T7CW+??ڵkԩQ#FT3nkժU<\ eeew}k[n{gwڕ.]^'O裏tṖ$b3f9rdpgaܸq|:W^ye}K.}W2ƍnߑTӟԿ8l۶<}Ѣ5PeWuPw=_|7|UVp >W_=dȐTe@G7n,ek֬Yb5m4X*۷ /^\TTqQa:u3Nw=rJ )5|;wsӧO>tkK.ih,???xu9hb mݺu sݻwe+VYw}:bܹ1sR)yyyOW_}utf͚yҕ֭[ӧmw]`AbmРqW*{DգV!iՕYݻwv*f͚_K8[nzƌ3bĈ, 8զMJff3fL$geeOQ5®]f͚5iҤ?رc/wjm۶=s&MUFfnk׮Y$Kx+rƌq.+,,;|+?y\Ydb 4 h.Vy4Jl,-(knu=3=Ka( b3ߏyaf>?z1s?s0^Çj\8.]2駟3c>b5oݺ5>>~[ߓ'O 6mZddj޽{fVH4jG<#`aaV$((H+lZZKJ233󩧞ҺьAQԱcnܸX3XPPaÆ1cƘعM65g[tt>oڍ\mϞ=8pI׌3K.TL`ԩF>j(C޸qC4T jaÆ 6}^Mb.]:|HH!=ڵK+cYΝk~~~^Z;mӧO>}LϟoD1YDiӦ>(%%E˫W;SLӧ)DQ򤤤%K(Jՙ3gZ>sIZ]YY;w%%%[6m:{{ZlRVVۺu+bb앨BHJJ?~rroߞ={vrrĉܨQ}f͚R{1lyy֘*хZ%mB?~DDٳuЮvʼn'o~ԩƕ{0X/F&j˗ݻaÆ7o6mګjѣ oo 2>_}Z%K 47nؔn\.g}]U~~rɓ'1Ԩ௿2gg-Z+,`}RtذasݹsgFխ[wy',,׃DzhWUTZHJeFFfdԨQb'^zU󥯯}z׮]o@ǩKHHضm[׮] iĉ_|qԩYrs=B+s iwjy_۶m ъ|ټyS\\\z-ao=Rp8;;- Jxx]f͚[cUVk͚5 4=|Pdq,lQtt0x{wڥL0B;3o޼~!<<ܐO~Wj I''ZuRRSS׮]vn?cTmѢŖ-[r%YDRVVz꯿:==ƒvcرxbbVk Ll#<o>~/\[oYYYHldnݺk׆~Ν}`]UDMPlXXD"ԩfڵk 6%U/kKKKWXQ/F?s||G틋̙3x[ =6Y`+Zٽ{<1[fST`}?}W޸qSO=eH~_~k֬i&LҡCkb}o޸q!C O?F,**oqѬY(Cڟy\]]~#;gΜk'R$J*///**y˗믋/VȈ{ꩧ>VZY|xrr5+Vx듽& [v.\9hРPhkPbMׯ/^|M=_}bU4rOXL6//OeIIŋ_d;J$vrRY}IJΊ6jhSNՊoݺuر5kͽ{MKqqS\nz2//ѣG/[l͚sB}ROX!{.da'ߑvޭ{>@STT$ zyyY>#JۻwEi-zג[IGZ O@@fTG#''gѢES-8p`VV֗_~)<<>>ƍqqqVɰ9::V|G3:+QTTtagϞwnSy-[:th…YYYzZ޽{wԨQ7nl۶k/҃b@=ШQaÆ 8pӦM˗//,,ʕ+ow}g1u+..6#D}nPPV$??_eFFZ~٦MB!H۶mV_JOOWT2eAAÇ5b$:::11ARhѢѡh\eٳg> h 4i駟3f?JrÆ !!!Çdz0 'g9rV|h=TTThFڷoߢE:4*JXU{^ ݶmے%KnݺeUЭ[:88j뛓#l˵JOkD"INN۷ohhE!!!k֬Y~9s'B%$$xzz+Vɭ 4hʕ.] /]t֬YFtX{%EJdgg޹J={|7\|YW˒qƥ:FԩY;rrrz<8~FtV߿{K&V[M4߽{\=\tM``VDk)ٳg5_j.z ӼTRRT(77WWT&M6MOIIp6mT4~U#z͛3i_)b+W, `ha0???##CΝ;"O 4HwV̔y/L䤫Yff~G}Tw~ۡP(>sa\R͘1)T:re˖a={vff峲\>yda|˖-n6@2,&&f޽~fׯ_?%K,P<#ӧOZhŋmyݒE0f_V/GڵkjeZZU“5+j 9zgϞSj7|cDo5@lM6k׮ubXZZh`͛ƍ;>}ra|`aacǴ@seѸ7P(&LjX\\몮 ?8>p@a<''gѢE6k޼yxee/Z/$$D+T*qqq+a \ꫯٳk׮]ldz)$$d۶m;wz[8%ioru_TTa\kF5#<(,,ZVkևo1YaUʳyyy5>Hӧ ?SzziF̴fXT*9r5k hm۶Y8+e˖&.޽[*Ӛ7ܹ詢F8hu*Zle˖ݻ^-))ٹs{57ԋ>KONN6nI]0`СCkϞ=d2ٴiӄ#*JRbPJ&M֬Y#ZZ"TM6Y8% {rrr˖-EݻQjnnYuh5|Z/]v긫kV_6oMF͵nZߎG߭[7% .4; eeeFjԵk%KHRᥲ_)8m۶( [- ^~]XjDX$&& hPB!.$&&3J$}麱CǏ'G *jƌO6...ڵk-eDFF>ZAZ7ћ^"]eaF͝;722R.(& cnnn ,{)[>}`NNY:OvDbyyyU_hN ۦL& lpꍸZd{GjՓO>)_j0LDDk&zԩSNj8r ըQ#a|ݚ/o޼yI͈L&ׯ_& Z-9,,L.[>kQ( 3gT*]>>> O8QRRb@#""񜜜ŋ[>5h a<--RӅ֞O^Cն+Cx)77֭[O dC?0^XXXTTd| ԩS'a… Jγf͚y{{o֬VڵkU_h jQ*ΝZl][_pamwEthjjtqqq¸e<쳢T g#<<\߳g={&v[!;;znݺY>jժ / *:8YQQ~s&W7RY RRReCD"Z>tڵ{xBBBmzgR0Zm5zhK/^p2`ܹcL ׵kWaw]t>00P+WEmVR^^~M͸d2epaә3g>\~\\\DZ=z^7)'uMO?t-(::Zq䯰&rLLLݦ]v#"",-5gĸgϞ 6ԋBӦMU*Ռ3- j߾h… ĒO. 9r%t|Ia… gΜ129Xxaa3,^ub@סCxEE31?ޤIaĉ&\ZZ!?snϗH$fbZ>#5^=,XPE ._ Y'[rsdd0^YYj*wݻMOO׼$k;;hܾ}0k}3bB݅Ǐ-4rPa<''gŖyxxraXLhh?//\,jI$Dc2@f܄Z>n)&b)W™N*G?|=9rR+ԣG]iEfeei; @3RULի`CQas IDAT ڳgO׿%Z{ɓ<==A[rׯ__5  77a|Ϟ=*J"ڵKR=DߵB}#%%͛СCR:ѸBucLLW 8\>gbIIIZg5L*JT ;/Ҕ)SogΜ;:: :q'JEXyKq___ gR+of=$**J8i8yyyj,lN:ޮϿ}vnnf :t0sZ2dKf vh}1)))m| B=z(H>|'- 9sF"64 @m-ZHW(Ç|>@tXRӈ#Dׯ7CZu/$$7U*U||ݗLэ7Dv_M6kWs>]d`&MO;wnٲVPTر>ܹO? NdHYYYQQV1ٰ0ۅgiFH1YD2aуk3gd3#!QwDgZjeL` \>fK^n ԫW/ѩݻw_x1++K3ػwoKyhev#FxyyY>[ :\6mDFF^ϟ?oڄ Zh!_xqŖǦT$#.ۢ{jkرRTtҜ9sL￁+(((**ƙ`(& {|I'R;RtԨQڵk>NyڵZ-[v]AAAZ4͈bZ\H'`7u'_+Ç|r4UTT=zTܹ品^|@K_|ůj|C>s޽{SSS عZJ?~;w,ZSӔ)SD}= LDäV׭['׋ V`T*ۢEnnn}1#ꎳĉe˖eggת+Ji[j5`o ҊٳGev휝Euvv ь_rE3Ҡ6nxѦkD"7o'|RQQa.\`JJJhBa}^z1c6oެV-_^q`BiR՛7o~7+S|wK.u떉]ω+bq d_K/-[u=8'^JJJJOOӧۚ,ѫcƌp>V4n8yW"""t]]jհaòM۷o_=͛wJOOO񨨨6mژ9&V_O0>mjzڵRӦMǎk4-A.…b/_9rkjATΛ7oʕKѣ!ijU1Y-`Oƍgz?Rtܹ=Ν8p{wU@sر &DDD̛7/++KV׶w^9rd˖-͑&sҥκ۷/***!!V>>qqqw3ϼVVV&$$_ڱcGmK&%%ٲe}yqJ_^^^ ؂[9rDACCC-5nxѦ#,Xٳg?s|gˊ{=r;vGUVVwtt|̑&آFNh*++߿~WW^zu[]*/>}L&ꫯrбńN<9!!A+~…̘1cȐ!r\ެYf֭[ר٦ ač;v1Kzӣh͚5fA"d^{͐;v\h[oT*E-[,)))**o߾ݻw5TVVyɣG:u֭[]ʕ+WK/w_ѷ<PٳZ'Ʀf3iH$EOYrԩSN… #-*޽{yyy999:Z:vj*OOO䓓cQACQ*|XZZZTTTXXxl]̙gիW=RΝ;|{^o޼)Y_f|'7577\i899f QL;wܹS"h",,,$$ENNNwܹtӧ= Ϝ9[n<ƏgxQQч~tҘ.]4oÇ7n矏;&ڡkbbb H-tlx1СC6ֲ|tbd E\\g}fzW6lxsssuQՇ>|D"i֬YV$IqqqAAիW/]dz2زK.UUurrرc۴iԤI777LVZZzƍ'N믺5z{{XBPϼyLASDDqdWXa͛g\19s+ GGGJ$={.]tTTTݻju@@@ppT*---jz*GՅ,sn]L6%%%%%,iH$S]LVPS8w\O>564$Ippqd7ol&MD1Yh-]:{ٳg=66/0}\Zff!D fH1jeee{f  k۶m+** /]t̙_~ER3fDFF6aTUVivΝモ-[ vvvV*ݻy˗ jts1c,ԧOP]w=lRcSɚ>b ˛nܸQ-^6+&6nܸ7o^WƍڨT U[jU5Voݺu˗/8tPN:m۶UVM6mܸJ{+WΞ={jذa#G41ӧO>}NcOOOw )'vnݺ^q%K^Θ1cĈ2d2DK^v-)))))\\\۴icxR4000++KjXXӌe2Yǎum2kذazjYf[n4iɓ'kl\0+,PSN:uʈ{}||֬YӬY3gkڴiĉ]Vc|FSLV5S5 &{oȐ!sʕ+WwФIƎksR{עEYf=|PcRm "oWT?U&M\]vN:Fzj9nժնm&L`H-˗/Sݸq$4hg}fPEfBHHH0z-ptt\lСCMiӦ6mz'k{cPPKaaa5ީS']k}pppx՛uN`>:0:l۶v"0;NYDDBԸqnݺY8V2f̘4w\6~ 5KV'̙#ŋ/|JVھ}{k'b5 "..\mڴ)..N.φL&M2eܹ<=ִiSHxvk>aΜ9_q=Ʀwu``KӦH$/BV՛\.ۻwoLLkzxxPO0B:u-[-l_~u֮]&M+Bԯ_?)/ѣ񞞞T52cݻwOMM2dYzkӦ֭[͛gamTk Ѳe &^JJJJOOX&%}ƍ[;+6lSN{>}zol۵kO0A&c$g" ;['O~LRiD?r<**O>Y{e2VP|Rt+Vشi k׮'N֭[mX-88XW† qƮ`ʿ!)VrԩSM7ebf;)Slܸ155FtҴi^zӧK.]7 /Ǐ?qɓ']f\?>>>C}|||A*GSjlSiX.uߩSקmذa߾}eeeFtҮ]3"2zҸWBFwm/E f?cv޻W)Q1pj˫I&=X۶mkjc 7^;uTpqzzq 2,22r]v5ehσ0`}6oovصkר^IW|Go}rrr>[ zk5/Je2OPPPun\u7b'O裏⦌ŵlrҥ9997nܹsgaaV ?SARYYYitbRۥy'NiiiuޥKQFj[& ;̈́"Z~5m|4---===33W^ճlK.h"44[n^^^L߿:uJ`Cã[nm auJٳG?}G;tOw޽u]+} Y<ڵkGFFƹs.^XPPkHHHΝûtφرcǎKKKpƒtmݺuvx≧zqƖp}뭝$..nԩx`PVVq?jBEEr<88z2,((8|'ӯ\RtئM;v9,,VT*Ϝ9sȑ,e:vO?-[;e޽ڵk4<{lFF/_gB*vرK.u2`hȴf)& JuΝ;w>|P*:99rLq=V ޽[^^.]]]Oz\~ݻ%%%RۻI&,,IuYRV׺uk͗@}5 $jćFU`bPUva֭[kd]dhM5`Z[|\lLaP1YuQLij)&,&˔H: Ѻuk͗|l `^bZp(>(X3``8CH_1YZm<` X]֭5_2E_1Y-ZN,Ok{C4hm IDATP1 8(& r5 u袵ċ-;tazPLЅl5``@=Ps1YZ |,,{ `/, *&IY<hb&{P#bt *q `h;`/ ah1Ymc`{99 (&$p5av^0 5vHYe S`djI Y}?Z*uZY<Twx1 j @bq@Q> R+znԯ~}3?@JЯdSP,N{|62ɈlF6 `>4@@mպI ? ]:h ڰ QIW 4( Rf͑lW&YP,=a`h YLVQ'$ILn,Y0`,9Z{ pf+&+ C.Kt,@aQ\h8c VYLV]5'_k$.Kn,1`;^`0 \LV#R>7GDHEsvh`#; dXL ˡ@C&:B=YTc6 h o FZӪ*'KIb@sͻ1`Y{0yVQ,N'u,OBu4,a h3 h:mb@eGD? ņS\P^^AkP]@W|( G9gf37-XޓM3 ̇,k9"LU!`,c\ΣypA LIaF֖-dآtl!%Q$e9(l6A( & vדyJSƥ0#krb ,S:*i P?YDL1.$TB J[j! fa9_]%XRT !Ypp;~|ÒGuX~ԍ 6EL + )( ,@?dd,YɆJ*+%e`;?) & @6gEX -fdmGUb 'ɒ@dd,}1FX:_' PNC h9rlBH= ؠݡL,!XmQlBcKPd 4A`,dLղދ_<*s! fmƲlB^J>@fw0w1ƅzKa㡖􂝙drO60Hel#IҐ`dN==LaUVMF`&&eش†lF1@Lr*+^WK2zKUycI&Pղ`\ `v^+ƸЋ !TɋiѪ*h̍,#S[O6IK,ݤ *!# -dZ0"Si_sDCJ 谜?/c,ۄ/UWDž0mC/2! 2iA-Ϫ8“SA`t<("bw6 cu*QΡp/̐=vGL3dy 5А 1YBcU2 0,YY`_+%e26dgh"& @ + lOaF)$P*%$e!# r$K 6RP?{L,yY38 Y21Y֔eu4A `U2,@r=vGi1Y6#:gi2YډI9m֞BˣRh`+R쬳&($# JdآP0 JLªh0UYCIL,,0C2,}ˉZTKC `0)YXKq`dXP3 YGLZZg-M_j## P04 Yz#& @rm*OFP_+L6bm6dg&l,#ClUY6` 3NE1Y$$eP1Y'gڳnyT X,U`R&dd,#nѷ<*GC0)ـK4P?1Yfl# ! 0MVXU`td+L6q0 IDATu Y`r *+Su@dY 1zO_t}yv^{ysSqi<*-c! lϞc?pp5}.c> =ܫ]2?~3'we}޽xΎ5`R<4&(Xwg7-~w\{_ /˿v\ |_pyq}ٯۏ\ƶ=7э{]y_W]N-?ߺW^mcяǶL@<OiFKJbZH=ӗ=u*t7 C/`d`o:WXU>ԇ/K_3׿> yWnӛZJ^u{བྷ^u.z۹Bx^>uԉ'~'~='\3@91Y"tPg]|S={dd`?ǟ~~л=*w=?O'/W1>:|o9?g kX,A]* cqֽtoo~__a0+-{o=,& _=}nY>uEB_~ŗ_O}W^dgK*u! ؈徭ߣ[t}أZvz}*t?w}aIvv_wEl Qʭ]նUf}1zGO[&ۯcimMxG#/?{}|wM@; DcMDS,-ŒfILL5PDQ!]`{gXf K~\5'72("vE )=7dM3VP("48VdZ7ȏ%E 9#, ( tavt[ @ YjEx9w۴@p[o95o+x`* Z7}'[(_3?( +@*'&I(rXhH(a +r㇏:ӱGNE>Чj8ju<[jRO٠%J!?[ Ro k V+%LV&-s(ZmVZjf aSE$RPј!K,Ph4Mw?‘;|&MPĜ{~õ琞‘PԶ (]d(vDP@T#ɐʫ Q'W*U4Ύ .,im}^>%'U*A2[ioo:/%'W<=:vvpt3Ͼ?9:;kW,G9N>5|Dcmm ;#Q#U٩jN.N%j>wuwPMَ2[VjZGJ+U\IPt_VNKNɕUnkߺԬ_&_p'!/7zV?UGO2d2{{wOw//;{;RҲҲjBpp[YKegd$UjgWg>|(=d"GB([Ȑ tЙԤTJji/ɺVL +0g;q|u^rV][c^>^{j.m ;s+5nwmzT]G/,$̠oD8΀0ѽz'j螣[#FMfz;o;8̍=s7mR}F4ye{،fOwoݣm$%>ޤm\eȳiiNȿQ{ 4FwUhګ}|  dΎkٹe]fTeɫzmDR޸9\L4]qFo:p@gWgy9y/uhvSf)[\8}t^21YYn C~j 8\}vw`G5hܦqЧZjH`p6U.-5;5-9çvw5Voܶq>{ UQmK;W[um%VvGEl8}tҽ$]].mZ۳݆LҰUCKɹQҀ=@@if5Uf{߂hmtIuR"MYc7Gzn[S=>`6Nݫ+y9ykGFVS)F9zڲi”R#=UVMf '5Ur8XΐBIo\ݛn';,bŬ[cWrc03N _0(_ ׼^}8ٷ6[밯~Gn^ΐwD#P'~0OސxL&Jϊux'L b彇l .|S#$)&/MSk};h|E/6=6ΎV<}E,1s 4Y ;OÀ_/b9Q Yr&̚}kT"qw_˷LmT͐!dOX9u[^:,u[wsܻ&}4ɺY,⤟$VZƕ <: WBҽxn'Z܌~3 ˉްtW"Hś_sT2+iC5gF4w+Tyߘ4^&Ӯ0)-%m49Bԅ Q/iAҌ;_Y|U)sk1#qBQ CJIٓ=R$3@yu2d om_vnViL$s)ݧܺ|Z78cB du.z* ($Vi b[k]^$w9<ɱ?g$Y5}Vy`[q߯xmjRdun1+pVvFv1f#zfI:<ՆC&<qF)42)RTY@1D,LfP0fBkWG81rGdΓ ~7zw=Ό>0.n.s.fz͘{39M:ŀЯh`kh)|\3:pjAfe͘{`ˁeyxZDҌoK7?Mf+_K];}mJ)&EJV7zq6uggo$~s*2L.(\eΟwZUV*lwZ.=1*՗d’?tǮGV£GqE3%(.wȄlCbtLtr"E +cwomPTf)9T5gМ)[=x3M6UjTQn=8q:jW~GSzLQA&c[viUK{=ӻ7N7ݒeΩR+>5}l_?u[ Fvf/hbjodm{ggd߾r;z_AN屽\cvX/Zm?)zL?zݏ>wO __.ܤS4oTn5\{>Hbc2&["f{Xov onwÅ? ov|0WnlR){Xonvprk'~޷fkԕotF笝_{ ]vY3gRNVi@џAK7MyFЯqVN]y&pȳbndҮKܨ7G )*59ujIy?SGax_y*M6yoҳsWN]пaN-n=~c?W oQVZcyhda+g1|(ȍc?k;5GzՌ]vHwL/[h-:cɃ^d’sϙ_۾viajkiPt/f 8xˋnayEaP  ٦g>G&vڤ{Iz.-WU/Y~,%<~ؼ]<4.3-SK7-u&:i [9uh aSN5#MM۴—׭򼗟yJ+iiټaƣ{Uj{"}OFiVyͯ޳UVE|%b<_c2,NªYD7㓿?1Z k-ݴ.+~9g"q3a!wO 6=_T)>| /-z73o_nn;~qw_gL&5cT̹w菏tǮ~L& iO3g<1xTj4Av0Я^pC­G&'qOaӳ.S,co_tDڀI{73>4|m{-lèz 1.2تV_FGgGst[:yiZJ~ѳKs_Lm?>$~>$G̯vU^EXðH۝p'AXn|5gҩf]gW2S3%-Ӹ\dLKcmeKhHA%pl`aW>,_-Œ]_^Xtp-2Uǧw4{/ju)Tl2')nT#?46P$pO'VbVa2琞#t/+WN\}sO%>e2 _xa=IΆe,hҳr&XeqT[ IDAT]Gg[Wxxlߪ5 &Q.om_>](D|5:iW\zze*Zv݂u欝ӢS xz/߲Niʍ Ż߾h"uMBaySH! Y҅ nt*FwL P ?79 &7ޤT["ѷ3]P(欝3A(sr# ZviYݯ.v(O|2)RTY# a!7ǮOPޭw"DZJ]G;trpt4DžsR`ē'Q Ecoܶq&uuz#$]ֻ.}]=u5qF,wD_xxyisTT)yŬ,пǿ}llVn:nmI&F+L`go',j4wR]0YM?%:-%[zm$`w mݽ-9wUWlРer9s%O3Έm|r졳¢\.oյ-ۀ [5DSg9:;C/j9/k^jf/.¢Fxk,sH1&]>IEm":t6{:(D^-3c5*|{PΔs) #k~{"[jPȉ;6&qN_7jH VqM ZmpvJK5$`k֯j6 E*M,%>znaq-E^du|-_DZ mڍjS'(Y/]BѢs tqsiتs6Ɯ]H-.}I7lG h~5,rmE?͊ZmihCz,Y=+=K`{I#OpBjRjIS!t׭n5(lw['2@d@REalr?<h48QF~2=~n`G/YӖ=ʜ0J`PcD‹jK̼o&Zm^URh#; 9/пH U*;#[XmR ST46=N1Ef sJ3؀FڍjGB/Xnus ..n @v"Aݤs쾵}%CCǿ7DeRR n9oR]=,}}PP/9+'t#QNvh&2)*d 4DJ!d(m';|Π]~eǽ3eȽN$m889ظ @rpAMƠrJW9w=ί1ge6?]JD ߡv W[p (_Ÿ~[& Bdy9y`/Zpe;{xh +2`PP:o/DBŠ Wݝ(G(kmCr)48 $#Ov)i%WK;&YXt& $gG-l@j۴}S~u|+Vvvs^9F7llU+шg1KWX3 a@I2b @(ݯz/^FMX؝6VJB) )g-CC_53"YKR*87Ѻ2[i99¢V(j;q&wQ3F <̌3Tή΢uI {NPш? "Gr3-JV_.]m 3>QXDS6eŁ;Wܺt32kd)ZOKIpT黃oV4Ic6X1$Yptv]}$-Y;@EC,HY(q&ڠ(LEhpbfZF)Ĺy Pqw!zf[&efO; @om_zdKUf+3Aꩫ~s>o"}&KpهwJc%(Kt u[I3I ޅ{mj~ 7/޼}v UٷOKN\MdS1%>W٬ @xEBpzy .7>y*egdԓ[i*^uӸhε;,=ޤA觅? lޱ{߿P(JN:q oWG]3\S&yVtr7{M((̮|ÂyPǜѯ88: 8̛L" da!aSO)lVu{l&+چ&_Xa-(7T:Фm8n0L\o֡YaUyA^=V+&9:{x޵MVL4_=uՒe ޸mcK8'{LXt*Oy,6>AQk38Js.FXoҶ>)j|ڧ?exؔx 3t{I2zEwdgME-"Q6eBhpA%pl% _a'rpGDOnge>h^=]eҌմZ-a-r\WN_&Jw2hufY=!kKv9}a/^R\W&*dHL3$Rx$C@i& FwLL3b Mi־hsf4ߛ1iVl#Bƕ͘(޻v~ޮUիӀNzXHF)luOnuČmh5]2cM_mzQ]v5]Q-$czM}[(ÚkپQN.Nu|^ڶw[GgGaжCf5B.f e}I7ʏd"cuHB9l(n^y-S{ZlQVJcQhph6EoQ#3-Ӡ/:/'O(^(ݤ Raސx٫gԤy*sh 9|ܵ8#z e[G'M22 `lkqqC)^xϿBrB.tT 8qû/G_/:M6ϾI{~IKM6zf6:$q2ڹkkwDճW {M o DX?9UHt EpptpprP yy%Oqsqs\mKїb/6js{7֪֫ڪk+0Y`e"%ʒ"#F@9#ѽ琞...Sç}nڲi?[))ܠ:x`V揚s6ƌe2[%6=ڜºZ$7BSRjg̴W.qs ̒{$Aoֿ*OeR;>&^X[ל*$/aQ<vR"qkݔ73=ĸDW.?׽nz&|_d2Yʃ_";89>,#L/SE$R@E`R,1ʖ1bW\=\=*Oa'rKha|N!}; ˧,h׹sn-Ld_$_?sw.G+'+'X4 3íU yۣGg#6eؔaǾ"6ǔM1YW^ |Vw ea.tiӳ{!vk_w!4+}E-V0[@# 9f$Ed2̯x`W.vח.i%U}GiM#c~Pf& lHYI16.40׵ޢ9fYX +,a[cE7K_['I֏o7楛?{V4qYʕN*)dmW|ݚ(}휵ON+ٻƌV rtvօ;_tӒKvb?>Tf) *nܖ,555{/=7ߘ0T>ymrܯ> Y`]6d^\ U_ f"(LE\lgƿbg?;q,mQ&kھ?U[x4`̀} ) )oX lE+WLm#&AV܅j/Xؔݴ/yyƽO2T4M٘o}3׫rwn`P3/=e^~𪫇覿WjWl9-/E_r {ߚ4;y(n_qvu|kcjPԢSrE7i5瞉8{^Z2c}8٘}7ͼ.*)W;o^˺'O"E3mǟ:pJYBRaeM5),4^mXaZi[}Vkbߢ~D7XcfKї Tf+sAo7iEE)]`#/!)iť\.*|]~?Z.D]xkGvݺ`o e}I7*kLW,@"Hb"68åKon4OpP mz7?}ѭ1_YP(|::9&?HVf)귨?\-\3>֘s1 F/V]ҽ$]=ZX9#^y=JАPJ!=]\9hPL~Ni׻>][vGErrq훯xYG70|SٝްPw5%MM$|7:t& ^>~螣M4=l־YU3g];~j_SREtt_es1^M]:on:}n}j4oXr%U*!.ś>5|RR "_4qDf~2n^١_S;ߢ~.-/,t`ˁlӳMnTA-//GgGFNNMNM|;8א^o~&Ŗ6ZqѼW̦?J_Ÿ̋ϴզvnr񉗏_>G{>߳{ҋ0YPt\m%r@G,),$̠oD?G+ˣ]8lPDgw[Zز&1.>5}VY~Q6dɈm W?~M>3G,K6Z640L60(к{12jd9k\t*s޸[֞vL&L4rG>a0QG/lIC',`a@av!Kg?7[4R=R.n._^zVk"ؿc5 HIH1r0)i@OAo_=u`?snLmrƊMݤj>w9)]^C{__.5>&~&-8t7>}@Y(d2LJUS$$8" IDAT k7X}Gk&9tȬ9_|O3vWݯudtR\r 1o~ey5|T3w(,fgE62f?a&k{WBUX~rI&^) ;ś9 #mvzmqqsY_uqw1{~#mzͼeZ؀_OڢS W/.P<)(u-) dI< ! 5;rqw1~jDG6|UW)r/smUo"Wl]n6F d&068u`W:“jԫǽ3Ƚ4o.b݌3cڨu60Y\:SyO G_-4rӗO"bDRNz\,':%]埬g-FT'ֵ=3+ݞԯ8s.La}K^$gBYBѼS/ 7пCak5UtUjn{EUJ&;7hՠ]vݞ֡__5LęglgoWf՚ jmRwHJ-2[=~FyZ#F:p*Blaۤn>z u`W)慹sN~nv{(8R_2Cbz#;D(ĕ{ˢqIYJ'>[wpqleܵ)9Or\֪Zy=gWg7nt@l R%qFGyjWju5$5tqs)α Lm$9(|P>p+~^w7L 6;JPJOz\d҃0YPVD7X0G"F6Mh)KT Y1Tqba5"DBQ ;S,Ջ@,@M&>ѧŞ N2dPي ᄘ@UcJHYjơ8bPdj>!&P>-at+po.lPq BLʊ1CS,KXTb dj !&P>-at%K4<Tkbɕ7$aH)1UDt!\#hlPT9% !Ę!*(dSeuO1zY#aU]AHY1E,@P=ʳpT'K,@& PL N)$ )8+,6ٳ`dON,aRapgQA YF,@VYlgB,@$L&(Z8*p2d8a5JAgR@,@ L*Ql@Hhmcl+j& PcƘ*+01FKJ& PD% F:& P[ƃF-'ZȐ:ŦL.Z݉1RF GV/2dja]Ah٣dV}bdj*a% "zdxd¤Se%Vbdj aDP<cl@Pie1C6PS` CHcLZ1tT}CTv P`j3a Tl# P  V#M GEP &LpZbdrw @5 cc* :PJo GTdT;oj;;U0Yʪ3 *:15ұ& @y)Ql Y%LTvctT1H, 4H@ K1C6P #L TclmN1F6\ JD,PZ#ePd˜豪5#VU,B, 8ؔՂ 7eU,H,0D5zjhu ]! @E& Tퟫ)va ~ bU`-P3XdzcޝR~}:WJ?!L/ )O& @% THVqƣol߼=wH_a@&L0q5TMh# nO֞߯߾y{@0ڨY7H1aTQU3R6 ـYضq|1m9_YboT}{vҷKCԻaӆZ& @7/BIJNJMO߸~MZoy@z uБM[AMV=p[c! -v̳ޚWwΟ>=:2va 7NXxՏ}~Ji 6lѶE.:kPN;%&&V\Pvddپy{I6/V*j@+g-6˵`BEd̝ϼp ϊ+V,XϿ?]{c{*yye$lԬ?㇜8$)9\X @U3}NsyOPe>-a1ƥbl ٓs7>wseI*e^wן7}#gONewdξ/1,1+CIMFB~<|nsMe?\ @U/7okϭFbFO}-1D>47?86G|TAwiߨYĤĜ9;ܼnek[p3Źg(KV\\Kݠ*A,5Sۜzɩfڙcˎ]vUуcЭo(?[0a; F +|]v?ٳ̞6~ڤ'-bڄ@ Kԭ{[vlYeu+ޝ}g?d}Z[ٛR3+|w_~7gꜾCƿ=, bOqA.__;t0fdC. wgCP{vسWjo|w8Շ_{X>. V; ):nc~(n*SBBI#Og_ɸŹ%2}a~Zƕ˾E}>skRrR -Ĺ+!Lꁫ"MlZ]p =ԐJ>JiWBB©z[%&E Zo|<]@Qd θ⌶چW.X7'.!!&`pьE!žCFLބjӎk"}GKf-s?P @ t'׃MP^ہ5خvnRlҢI4A`0^?PH,WDŹ\WHYB3wXj-!!nHL{o߼ns[ɕTM[5XoиAٛnź 6lݰu{i[4ns`:M)J$s[ ߰jÎ-;vez5mٴe:JNL`DP{D֜`0Xs3e.nꥫwgNJJjԼQRd2f.|wlٱw@ V/qm;="v{vz飏^hΔ9 ҏ9㘟_u-6̯f}O\rӒz u#O8v]Ÿʅ+?|ÐⰟ~Hw\_iަYWUq:ι0@ 0 % ܖ9}ovKg/ޝ}~dO~^{D¦5~:5cbƜ笘"7'7t?䣆ޮsL,ךek‹WE~g_W~`nN̚Y3G=!dWxef VX7? 驇 =װ^QΞO/ 7iġ3d%(R65j);^"4OcO![y<ӿzۦm;dw_>:sʔ:eN0zn%N͛=e)Q'uc MHHxC&½/X𿝮Z&k`=;͞2{퍚EVq oM/2&eDTM:XgL Åwd IDAT;?l{/Dg g]yV쯽es}g_墌E"??_0jܢqII3h pጅei#7'Gߘ?[eB6m{w}‡eLޝ=ٻ&?1IP*cdKp!1ʊj~ țGVJK7_F7zY _{3?ރ驥;\4/-űޟ_}W:ul׹]jzjťs1ޝVzYqR>Rc8&/BH6~CG=GrTG\g-щ6TC'cï2nJI/*??>G>zA]˲@X @'kgo~RlФh5֔K?+wnY۟P­O_4xwԻRZHاؙ`? ǸBGvt<ʃTCF͜s.xQI͘ ǔ)|ŷrAd eG^iͦ(R\2{I[Ԟ0@ 0ǃ#LSkW-wĞ=u̯f}JvrYgӚMWpUe5H,cˎ7ZJ\촕 W?\U_eX&'$I)ś_ :@Q r 4qSnmڪiġ~Fgn͌sK7}3ڶi[)VքGMd5?'Te|ظzc9w |=9WONj}R:Wя g.8GGGѢE~RrR,;r9{sڕU.'Sc8ݲ~=SnPP-; )ߞW;=ѩw6i٤ !kW֚ekL髟NlZc?xo8sΥ;ǏpLTmc3K.hU'N ۺak]6'^( _ˇ~8>g:3nmPĘZ?_l3d5R:~CW=qt>7 '&v[ru#۝ԋI,\|7/˵}>ׁi҂~p?|];vzF\V/-]R_pe~]9eܔmݰu!ھkD_kX]v/5k3O~]un׼miursr7ڰ(cWKrZbO} US9zvӹM6Z7+ݛws&7)S?̈́o7(-T d֭Xw݈wg-I3vLݴ^nzǟs|뎭#NH֥o.}zɩ_}ܑ53|{ϽwCה9S9eW+؟2Լmm;ߩS@̯_M|ob&ѬɳF7:~{FUHNQ@ _t3&eDݺa]5u)GRSF;z놭!Crn5l0kW♋[vD\gߛxώY;nY"sWqI MHHhܼ>~c.]+'ֿ[xq%Qd7X=y ߁0ٞzF\#u!t#o;.&ۨy(Mk6mZ)V?c,^T)[KN9:tqB:{vس Ϙ8soߴ&@ s j'ajS?z;Rz]5ra'1"#GqrBO^vu9͉8~vZ'4ipώ;g͙6~ZzR Wp {=۟= @P5cd#V &jĤ{޸#.[|,wa}F=jq)Z/?8k&-\ve#.rӠ=Y{~ߟKM7%0'nybU?ɣM[z}~~6GE ]:gis^zê fL%L6 .hɶ& 7ԺCHju@_/>9^Zkߵ?qW.\2b67kݬE>tC7m9Wկ̯fVtK6n{jSE+ǜy^?Wg$iuG3;/ԢEq霥x:)p$B]vڕ_qO)7'7^Q6ƛ.-L6JfƤ(_~E+0LY6n~9W֢~Ş$[^z{UdV#={ܠqJ 81hdȆ7@6Nyvఁ.?ߞǡv}oװihsn}5rg|6??MF31~_ >~px}U^nT)e윩sro4NE@C\sNMHHãi_{ҭE+ޓnZϧ>l޶5w [>{ mߵ}-6 weWIS3ԷmVq'y`RrR^n^H}놭@J5h/~g;UI@I&v}ݙǽ<.p7&&Uo`<8I!?^)] $CK(cUSbRⅷ\Ҍ; _ο;V/Y]A-]tE$qh;wG?p³_7]R;g /xwq@A]C*`p_6wYޢ]2&e쯱3}s@ͬF 9{s*M[5 /,VaԨYz6Nj*(fٚSgUnIIFJK?ܓ'~̙t߭U;O^[;@|(F6T(@mЩw?{wسcทǝG~Hğx]W>E{s_񱟽S/95b?/FY:giO^V.XTa MJNMb[+x5*:j& @m5W>Nx>kǮJl&!1BtT0Yޟ~姗4qOdx;.qш@EK'>*>b|R1^"*(!!aC;ѭ;.v~^nkvns#NC̭%* =b=???Q7oͼa'ًxi?:ضs۴zΎ&̈́o ?NNI>ȃտ脌ICeZxUJ ߋ!}QbREe`F6NjgPYvj[+kۦm߰|u+E{Fvi/r= zæ ~ٴywx_r]~P1հو c`Bm @ 4'{¿u߼n?_rcח:E^T7nIR͞2;b}XGo['Nbbbb~]BX:'Bl~^_~[ꥅn۴m•{t 9vɬ%!߉z#'Ts͉X/ ʅ0YjVZydġm}ŷ_/bO֞훷? oMk][4Nu#F:ФŒYKn>ܜܐכb !?O9%*RꦜuY'_|򛏿^ܚ}{Ͻ(c<ܴUӲ IВYK"{Ywayy. Oqֿ[,a2q]vװޮ 2d\\a4n듩AweGkX/Ν@@,P捇5lYvl#+ᑲOa|uiW.' k]]v͙2wg',gY1EzCeraՆk_[4@C)~> ƨ { &\@zqc^ڙeG=ꩉO5i$nXc-;DZ*blHeY;-~3ᛢoʸfL8SC\2FV_;X/˝vl1%^zӚM۷lڙWtΚekJ~q/lͲ5nܚ;;?/+WV{U6jM^>O2fٚ+)6kݬԧzޚ0eܔ 6 ";n\1޺C?x8Azt@y![:$,uj~~/Jg•7~_>Y4mФAYmپeӭpu /.E+EdSS֧ハ:8$L6|5a^m%|ڧ? g,,s_[~~~ČlGW/]]T'Tv]=|CVKo<ƙϼ'׾̻qH 7oX/Knٛs7-$|Onަy[$$$Ē$ FjT[4k#.eڬɳ>46n]ڎ-;‹5*~a!.93&(|xNRqj630و;{ 6LMOq`08W=$ܜw}_$0Y! <.sNsu+וGޕV?-mD1i?q蹻/ abg]"eH-G}?Ѳ}=Z^=ZtB֮bzvۅYZoe߱ (נ^)E'jfчkݝhǡ=u_s^!C~ WfL(׾I|޻bJ.~ uR=;!COF7Heܜ?_73fm5i٤a驉IwIJkĪ#xc }>}(3LȖZ+RLpc}a ׍n7Gvf~X"~-{wBbw=?9]6gYч'> ,85=瀞.dL8>\oиA1O&_6wYxSNk{》jԬQ5kV?-)94빛n*EUS~~w>]K͚jڰ7N ן~;ja_4uϨGR߱e.v=Y{n:)O8ZQN9jఁУCJ9?JQΞR4_ )9)b=go,wq5}=9;w.}Ŀ1b'F6 HY&iҲɣ疳n)/7o̅c>q#N:bЏ^M>]z:@-$LO~jS6n 'ņ>ot$m_~'brTa[vTyK$!!'oںaw'fn˼'>[whNl*?>@נI{߼/ Yi*X7H/ڱo ]w l\1s[f _/cMLJ,:ʃ>\݂}E'0]z!}Ͽ3blrJ9לsok޶y9W?d4iyeE@@R'eCs)6 dCc׏q}KO$@ Фe+VK$)9WwDWoko"!!!$`0(4b1~*Q>Oz~~N 4iaf%eȧϞOYV)f-60dfL~^쩳 .XTtֆNxjx1AzCzD9jQƢ`xC/~U\U{d;ywydJD,@YKqƧtxO뵻7_nE6Kg/OKMO}轇u`Wp ٻ>-O|@=O"@e&6NJu7eٵ#u m:IR\:ALh}!37o93ܽzC=ZS=yNJNQ`+ Ouӹv^A|HVM@u'L~eVmrk'kOHn~}=ׁ# s@M>2ZD5yޞ(c ٪ ςHYj5WP*cώKfGQ]V%&&v=kH0Lv M[5ԻS _aƤBH4nj#Qx&-[sĿ׶B |Δ)@u'L~`JYwH0g_}vZr,i:^_8cdzqoT?&?~dOi}(QTYHNInԬQxL8Gɳʲܯ6mغcn!]Y.,600Zh^ IDATYggQƢz}?z }Qʸ!Cwxrkx5N)M[5350YMk7EICV.\.b6a;Q]xļO8P;0*C=W,Pu^lڲJMv?{ZӯK [| ~чDݵא9K@ƤX xxHlά3kg츗-<~ꥧFϧ;j"~"~ܚJ81znz-w+#֛i^>=ӇFYL9^+11%Oj})Fz)QTYmݰufbvԵ>p_e)P%*CVlO,PV~f{7W/P/j,lp'ފlOJN3O urj?_l6M,`woP(4oڼ;1F O(;qȮA j,VmP(Ln'nZd㞨\84|֭XޡG: UBB_gj۷leGI(TnWBb;#jK#__ URi/]px9sKbdKJHYC?+)#YQx;>G}uխQFv4Nnʬɳr{1Ƙ!2Lv•;miѾEbPɄB/jա7{nuN[7o^y߸~du?s%u43#3j_VI,?luè]>16>xp‹(;+;+3#3yP(t%O]2;*)((Mqp퓧'E ]rݗcܽSNPiغc_G8NJN;V=C:5S#ۿx[Y-o?IMȊsnٸjɪ=8?GV-Yuyˣf4)@ L@ }CmX!+5-uEblO9O&W=_8ca[6n)pݥ^1]҇5$jJKy #wCjR 3TlZ,鵇_K>}g8Zją#"?bw-̫lجad7~=/9I[μ%jPfuѡwoeͲ51=cm_Դ[x[Edw7hAޘuO\.Γ~?@ e'o}2J&M4O^$j $[&$}D=()Ѻc)3&xn{A ô {r& @U= <Q apcPqvuYN>zA B_ؙ߰{T' >GJߐ~1Wa- ;7=^ 㗘x׫wu;[u+ =v%P{XSG?8$g?I\ȼvY8c}._j}`0xՃWD!fGEz'P({- ;iؤ "j֮9hӹMæQos%&%w~,J&|>=?lQ\87?')3~[pE~cO.*PJݢ$ގagVw\-ןzE9d}߆px҄IzɈ Glݼ5aEq P $uGlݴ/ 3wen۲-}CZ7mZ)y:0A+N낻"۷lrّk;ӁRkv̛:o'?ze8ǟ|X\`nQB/xyǷܧey&1Ǐvݷ^1UF?t.(w{lckTs5Ttq~ %GΑPa3aشv+Ce>qzvJH,n_~ΠueԻ}/Ev=&Mt+-rOG8j݊uQgjW<5/J*h9}'-]rׯMNIp8ڵc׶mmZd9K6gͲ5gpyI{pрy!3#3v_yGPw`0;+{ܥ|gy?3Ҫū^׫^{nDuImNƬ̬[r'~ru=kdfd.UV1ԝqߎ+ړ{f zpךk?G/'}6~82 JeUɞԞAl6M }͚<+j[7EIg-ӞZ3̫~x̬/{f 9_~ّHy]ڪcM֨U# ضcM+\2{)s7߼}W >C~g+"jW-ޜf?ϼ࠸;/9%Q 8ۓ>91TKR-e[P(u@Jw٤Ȯ?>ԨUcܥ[6># [ekNkwZ 6zp64jѨfIII۷nOߐqUW-dɜ%Ycq5?]OjZjBB¶mӬaæ #o&$20'Ovጅ=堊HMUS~n`@~f}ztض{_oM mޮ>hÖ/X|g+eMZ5y۲iKd}{e]Tnbd))IUk-߼7~Rv5ggeoLeGn#glR=sCgLuY#?:(3/R;4iAd1m[(5aT͋mrJ]1j¨M\g"& ?s_<~wiPvؿ _8As}}^vjNJHj7=}SĐų PV[GWOt;[ ObRbWJH˘Ǽ9R+\)TY,CH,T %Xvڷs}>UûWUaz/>nѸ#V'OtݠnmEթg'zgnV+c~Ւ9KBJLJ{Eضk>BmuĀ#^n8/gIHHc\ P$uP4ӰiV[u='Ѽ]LW䑯~½/yfiuﬡge<]pزiKm)O?s~9-CMzuXzwa->vԵ?jC{~ǜ~LiPmP,%+*Jl;?y"aUQ.?gժ[+,C`'9اƾ/sguQJ]~as`3t?|5VRPVв~ܵaSRKp+AǍkc eONN<,ն~Mx3oY8cam4mZr%${d OxiK߼MfFf&S}މLs_5ԚGzYC|`<]]zuylX!궥9Xe%(:cDzdO۸f5ѳ"%$$$$&$$WKV#FՋLd⬟gX"}cz8YfMuk_rJrͷo ?tҝwݫn}v;[?nجaq ʞiOۜųYfÚ Y$$$nPQF6oݩu;tեI&ˊ+vlۑ~+B͋g/̊ڕܪC" w?!K HUVLP(S:}Yg-0FVf[8'ϽcmEyjRvB͓xu&%ɳ~/hՒU;3r{5k۬]v!Rػ.*Zuh79cgyK4&$$D~ RkǮe8, &$LUVjPXk6~9K6ݴkԚ 6ػvP߃bC?~|=o i:ڷ=m׶%rs?OyMl 6DܫYf4iШEƭhעUVa3'\`kԪѸe]=GkǮ/⇏~X4s-4Ӧsnt^M4-W܊ɳnrlOd**`Bwf )jTKHH(O N-'έ̌ WYf ҷmٖ3#WKVv7iդM6 {v SR &+3k׎]YY I)IR3*?blOIeUP~Ԛ5S˺GL'_puB喜ܺc[u!Q0^z˺(䤤dOo(_R9E&L\!KEWHـ11T&jl (qfmR1"eJ0YgxM*8aA,@#KsTx p ^ 9cq GR3ٳ&qC*ϣ7*a@E"C(}KJ%(!dA,PrV#. !drM,P.8^ PRYY \rX& /2d%w9|ZGx䒚*سgIMGoT>dB,PN.e9,e@Xً@EPHـ ( a@3FV"P.\qZxʀYYD%B,PzdUS{ Q JY\KbK"0Y`! )wыHZ$d=B,@r̜L@,PdV"eP„%C,@q.~ IDATqZN & KY-㌔ X]*& Q1R w3Ub U0YT,& O,@Y]Zc/Źb# PN䬴.9,P%C|]xc/ԹjQrm#C"*Tl20Y # P.qZJouVIwH=@ /oq}y[ˤ(@gl@ @őb)׮^IV__"X.MJL,b<& UT1*d# P"e/XPM@jj֫UZJ* *.aPUȐrؗ^CngYk׬ٲIv-Ztkx`jj%XTDd# @վKC o6}W@ Fs?oۼy3ըq~ݯޘ^ZJK,]c_,r{],2uzk;׿䤂_Ұn1w.?las e]P`iQ?9٧ @yxC}AŜ}v >m޼̰cepU+֮- zz'@Y& PUz[NwUY0S?,[z:kq2g3f2oN`((&a7$TDbd@rG^rTW IKۧeًi_fM{ڎ]ξw3߼믿  %+1!PaP|o̘=&;| I,HWPM"ee@4k0M[I1%nʕ?N6}kn޺5+;;%)Aݺ{7k>ڽ{5ЮCef.\hk׮۴)}֌@ T;-iÆZؿCn쓘ȱ!=3f.\tժu6mݱ#+;;ukjPNڧUի]{U M;3,^j۷ի7_Vzvܳs=W@9e_g͚p+nܘsjɵjѴiڴC[LNڳL S_f.\zÆP(-5e=;w׻wZNwedL9sܹ.]qm@rRRݴuj}mۼؾsӧΝ`7n۱#$%կSEF޻W׮-7ޣ5]SΝ6w+W[mۮ@ Pz:iijnҠA;wMe?Ny׮ݴeKVvvrRR:u, =jΝ'^u՗'VFZBڍ323vo޺n' 0Y({q6W\*TY6PW٘ ~%%hƍό?`AaI}{SN=s ?4釩Sӷmgkz1Wyf_V];f&Le֬jդIΝ{uzHnoKs>oNv֪uZ߾CN?g%rk_̙θnܭ=zsA{իW]'EKݼuoo/Z"cҽ7/~/?c׮؃4kv1\2p`֭Kdved/&ňѱu3N9uӦ%^ƛ&_sߍ\uztxоܭۡݻ7iРDِر/yb KLH8.:j@ νdlr^ f-Zw=[.7hҰ/޳uB,83dhr\"euƨ =W^'1+;~:iw4"1LRR~=nxqa7d W]kMIN_gϾ'#G٫^&%&/9{㮼rݦMyz/oUv, o59`0xřgI=0Y(Ubdrk^sQo5ruӦ_L1G㎌"k[R KoY|5<\=.۴eKITd١[oy۷uVA%XXYmS [reY0eW2dvC{-[ҧOa7xW\QoΌ+Gu'o9)13L>ġCnX26oz-L;+,Z;L)N%eܗ_}M;32<Óo9owի`a%%;tĈ<ZxԨ8s`{vQ_xaZa͏=7XP$s.iLLH& 'aPd@e.e1eW'G3kv_Lq~{~Ԩ^cj\rիᄏ;^ECFU35e ԩ ۴iҥ1>_ 7t1d4k֨~))pxǮ]W[m[vC(;_|4_m5Wߺ}%Kb<_jTN( KW'۷V=%e֭1=}ٚ5% &vƾC)|lKGӳgZdeG]| bٻy{핒e1ƍs ;e֬~C8ڵhѴaĄ[Y$1cB%#FNmڰaƍRS`Ff-[lܸ&7hޚ0oVֱMzjefe-]:Fg'ַxP/vM^=tpE' ܭ۟z>[:4m0kY?oD-^z5W/o|kD$'>u9`Ӗ-s,>gg.wwGMMVmg\|)ڴB_fzGN8#zhݴiqJ*Cg眓<2'o7Gv(4iƍ3~|^j߲e@{v}دwݿvfd,\|Ƃ9GS3n)Sι喨މ~9ճgr^|͚W>街^ό{) efeӑ o} ;ۅ'/7B;~#GJ =B,Ė{}ulo|Yd{ԣz,zfgxcd búu_1Cܤe7 x]wnx+Bkƍ/8'ԢqÒxk i˖~1jV~w#{vڽo2ƶukᄑ@ l߹)S>Ǣ8/}]vm5Ӟp`.vrgq .3`ێE(ٰ̭=$ٶ͛kذ<2{ի׷Wz׿fdf~7e;_~VC7Nuzt=\ByÇX6ZJÆ]:p`BDobBݺܭ5{7O;7rxݻoO9F16y;i}|Q짟>k _}Fffjt䑑4o/8~O"cgw\5Pg-\v{ =U){KZ馅?p8[]۵;暹wQ癷ti~_Z|+}ٞ #dw׺i O>yrp8|]wE]"?~O?EWOI#dwץmF>GȮK+qge?N11!G$)!!{ 'b#KGXn]kQdsIK{{.>Ȯ7?7n\*}ƋO9NZZ&<{]i Of&aP2(pYTHYP0us)bO>tZZ7_{5 gXi6I;!( (ET kǶ>*꺂+vQ HSE*Uxf&@|x{.d2N2}mu)+oQ.3f߶mj**Ff̶={veUU[XD_4ݝqmn3ʡ>xVVU-߻rK 3GAo×/9BqIQ.:qc:mܻ-I# w!,-i =@f_߳kunbZejlL^0YhT⒒^EG3FdؙTsnoдXgY:|\Z>^~zkx<"ggʸFb U4)St9f͚i@ǏIF-Zq(A,˥W8zJfC}MI!iFdJKd\ܹrgZtc`9s$mQx<)9Yn9 m#beEm^^/'/JȨsݻ/0v[-=>{ZBBb\RW?iScx&vtࠬXRV&PϡB=|2e}e>+nfb2kcW 3ss3ل޻Ko7oXb UQY<2WO߼yy>'J5px-ݺGx!PhdmXp jjRÿuXcU5ntW>ŋgφDDʼ]SGb_WXxލ$(K#GN6LYQQO_ |ETT^a!=}`Mq={ǏccPGCúsg[s^:RisRѫW/=|#b5Eg\77=_>qZ@H;ښwr7YhO$PٻlY oܴ);Ț5_}{r[m:w'_Ν "VcXfQPdT$1$Ta*÷f)>|~U<^w.] rl;PRKÆi5]. _FG&LqQzNHaØ%o׸qƆbA*:z S7nP͚hN={ߺ3mD"P󅚔N%0ΪS'ֶ|$r,ADɾKr2>ٹ6L!VA'L"sN5S]MMQp|)P u9yy>}" 5c 9vah#qXش ȩ^ [QY04G=IY[ZV^bʔsiuz@1<6vgssq zTRzzI$KX[Gn!]3)1Y`$3g?--se>q˛зRbdDgk+KI*[Z[P.66$"{~l-[dկ>%&C#b Fؑ&3G+YQY)8-Z|NJ(zgץKG##͉U<@Q[CJME}ǡ>n*5 RW&~\޹@}mmrg(SGڷnyd@ɛ7\ V7>n,,~MMTxr1W2-) :{4-%7!jձ#Eh{դZjK5礤uB/_x{d]ۿb×//ݳGf**+>{s^d}:up֭C͠ $Y96{ф ˪=߳1޹4ԩWvsܘիc0='guן>=i["$[zx٣0ɓ?nfb"8 iiK~U$wѣ9[a'hI6J 6ks7o}Ν}uckhj>D|9{VaF|兵u.]L--X&_EII~"Ư^ ;)=}Ï]D:~ A#z5dG-nh(cێQ?WFYzx 3sG $r}nOY6,AA䢲4oN.~NJmI6?ʊ\͵2ի)ʃF^Nh%h=}2hYUYYV?ͭ:_O޼!Y, RFK.VTV> UUƏ"X}i:XZR֧o8w۶f]G //ĨE rKr2. {?| ZI>E;}^IhQ!+P57}{Փ^-+X%GegKތ@ew?w$[[Z:zׯ5zf;e $מ<7kd 37wE?<}MM=cFcHz5$jU<ފ}~۹*HKAqo1H_;BU!v/6Y^aЅ Ӳ;i}*JJRW =N={updkkfb"7zEsp''bEeѫW7>(.8Qaq9!n+V0bUM&?a ITUVan>gO3X爸8ץK;Z1޾W8l_KMA^ظPwԩsW8)a+4km=8#W+r(g֡MG;_.T+zK(M557u:cs>ND;MwbEM^ UU.]88 dd$ի1"O~z4YTA,f)>.C sLwuDk)M @Yɑ$WTTQIToߺtW7oR.ĉM'N07<ɩT[ݧOUUtJʦL`oF*D<.LnA!65ˎY,DL|<))zN$i?6jcX[,p]riyE?wsNK}NN..6;K1{>&S(CY]>&$m[rr77-MR_@yEo_ u55<،*.%֮kWsUeeJC""v;04rM'N8ٗ"`ܸ ǎ ԏ]vL%!w'Pmeet39ۻgfbƦmV</>%˗((eÇD_Lu{ʔݻ׎+,&/>q\IZan^iթS3mmU>!!͛/_l͛fnm` ~8s'/'7yذ)ÇwJyӸQhgh"fJBj*e$ê()j֌G@mK؆Qrr eamѷl/߾p=y |͛ܺU{L-Er%9\[[C-[MDWSkܸÇs=:yPmM dv,:JHYjmMMthgIDX4dPxxPxwTHY['>͛nJ:i?:rdмyuFo>}?'+IΩ":+S\\.orD?) A_&Kw. fꦑHAvpb{\3>%e˩S[Nis?ޭ/^DE1˕h feQvA9T2lD>Lb|n%N؟vvkgkmM+Ś斖)QQ4dL2D  }~~]]>rO ]c0ԶB""ju֦5frܹ7K7?>z{o;fkn.bC%gNXTV^~ŝomVM:|"C+ˎoN=7L6 $|㷅^ɓbWffڪZ;yݺ*ҟ41-uu۶60x|HEGF֍AK2LG珅 - 'ʱ"6(?ƍ;r跼ݳ'f oIb桱cm9\w/]3Id?''/ٳf~UӦռtY,oǓ2&mNTxv l537tI`Q~Qщk4@0Yl˖un۶vE^NNUYY[CZz.W\쨬B| ǎSKu2j"￧_ :WTV()8p{/ W`_(.)yı,BhkhH82EEnC]&+ 2y9s7?z;ȸȸU5jԸ>(<|@--%OoSNzU0swQP}zM^Nm(!W)A~۶N~'oN볥't()Q֥5Ư^#3pK :2vp0ef|>A;Mn ښ7kK'NֳgM.ׯ_ Pc7ϛ'84oN1c6bqIT*kjFr:wrk)Çyᱱ̛%xO ǏoO5yPrqX=9\|$ЪUzZZEy993=y)1ug؊lx>Z K Q ~|bz\ ȱgzVQRRvhfMeUyXGG~t릫I㰰cWJԨÆM6,='FP``۷4оۏ9bjlLg}aESM~? E ()+q' #T^Q!ȥTϷ {ܔP_mPPe!!u8vW=yrٳB:~NJ7{u릍Agv ?IƅOR55uzYZߺU} KJdX[OI]/^M["/'۸qb5"ut%&: Dt,˗a}l)8,k@_0oN:B*.-%%𷂃:ubؙgn"+|~]۷ٵ+=O޼!'uXcNJ;=_)ˡ pvӭ.bƩgOAufBE8L>ɉT+|_k[Ϟ ev&Yo YVq/f).@Bj**l3pyv-9IVA^~Ŕ)+LRWqKd-滻ww3(ړ'LLtpA*@0MAi(^EELژ۠Iqϵ_ʧo^WNeUՌMZ9׹ a֓y0i v!/dJ Gp!P6mn+lj%Y̠fOp.77ϟOn@@f@NFF7l7 g{{r,Aa70܂;!!fٱ#3PM IDAToX?Μlggoa31 l6eh,ˢC'oԣ|a/ݻ䢽Ek];t`XߗF}0&fo7re8,A/#GQrFƳprٙܪcGr K <#@bXt~KOZ@fڔeIqIڵTf::/NdkԜ1rdYn6f@ʧk$gd=|ȍ4l6@W\OHK}3 /  G|]5ӣnCG>xmSKA^ީgkצ?|tB#9[9r3!!BnrKKG,Y%9Y}A=-ةliL1BSMMX垹uz%P\:q0㝜MR%ZϮ])M! pc0efI廕 ʊ\,)+0w'1Rw#(\ؑY|NJb0/ǻL;2MáءMaK?&v42r+Arrllֺysr1!54Oѫ#y1@mVw7)=]ȑ2nF,jj..+Lq :4 ".W4-[R% -H&DN؋3!5U6ҲA"vǘԳ532|ޭs@aQ~brr*e~UTVR9 Ǭ&]#PWVT ؿc2k(rŋU<_ Poccmj* `NFF1MndbkaAYO`0ZCտ2^V^ر6Cs9>0J򠐿"~8(PۨZ} iLnޮѴ摭eg3 3Of2d6q6kBGɲ X3cEDZ =<>] %BEe'OĄ.xPx<^HDi(ںzϒ 1!2B¼];;QD}"I1uaɱz~ppSڴ\ƒuEGJv\!C ʨf b, >dAAu6}qƬ+(.˫iѯwBB{~IHR_\o{aߓ&,`|~b߭Tq h:U×/MaԨGTvZ#9##37\ddl@ʃˣc !" Șpi/\O/Q|C7 cݐ! T_'y]Q+Nl6۶Kׯ11唉!ݻ<"Ś&HA^~Gm.{`X#t,f#>{k>ooaqerӧCdO`X]{BӧaAuzMYafFYWp,:t x mDEQQq}u{yLɓ}X*:?Ų?Jr\!M55:TZ(*lusK+VlLqr#"޿OII!$Ja[3pǏ/\O}߿Fv42ֻ[ikff 7H3WT|NJ"3z)I4{"**2...))9##+7J1E[=mڣ瑑"ԩ-NYu@]HHfmMyikh0W;wnI8((H!ד32?%&R%<@#0Yhh" hl* m-9LQh舾}e GL;i-nAWHVj**v]}Z媩H&b-e.PUq? 9Lɛ7ߪwBBE6ZhݻdsccgX PWQ65e6XvۄsKK ttDo>֖Ee/::JQIY+,.EEٜ=ݺy;:ii1޵>~L>{Ha4%9(̒:ig h>Ccbcv& 3@Sb$|D4"h^/k2U-!bqi)m)JIYw%m jʊ\/r_ ~k~uƯ܇zo++vK tZ 88(h*{SI ^u7T<~-;vEGCcRn$H.WTȾdfGeenrK)S͞}-'VRWr#o1ǎNN#,L[|P~Y9(2 |p& M ?WG6]ϊEKۚw?{^6= ԮQ>ԭLg[O96v9åGg|MMq3 Ԕ\Io NcU0%O5ۄFƬ]R!VjVnˇ,\keEſ֯xeǎ  q]ݻ<1#eAT5D9p5h!Ck D٢ (37oMt)GA\OI!grQhUt4Аζ-99k&hKs]]r[Z:~_'`wرbeUw__r]K]݃WC͞FrK6>%&u\7nҥW/qGc,4&\TWQiMgE9۶Zf)oo&gdHkYYb6m 5u–NݰQXaE{ϟ;k֗daum~^m3gҥV8r+G+~::(\ET@׸q $TYYI.*Q<7* 5a[Mp#(H:m7ws֨QkOc[Ϟ]- w۠A|NJn]YSRo @c'0F1Q@B3  5/QjbWW_ݵKVhm`@Y?zfVRVsʪ*y99r**4W&%34UDfnhn֬V;pb֭W8ug{yQxlۊvVVT}c26iM'Ndf }swtտ?qx<-[(sNOVر;_T$P=p%6+*mPYUE^jT:Y[y<ަ'ݺh;F¬?'%Xqc_ .}LH(~MM\nVn.nޮΜ#og'OZ2\С?Ξ\jh`0iȐa}t73YTRr;8X*T((xa+WVNJm@ť[ɕjjD,rBn()I)\ݠ Q8zzZZǧl>yRF`X.zU}?04{}Ҥ[>>}/h˱b4]玤:ύlmuu)E[CCCICMEEjXƤ;4 1hU_yR.W~qrrȋBB͞m[;CC㗕'w429e}׹s֩m$lܖ4?͜1czvJ&-+"%u*r8 9Hmzx'b%&ZXUYA{ղrsax3f7ccɋ|9p;w K%3;ܲXpG^4yݺǏw73s>oߝɋZ-<54͞#PxcCNfQܼ9{m5Xs`L|<7Ҍ W85yAl~os= 3smhS^Qo ֦=(~NJ$ DfdXV $/-r,\In}8|2eE  3fɓ99W?8dHiiן>|٤I"<Aʹe߉XM e|ٿ>%&.YBNeX Ǝ0g~#TWQ4ɉ[Zիkߺ5]P~e a'ëVM>\@ӂ9aGjȦ+2W#4/o'Գ,!aw;vùbYYU`ǎ.^w{{\Բrg/g[QQ#,0g$Y (s3))-={vM68WnFO'٣Gjjj=rJg7**+HDAqG.[%FK^N*JJKCcbO N/^5}(iY<]\".wܹ%%7n\zxjaς Xaӹ3{'oވ<`wɋluk{vmgm04AN^n?l9v,All~?rQxl+ATx-ܵ˟Ή-@} K%<".\T E}~־֔Krsrs_ׯI/aӹs3Gj$Y &F}eWhggѡC|n\Eشv߿˸ʪkגSYTT:thժƐ$[|m oߴ#4D_z 9(99Li<(99x4 ̈́5d:/fW iᱱO엖o?}wnjyEŇWAo>~Yunn{ϟ'/9N={0Ke߾zLtl50o׎5>'%m<~|m[rٳuϮ][UyEȿnܸSZm7OTkҔi6=|xС#nf&, 0".ydׯ_DE?xiӸի)ZP\tϞ?Μ:Ν)B|~fn߽{s7.M,M6'~XTѷItfy />xLʑ;zm(_ؾEE4o/?~sgٳ’U7Ν`iIEEݸ׍lmҲKv|>Sb_=?rëW+r8b%߿yQyEşx} wG66ZꔃWT|LH *n7@EYQLcfb"௢)(l~)8{5+&>kjE8As%J+'Oqё#%'_(UttrH,8Q8**+ɷ ئ\eU@=>%EƝ  bhg'fĢ gx>G?07<6r͇>}65=`ۖ-)))ll$m<(E\n֏dFICƝ@0Yhx43d $@V}B3R /FEI}}f7-rsX,V֭()UxՏ2&F~uf]~(UHЇu"fXpӉyz(Ǐ'7-q UT:jj**(WV&~ra#(r8Wv?8Cv\FP--+kٳ;ϞUpڶ5jBWK#/_QYWTFxyxn&leEE36͛hjr** ӲSR6uuo3%/|;8XWSӦsgCB~Qїȸ8M=}/m\ݵkŔ/ _i }}y/_DmtqY;cXm((.O kghhܲe3UeeW~ ,ʶښӇ:8k%־}}g,$京={hghؾuk]]%%>_X\GRFFBj*S( (r8{@۷3Fdd>[r}hޒ [MK]OO / u_}Sb/ëW7x,Af&&N={p&Ḽ[ŵvn^F-m_SSA͈EA^DŽzl'{>̑#ylmJ!C<]\<~<v .=|Xge|P6%.dhp-sr8i@& fr t*K_GfBNdID\|/Rܵݻϙ#K_k{[YŧvM`iEe``gB.7<66<6&J=/.^8re^a#DÜ1ct55n RRV&jBl:w~xŋP=(N|:\zOavvq_r)4(M2Ml6[6KJ*׸qۼ@GɓKA_SS2 @3p 9L˗UUrrNߟ񘵵jӧS/> iӹ͛ܛ hҪSϞLK<=ɷ S23[Tx.8Q@Zٵ;ag׮0Ҫ_Kr_ɦ)bXܶݔ)/e.pu b^~0Yh zvZ=cm>k6HK$;'h,N,G4Q4dh^ϡUf/ΜY]hԩ 2oiuԹ ׷On HCn:I2Z1e}T0ř3ڴ y9 ۼY)]ʕirrјIw_s_d ֦۶dL8H޽U02g׮6U<˗ NHKH_[{Сh z\lլY;CC7#.G[[ R.ayM.*q8:ȬھBr=*[DuPb1Zm 2ۗeǎO 4'*vp&bGjȦ+C†ey{G_(r8;:YٮK HVV/N>qzuŸeCV0sfkY^QqMr}ьǤԳk;w h?=^1)CknV7l6{'~ڵ??czUYUu*ϵo&k{sׯ߾jj[QPeXVؗq%碦L.VxcGOJL %8ih ~q4KYYU_yQT.@)yh=RW'7Tou[tp}Ϟ|ݻ'!`fb^MEuueTr]ҒtQjj7-M`PX Ǝ]0vl!,}3NZ)*JJ[,XPX,VufC7XX,{ { +WOx urݺIejjjK'N\:qd@_[{̙kgΔ~l[ss[sU21 m7}ƌghFSϞCCky<ފ}nuWn8v\_|ɒ廕 C;~03,Ȏ""[J$sL\R1̲ĥLK呔ʴ%M4EEAEEda?}3~g]_{{x|ΜC'TVUWٺ"`nش3~!pu?>6{eܒx\Ӳ|.]pas\4)S=9^;C x}ي'My 'L4~!y @njPl?F8`==߹9o9?ཏ>x /30]W\qun޺~wGyTjHS^>;zsjݺ0cW}qc9d$' ?'^z)Lgͺ;1Ǥ{hIJKpg[o5UUo.]ðf="yΛo;﹧t]f1]r\瞫ڱ#]5mݶ픫J D"!;tn:Μ+XaCaӦ?)OuN7w[mk;]:iҤ31,_L#DѩW^Y9gqᔶWTlYS@& @ڄ 8! PK}mʥܹŭ۶Iy{?Ko9pIeZ?yȡCl]~ .X* 6ԗѪUG߯O<6n\?Y5]o&03ZaӦ]t-l޺>A2c7&V4WhLA>w/vY]+ճ)=ǎԩIzz?fw4շ:>'y?~Տ?W/رߝ2e;߾Fw/4nԨ/QzW [fD6oSO|=ƌ7tȐ!x'bE_.u5;kش_r׿ D>Zj̙>{׾~4 bt^{]yYu6-XdOdʔ:szϿ]>ӧc:>e$| Yb륔x+ө}'O>jmڲe̅^ti?0kǎuƻ^~O֕9۹C__RR,O|뭇O`ɒe+V{}w6mk͞T]]}C=<]t! -ΝK/ߴ)KGy7ݔ[f8e}ܡCeUGW[{7. [f8򠃎80V_?mi3gva8`!_۷Τʪ,c?xeUUbvm:iRТ_g|n]bMwmcǍ5b}vjݺlܼŋ_x+*+4\vQG]w݉_\`'} ǎ6xpg{A{5tРث{'Oeyw=qҤ/;Cwu `s~{OΙSO:h'`A?v:s=tȑ}āַVQϼ/$[|'L #+ A˧[da]~%SԺ3z?ܳ_:쨮^vחY^V7ѣ:mi9+W&[k^^V!Ӯr7"jǎ?>z*H:oo}nu}%N<ѥGVn4slt!]DN:s֯jO{ ۷mk> ?^'3b ^_II7xĉs|}>\$٭{eVHd矯ٰaʕX?cCq= :v\{yGQÆ җ6mٲ`ɒOֹCc[qンf=8kV$)/+R^{έ[ڱ>OZ{BII'uא׳[ǧN=?:;lܼ??d@^vۭ;*/ڱc֭ͫ{㏓|;b^x!i{Eo~7?\VZ:OکCeeU;vlݶmæMk6lpժ~ad#˫g?û~xw^{睭[{ؾ}i++o5K?H:'ͤooח]cO?OG"^ݻѧONڴjD"m_Q39yG,!F b)Ʀ͛_'6mS^V)Zڮ]Wq96nLl}jΜs~'O.)) {wOzW7`!C=z|Wb.?ۺYbޥKƽD׎8b?Z:;)ON-Zd{{gٵcǛ /^<┷:~Cvm4U**+ZeҢk=qF\$_w%&5UWW/^|{5^?>I6keU՛K8rJVλʪ$>߾}ޢE-JKCJ;txO4^KUO,<Bn23*rqr@^f„)֩SQ5>}:MVu>0kߞ u˖ }V7^tsm]vF_pA+׮ݴeKC_%NK~i,/ocӨeYY~ ^ qivj:ʰ;ԩ7]|q}og~kGC;bD$SS;g,t[N/<}()g/zsOyC5lXD:w0󦛞뮑CixW\CL~^{eu4꾻}o߫=SMD"'.|cַ UyI#GqiÇLS\d2&JTZ|uƌӧ/zw:e u{ee-y񏯹:[Ig~/s_{?_yeGuuG1aܸ~={B_޻G{ﺍk5ma7!n5￿ųN8;7 K׮_$n޺u5[v옱d˚ b8m̘ z=LZjڽgω'|'krhw޹gn.6.>.z3+} 3|xٶM_}vm۷?dȐ#H/uVZ~4˖ cd@޿)^Yz9oQ(Q4ݫ_6l֭9CvM ձ}DݥEM%M#rY{Y`4HŃEB';7[#xDW6`D"A{??̳s.X"!N; 4ha_?ȁ}deg`e>Yv˶m˻v8Oګw)o^Qܹ,\ɺueez:h{YE k<5gQ\P?abNy320zħ_~.sϭye{E?_yeo'kVTVmӦWC 8򠃆Uغmŋ,Y?Zz~eKEUU4iղe6mvس[~={ٯߐ:mۯ-^'lܼcv{"U] ,׼yXQc.] ұ)A -[Wx͚6m޺u{eeI4ڪeˎe]wܿѹsXaëopٲ?86-^cG-[ܮ]Ν{u^o~23VQY+֬Yq?^YDZlپmNwynzuo*ّA-Z|+W[g}}{uԪUwڵw{wݵxr[-Zƻ.|ʪT֮Mn:֭[vs0 3QP l')L ?l!E[{vv5\7q&LxҮ0k;'NV@7֯I1+HKu_-JJzѻGlH$yeZIHdgde01>֕ Hhd] و1|[]wպ8 +YlYͿwl & PBȊ;%\%_Ւ Z/?>Qr%|Pq~5N,@1# P$b+˿X?^ݻ裳2{}֕+<3+#("2dS||Ao vTWw-u ʲ2H$7{o+}>dk<4lLFad PBB8}f͸/c0Oʐm8Fw[wW]|J=Q0E%K\Z.P6n\YUDvTWoO֮]lٓ/smչE{El4'Zm>v:uxh>d YRS.c,(6_⊧_~9d)^:tРfOxog==O,@A !)g{H$ҹC N9ʳjۦM@& P BJNbTY Kф ?='9_jepesղ "L9bKXKh HX)0Y4#+ /CZ`F,@E"L1d@s//㭖@& 0bd bKXKq E,dW||qo8 0Y@NS.c,B"Ln2d ŗɗVKwM,<2>2kdW و)= XI,@YiS@~& 51Pbkx@& #P K%@U 8a@q# E(OY:(%L( 2d?yoU F,P)X;I,\"e#  Y xi2UV)d0Y@̐~BFFT@6^YIO@J!dB$a@# 4X!R)d#CȌxY *Cf%L'bdU)KX% Ly@, EG"%ުHK,9(V,Xb,@r Y 5(R6:4C0Y K< *M,BfF.*Vn& dYY)K@^/!Se;Pl Zv0Y k,Vˈɓ [lCa@ɐGI^ [BP$G,9bdtRE"Ndf'C &^$/ %&a@3# PXhuP41d! F"e#((zdt# P)d 0Y BfFd/V1#Lh1BBP x 0Y4X=uP|@1& 4 YLW˱xr 0Y ,1YRfJ3(HddxX*%F gj[ 0YiD֠Hو:0Y=SeZTYe䩨w !3d#O  Z?&8 G& c`1~gZq@@qɋx0Y(Rbd ^KY:(YA< P@q! Pl%^0ު$ ɓ 8w @ b!F*a\#O5%L Yj xd` X=xuP<@.& &dlD % @#+ZbTY%d0Y{!3d#~H%V9$LXY>4HL^x[͡$/3m>|3G,%1d@LY:(B\MK0;aOdy23yYoU4])7]v J=h=LAH.$dY<:@Ios|j.v J=h';h&9(^&/Z㭊V@n @(1BFh4 V"dM9t @.ȣ4EiVe biz6A= ^. l"aB=Lma|x2UVy 8h @ S@dl4 $A|kZI5ςܰ ;-4@N1m_x1*~/`PHJ=h<=F$A_ ĄmCV)q"5g rv;Gͤ4u]jȓyu@rR7yioUA6lh:;y%:hA  YG3$64lRC!G.@/V3@$ ִF5CFv ÄrXYH)V,c9v(H%_/A.!1!kE7@J-͊m@( я! /V9P؂ 5FQSYG|`d{P[t&ImhV!<@Vd? Be5LgScB)dXOY;(ށbFMw ;^%FMI y{ζ@_Hٳ)ql.]۶ ;ЬJ=?iL)TurGNOY;>|l0YrE F?$ ѐ)x2UV jMzh@e`;-g   w5Y &VŋD(ɦMB D,9:492dŋV@F4rlHl$F$:  dr %riК``Ci@k76쩳P%-x-y>6Ŧ$LA'D0I7'@flPPOs sI~! "H{`}Aع& @44Y !Sem&%Av)!^x @䋽{ @ Tֈw7c %;FdL2%KD5} ;lb2 &n }jun8r'!@&%N_f*rAwmw@\i@ $[!7B[mD9"ڈCn@((.a #(!B~ @晝MvJ=]( 4COMdQ&h4|^Jclh:adNMLu*Ȍ.DʭX[@^>|l((Ihd/4ip rG" 9\@.hmHTP;ٔda9{:2)0{I> lh adG3ݛ4QlB,^|`ٻ$abIBGJUXRܶ(Z뽨ZEj)wB- jhHD$"<y<93dy\:_Kɵ|/lUYkq'0r<20u&<H:.U*ɲT,6` % + $ j ȉl3N1YؐdyM·Ș >crL+8=3*ɲ8gCM]e:A-ZIRP9~° k1ç ^0u{'fR%YwʩtT܁g E`;|#)m1nR08 y!KmpO`/`xqw@Y:,&x=1uBa IN^osogtU6΁< kP]Q*2?cu L}t/1i]g΍=`a| HB1Y޶ѩ' `uV]&pN~ʀ> {g@LiI/}/f G'w%P0$g#Ί^@!G8{^pʎ \{gTr:CV.&˚$g"^dۣݏbPT687ppKp& ,1d| Mgh]e,Kp:<3* `xLVL\J?q02a0 xbA$8@X3h,90PIPbpn T `p8@)sɲ, Rb=YKJk F@,@BY1 P0 ԓ1%8=+[R֤~[aQcLpzoNW> , b:QT:/~'kv-0{$^(, fZ1Y^dmTRVK*8>f*- K΄^J^ ,ôb:TlWnIY?Q,Ef`Ryc4T0ePLV罙eI''.@k@E`/aԓ1,`; PR0E 1-&(eI喔x(EBc\ԓdc,øTNԓ*&(KA,a,^ ^SQOQd(iP.`/POd9 y_ דe0@,TL:K0dPi_L7c/ PR,C SvFxԓ@T~1YA[RVd}QOP9du^u3@ef דeHT@^ ^!&,*rHJb zT\`zFoJ8x{@;Z* \)$yE˩\mJ%, X> $1YN^>y1N^&.&T*eBvG{A,f@h/pd0sw( LT2eP(*{'(d'>>).Hk IDAT,`/SIU*e$KQcbb'C*p bPPL*L||SX^f(,+`$WGRiL-0 `/˹I!//@Pjժj{'UXXWRRR]]]O|Aj'3jsssZRRJժUQf @%'B *JѸ;)8UVR U\\[RRT*AwP<}HPi4JeAW(...,XK||߳_2 PQdgΜ9}+Wnݺu j֬YNf͚nݺM6Jj/]w7o޽{777ljժխ[A͛7kӦM6m4< \|رcΝOOO/)))RիסC^zmۖXxS.^xԼ 4M:u4hдi-Zjժm۶jղW°An߾{…7nܹsѣGeTRG˖-۴iiVrʙ3g._U۳/ޖ-[n: ^ Ǐŕ(XRyyyխ[QF͛7}Zl&J'///..ٳW^MLLLKK+(((ãts=GY`!ݳu˛CKMM ۳gOvvI7Tx;w֭[ڵM}:{̬E1-Dȑ# deee͞=v{ܸqv"… 8]\jm۶;wѣGNTRzҥK-LB8;sÇ>p)StV˗/^ݺuktttȻۡC;T(߳gOLLV5^WWݻwܹ%8>(==]'h/Cݿ?<<<""Œ7nܩSnݺѣN:3f/ӧ7nxI=qƟ}yܹs˖-e$hb+__:ٳgkΌJƮ[ldȐ!B+{̵Bkka2駟~;wz?ߥK]6mԤC֮]{Shu3k׮}g͚e-X --Fׯ_ܹsg׮]IIIۼy ^xYaYVo[%>C` ]v8pǦ޴iӠ]ٳܵqqqfZɓ'w@RdSWA{ 3sJUJ5jԮ]A-Znm[O}w^'O\RRbqj/?z?ӬYf|Oo3fsɓ'7nh#j[k֬Yn݆ hѢe˖V.t…+WZޏ>&Lxm?`/GٳSRvX| '3ΡFjaaa~yu3KJJ_~m۶)_5 O>asa|a_pA'h?~vXr  |y=hڋ/^x1,,ޛ9sfi6ɓ6-پ}{cr(,, nڴ3 :cƌ1[nܸ133ӼΟ?o۶M\'"JLL |gl͛W޿}sΝ;wvWvs{LLLNN4ȑ#7ncǏ7u-]bbŋ>쳞={˗/:uJ'jlbjWfXAAmySN:lٲ-[<ۘ#+ӧOt#{X];Ν;w5=MϜȠAl9ؼZϟ>}ҥKͫ$+fԨ_ӧ]IVqǏ; B ^dٕdUddd޽{ϟ食my?`ABCCNjv%Ys?~iҤ_|144ԾD:M6 4h޼y2D||Sk]IVѣ_}VzXlldN?qļywA{0TLI BվNK~aqqqWnnuC^^޼yMVvNC}+ZŅ BBB~ǒ=***9sƍ-vٲe콷 ^ lAT%0N&,,l-&&fȐ!*cݻwȐ!qWwHOO_|y߾}oBaÇMlGg#33sܸqnݲw"p\ .|SRR PÇK^a2͘1/da.e$˗$[jՠ6m4jԨFJ2???++wܹyfRRe^qq… T?|v7nR=zxլ,<~Ƿznݺvy~+WhѱcG T^]Ǐ߻w/11zl 뉉߿j;o޼"mZhѡC__ڵk䤦&''_wܱzVPIIɜ9sҥKQFUTϿ{j:4gΜUVTJ?},bJ!G.YDRl׮&Mxyy'O$$$ܼy7֭[ 7o޼v...iiinݺvڽ{d镔,Yyjo[@@@&M<==}񦥥%''߸qڵk͚5w"xE>7xxx;wޛocǎM;XS||j{dɫ^^^;wkР O>}QZZ۷eNȘ2eʥK VZNڴiӰajժ)̻w޼yڵkrw522rܸqr3lٲE.^މ0 g\oR;wnxF}!Ch4?y… gΜ9qĥK[I]vճ KG-M6&/..9sC5hٲ RfM}mAHLL3V*ʁ]t1P/U䘘'N8V}.]X۷o_h5j3f̈# *fff9sرc;//=* 4 -m۶6|V=r8ީSr6j W\o㮮&Mz뭷jժ%oڴIu+WΞ=$-[W^eee͟?_VɓGUN_gΜ-w$>>>Ν&c u.]流כok$==ǎ;rHNNd>,yzzڕ Ȭd…vo 2<wرCeddXk^,TͻQծ\rVI 7nӦJ*)))ɣG߿ɓr{8vȑ#hذs=gy? ռ PTT{/Ç3fؽ{yOHH(kӦM8;oBsΝ>}IIIͭgϞUT':gq7xcر/˗//JWN`ر#G۷oWIVVO:uԩYnaÆ 6رcv:zNoMIV;wE **֪UkÆ 3zG~x☘;v!k"##NjL-?wׯ_p‰'bcc;HLL7n܎;74G?,B&M TRRRz;GÖ~ f̘apVΝ;uTRYnu V( VU߆O<5[o$ۺuO?ԘQ۷o~ƌwݳgΝ;ݻWMHH!BCCm۶-,, Κ5B:u |%^xСCv˓lW_nj3tW(S#>}+V0ooѣGǎ3Z87믒wo߾W_}՘eyxx3f:tFV\g;%%%gNHH_R*s駟?Yf1cٳ租~2tt;wnsbk=|A?ӢELV{jժǏ[V͟?_wѴi۷$/"YCaVPoŊZN۶m ֬YsӦMTաhrʘҚΞ=駟J^ٵkٳ$[ۀ֭[wȑ#FX!$<<ᅲԭ[}sԨZ /Zѣ:b^A{}<==t2eʔ~'NTՒ-ΝڬPT͚5+]7$Y~}VV̹ [N0aڴiT;}p+%{>qfըQw}7:::44^z /۷Od`@jպu'DEEWMVVVXX̉((&ڝ?Œn4h`q+W,]TRHHƍk׮mI/%=޽{e{\XX.TK;n-KTvٌ&226= ̙#QF;wԩ%שS[~ݻwufÆ u1z_jժ:񔔔 6X۷wma'`+{ 4ի7nؒtбcʐ@xxx 4H|ڵk%/}M6dӦMyyy:A??ѣGرc5kۼyY۷o(x"00ZjvI[nU*ʕ+$+sΉk^r)9SN; ԩSgӦMKV?%;9rg˥KҥK`nhŊO>_x3fȟ`Cll ()GSj)SH^:}(&+-99YlРhZ}>S1/_nG;wȑ#⸧租~T*m@E!>zeNN'[N\TP˽{s$//OBi̫"Rj۶mftXVhhOׯodh[V+;CSiZ}|򉷷8e˖ `,W_I^ڵkWZZؑKqAn޼)>PI;(AjP(w-k4/vvY IDATRb7xFHHHHHs47nȜ ҲB3RRRݻK#8qDllM~z{iGW Zv:^xF<.;;{ǎF-fؿVVNsf"u,e ;X`]:'1rrrA#P(RSS###q3gʟ]qcǎLs8 .DGG; c 8W^xqq?,>vԶm[xzz̙#-**?8͛7K5yd'No4[r>vհaCq\|D%( g?cjO߆ٳgxzz>|X'lٲ۷Ν;!֭;zh[ؼyd|̙L }رc 2q Z0aylYYYOga6~Gرc3227n8g 3I5j[h!>p%%%qoBhƌ#>y VZuM|̙3#""t\~}߾}!!!$eϝ;שS'd%?}#>(>>Yc=xK/$2pz8>f̘ի˟`$gC޺uڵkmڴ#9X)ĮÇ?[Nr,PI\qĉvr(4bwq%Kؿdu֒-[VRRb WWT߭~ >[ԭ[7///]LFJJJz8ޯ_z'v - BVwžv*תjOpQQQF0`3gj_%.fjxɏ]JnN:^z:t(Kpp#G8ɯVB1|p3zN>-k4 {Y8cR?F Yoȟ СCJ:t8w}M'T߿dS*DqPVʟ KJJ>ü<#?q8$^o 8~lcǎ+w׮]wEDD G~0BAAAJJ8޺uk\은RT{޻wN<''g[l<-''?{iy=DXWӦMG}v+uf-qq' >zȐ!ed:TWVPPK/8|nnn={4+Xٳs=gnܸ6m̙Ӊo۶mҤI 6(EҧO:sέZj^O v'9ձcGAZZZ||8ޡCڵk˟êWرc7nܨ^~*U%+xxxk… :k׮U8PI~Je˖>>>}W\>}ի(...??_0`۷oN$$$/ r)6))).] ZVԩlSVIU*Mժ5ڞjjR~f͞k޼O@%*Ie5bݻw_}hݝ={VrJG'r͘1M4v֭_պ:sde:/;uTN>Nʼn;vdq???߲eKq0>>^rÇoѢN3?9oժU LvZə)8+WݺuY0dmMRN*m6c:ZV|Swa?T*G)yĉ#Gy)bcc%#KMMCRF%زWV(k׮'bbL-˶nj՘ :z_;u]ss{dܹsPP䥬Sσd v$uJ~o8jժBk=E .H>qz8~xNNNوjz{,waK%%%|ѣ%KY]xQq {9dK.2g&N(Y&l_lӦdԋٲeGR 6. 7xCׇz|;wN?H׉tۻl^߻o>Ç#M_={Ŋ:$7뺻ʟ 1uTwww`jjۭLq<00PTZ)N@|TuHHhС{>lm)_ yܾ}[2ްaC3*?s:AVrJs2L:Yf?W^eL v믿AJնm[q|'NW]y-[쒏'%%ɜn߾uVq֚yxx,X@+VڵKʙBWڵS؁ 7ܲq}ʐqɟ zdުU=&N{1y֨a͹*W?4}z>}[Nr$>>^lժKVZz-q<44Z_ q򢢢tڷoߠAq\0ɯV/v"SU[I5@R͙3Gvə@y4w}WZ5} Aػw-Z&gnRlժ@P$&&͚5Z85jL6M ʒ?S5jH2s-,,_>}`ʨQ 4wQRR"[b_ZZړ'OqÑݹsŋe#jzwuuMݲ޾}8޻wo+WzLjDrf9^}EntҾ}$ܻwOoѢx&M fffnٲ*'''KQѣGu6o\r9J,2{ԩ[n¦9*{zz\^|Š q|ٲefjݺƍ ԓU(Zvꫯ233e 2hذ!Kӧ_n'SQsrr6l`|L";V7oNR#GBP*}K/d٭[fϞ=|(A 2U(͛79xR={oQ(5jݻ8~Vk`/Ι3Gwss[`dVM޻ǰ^ѻc@?Z5k$9-Y˓3AH섁qy{{G:uddd|aaa3fx$wYܹs?Co?qD&*Ri޽M4(YC6lhs!5jL2믿։oذa̘1cLxF,ٙW&2dHXXNPQ aÆfPUVҿ[aaNѣGfT*Ν;f#GΝ;סC3:v/L6ݻn޼_~0aqzֵm۶poo{e|O?/;СC+ٖLGKzzdGLʕhÇ/Fyw/^߲eufJfggݻYҥKhFz|pq%___**ZtRknyڵiӦΙ3{^ܷo_Ѻu;wZ7;gL?!޺b&Mw } r!`ffflllKؿG}SEYuNN% ZdU25[w7n#BrZڞGݾ=AڒK7o.^Yl#AQL(C iݺ9s^jejj~3gXRj^eoqyk?![ُ<Ǝyf]7n<$U2 =;ܣG}@@@&Mu^rrrAFcGTVMߢsԳgOe˖㏔$` [޳gŋ}uYf֭'O?~|jlV5rMRII"o6%%%OLp؋S"%_]{0bĈק YO>lzbft;vXvmVVj7nh_pX*jܹ:t?~ff/^0aBΝ̙ӱcGee^ KuL2J9 Jqqqqqٷ3 )11ڵke#nnn׾_~FI1Y9 pPRӦM3~1g*`RٴAnj>b7[~zVjJqmȈnN McbOȉb ߰aChhhKj?u͚5ks?{xxȜ LhOg7oyD2^V-Ku&QQQe#+ʡCYF'~̙O/XZEl777+>B7iΜ9b111`#+V2d_|fqNN7o;UT'INnnVǙ}yyyɜI:m۶M4.Yjժ%+--ƍ?/))HHH;}a>H@hRi*"*`"bY{ tY"*mq+6ؐ"X RiQUQY>mZG KVs|%TTT͛m۶k׮պ7o,X`cc߭[7jAXSSJ@ cǎ=x`nn{\RmRE:&qww}i3Y>1eʔZrIZkhhz,(LT&)S|efoN| m#ťO>8x`EE蝿~pŋ6`PS$(#arհaۗsf݁ʪJ]j}2ڼy͛%:eӦM::: * hܸ͛Ǐy椤ZXd hݙǍ:$55U *J|>8eCQfNNNr,jA#@GGgӦM... * a={?7yd~O(1SSӠk׮ 8PSRRƎ|ra C@ڛFaRnM9sǏr?@> Ӷm֭[I {ʷ{)HҏbamU,Z6%%۲ݻwMFP@^^ҥK'LAAy /0T+&K_|۷#>Ҿݼy={I_]t9}:t aaa޷o{}P/Yt_^z%pUCCֆ"ƉfbNNNaaa$ X6u1|PQvUgϞm߾=!!/] <==۵k'jjjr)={vƍ;V̩Pج(3|} <%S]]gϞӢihnݪ9bcccff&(:sNǏ?~شiZ'ݪ,WDMXZZzzz ă A@ g̘_xÇ_lٔ)S2~ѭ[7ggg']7ruuܹԇkkk׺8zWSSs…RަM9󃳳sNRSSx;vڵKjbP+lll "u%< ")&9~xooohtS߾}oܸ%zt;vȴ,XШQ#חK u5p6h [[[ӓ(6-8pԜ\"y،o b|||>UVr,B|qW̌/ f2ܹc}3Fpd]C:HWDSS[t/ш/^tQ)UժҒ"UvESNFD`0C qqqz]rssE{nGGG>qDCCC4vH~'O|>8e]c݉dbcceYklٲ*.Z}k s%m&#R۟;w.::z"x6m*..e1OP aS\ Hťcǎ/^9rwܹcr (--Օ.gQTTt}d-,,:tK8ͮUVV&/)) wĠ|{%=D7o>vS1;v k[ w-[ܾ}9svqm;߿_~@% G5")ݻwPJI"7*={ӧOWv!u9rϟ?عkϞ=  `IIŀ7o Ka eϟ?mm}sl^tuucl6db*Q IDATwD$iW+QKAQQYe%FTW710010PYUUPTt 55},UUjj[iiEeJYF eTUW|S]͠u5jnl뭽KKJJK94`hiki6jDo)F^^[P.WUE@WR6>_TRRb6nHEI+~//?~ITUT44ut 7V eTp8_|M*@ÀN߿~"""vؑ-b琐SUȁ~ )`0|}}Hl9s:t ENa?'OܹS ةS#GȘ7nr?GLMM<ݝL6555''\fx* {xH9 \tIY/^8n8]v{}D왐LP〲'/((pww'Ư^ڴiSt:ח\DtttRRR}%T٤Iꋑ :::۶m߿˖-+bX&M5jԉ'/b-oo3gPY!H u9 @UP-<<\ c˅ QQQ)gddDt֭Ç  6oLUikYP\ܥef^};*1qzzYEFv: sR)&9QZZʛ7srHw魚5ne5IO[[e۰Ǐ~[P l7аymmkױc;ss\ry+w=y[.'i&66nz8PAF]P>~|gϞgd _Xjij¢s۶]۵ѩ=()+rnx|wyy[ F-v6^YT);7}&<RUog7j:):NUUw=zI,:̬E6mzZ[[[Z6D >677Gii߼y ̬~55).@^LV khhhppy}6lǣ$ i<77J@}KJJo߾R$466&nZ?$N#N!"nnnD>D`m1mz3lYYY0(켒244{Tn::;,x ))eŽ2?|8{|59F=[^MT|;w6;8x</=;;=;{hhscS5*\stU/_&|tM6gѿܟZy̙ӑs r bDijm۸U\Zz&2R ´xzXnݧkWIR..wACx<뜜99'ij.;3,Ch&+L&~sssۼy3gHںukHHŵ7߾}z@:t:w„ ǏwM҄-Z geeWD ùzI ]km&knnN<{IIɗ/_`$bjjJ/**j֬|Bٳg9sD>}zƌr9.]t-[Su!C`Yzܜ8sÇ&fbbG b&&&[ ALa#ï^z=xrr'M+mmm~Z>y򤗗ŵI1u͓'O`3Blp\&I\d+<<dJUU5 `̘1#""tttR;_u5-(aCx|DGE&$ܸc:%y^WHHnAP8};ׂk'ur &\y7)I$߾=rdc?ZNXJRΞsNٻ-PU$'O^}~,Ir>}ղsxϝﲊ ֭CCO_E_ѺsϞ]gϊh#KͨK]^m*y_,ߵ8-ҥ{J+*D4-uަMfcԯf{Νp`~QJ79r֭;۷l)fnݺݻ/[í[,,, @---qqqm۶}K3|!p+)===//;w ;f̘Fsp1e;;Yfi),,vژ1c ciiym MOOܹRJYZZɾz ر!C"""۶mի I6XO׻Um۶ɩ%HyǏ },{=2[[gt1KKZŎŋ ˣkr%iW %eeŔi?mG$”"ι)Em„ݻeo5s%%2YD''I_>=-LLd@}:tݻ̀~:,/_R_ ח|QBBtuuIa&ɶ_@|2Va+8,ӧ7nX 7aOPcǎO2իG]ӱcGbJJdѢEĹanRJ=¤*** ,Fk޼E˗@L9k,ҭ>$jӦ 1Ӛ rxKUUq_ c":㡡r?4H,xжE +ѕ HI_iiUn?ǯ%%Vv5t]|n:kٳ6m1r]fM]$[TR2`;bm?=;qt;KٳgdHo8xpҥ$ې}2鯿xu] >e 4h{!n$N׮]OyDql+eccC |LL̒%KWj޼fyy@<++K.߾}K jkk7mT.i4ܹs7m$?tФIu")O:)99BCC@ ]v%zyyQ\ hnݺݻgmmMq1WVFq%xppLR""ܡCE}16u԰ɥ'N6m2t%K[ݻH}U ] ˓{쩔?<>Wa2;mK}=RJLWMraӵkKSSu5_&9m͚+۶I:G̬UV5kfdۨp>?KW\6kIY#l ޽[YlڴJMM¯_?f}2+yFƇ|IKj޽Ξ%dmi]M__BPxAW kަMfFFښt:EE 2?|H|Q\Z*{%w˗Mt:ѽwN[7ѩrOI9{ƻ<<oʪU,-۷l){aJ!?_Xeuk{hgnUd[Hz>={V[SSJ߿v~;hkj:[[o@]M~,,x>533ŋ85}˖v;ZnmѴiSCC--QVQ!?bb{RToQ\0L sνpB>&˗/|wž?>ŀ-ZtMϟ?u떋DzFGFFΞ=[*pzeǎemmSxzzzVVB Çׯ_%WAkkky0aÇ߿ٲer<Iro޼QJU )cc-[fgg >|eKc0<O 1o!t)3"ƛ7o_/`={.:5ٳ97ef\<_?#}}QCH;m%]ڵ5x>!%%&9Çg_X,Fs YgLwkiG][\RV&pT3/ j߾ࢲs܂iCz{7xWנE6;a.ߺhrED'6-Z{yqv#nbbDBBD|o$-lm'yk,czAͻtΜ{/\@3YG0&oxEEE ۷oOY3 ,KKKy>5Dpqf/BO&s~/\rݻw' _I汳=wbĉd#O8q"i9}zAthF y|7$_|mn:=9}zX~ڏ{78!i%JWv5bDԾ}tht:MS=<^f?"i%G_?A/0?t 9kĈ'ZF''MJod‚=t:ЪUG֬}ڬY3Yl6U?L444߿˒ShȺ ɅI߿\6m b߻@kihLts8}'h45+b{kkœ&yG[dkZ[ZF375~-Q[ ͛ 7(տL j(Z؎ZEDOOlԺ5q+W*ekOO^/_:nպ}((=rv_Z57۹EKTI]ٮgg ^={^:tepykT=RUn<\4&s@ ƌ@h y?َ1:XX;rS33TD4RMItB5tP8NOOQǐTݹssqGI߹s'˕,<<\ ҷo_###Miϒ"m۶oߞ/..>|,? Hk׮eɘ966400|Z,Z?w܇~:X,CCCbÑ 1ӧsQ_xxxlW244:u*1gϞJُv ĸPF]]}Æ rrr(( _Ԕd2W6li|޽ 8eMKKKZZZ˪=xϔ@2#kjWIr8b3Y鲉cĈx~~Mw^|~=馅c+ϖۿ=Lqg"#ůdd1Y"3%lݻ G اk-[JWtDGG%& UUT.lެ-N 5~~גdgoS<ݺu®2Y , N{}DDDyJpzz/87,,L !K>@Ix<^$ {A : N߰a:qŋcbbwj9ϛ/^^ԗ$-ǎYvljG&H5Ԝ1|81ξwljj-, (lo D<o޽ĸq[?Onݬ--8SҋشI*kxȵkŋI ;T;I_$|)TgN6 IDATMI \oC|&N,~1PGlڔ'd6% Iz.2x M</))(84m999W?~SWW0`,97nܫW/b<<<\ő#GΐrT1b1/uw%ֺh#G:hL&Ӈll8 Lo@N533ń !h wQo̙GaX"/rYnݺu=b۷-[BRRɓ'aý_ܧw444ƏOO>ŕ133%gǎ͉q6-KZjhтN%LOO'5k&]61-Ztzv/*8gv7.53`X^[S¤|h(1dd&C#?}A ^V+y/I*7-ԿM I.((HNNqɽAAA2V֭q-ǓecLjAkkkm666 'OQnѢRL8tӭ[8@q=`ee%lM6=zziӦ+/??ʕ8]z5馭[R+P+5H9`̘11x.pBKMpww'*>:N:e#)))//Ohݺ51DneyaeߴiHX(PgwyynߞaC3WW[ ߿-I,oIʶCY٤/,NBZdӊ9UU%1lܘ|w,Kr^%]$Iz}ySRV&MeJBzQ~U'ÉCJP6ݫgψPSLvGNWh4//MuuI:B.JfTrW\!{M}1 #:b M555sIKK$y fϞMs)' n5<|>?p"I9;;㵮t=uԦMn:xe Dg͚E?~R$ӹs3g0ht:}Ŋ PbdR_ hnzE끥K\ܹs?N}Iy])))5kVQQqӴiӈ$6`...ȦeVVV._Q_@=Cb3G#sn˛1cׯ_). Ν;AL$""B@ ޸qK_a;;;PdF\x166V}((\YG}߁˗`0'e[`- :Ą%MeS!}@1ֶj,1>tdj$U^YF}ãO)57'ggKMY:mK ~).0g !ih.J){ȎC@WZl55{FS2P|(RҥK>|2!C(Q=FIdҤI >}yfΜٲeKM'Onˏx"9=6i.+@ 5kۺy۷c#e[j%,//bl>n:#o۶%^g_ffk׈CR_ NWW?uŊ`r餛>ydMQӧO'N|%qS6m|||/It5k5jDܔtIKvE|2¢CJdeiiI)==}ĉB u݊#Zj׮۶mۦMb<,,LFV֋/'N~EС&da(bb"={S?D7{ֈԔ*UUIIxΝKhظ11YXWp8{{y]9VF1ɕUUĸthȦ,}u"_t?~ՏZ@$ٷYMfH6o%eeRd`c`H)E/Kˉq>}44@.Lʕ+,Hʞ߿YfJKKg̘"%5fDzNII>|x\\'l@dРArINuQQÇEؿѣG ۺwYf5>u#Ư_~%R8q˥NUYa;}ڵk߾}+{%KG5kFV#F|7o?}FeI?u֤fΜ\]]MqU… XǻvСCfvikkG-[Fi֭999@I&r8Sݺu+44>}:N=?PbnqU8e]^0S6RRR~={k׮8֭[:::nnn2' hт۴iee@@ӽ<=.^أkL˗}KmZۋClx78QئgF,^[<~z4ĉdckkxba[ g̘㓛+>.WNgUVuA:tR<Կҩ7o3իW7nHGֵkPs.hO<9xI&]~]?+--;wn*/VQQLP:H:a3gHÇ!!! {DRWW߽{5|>?$$dذa9e˖ԟܹsÇ_xqqq1qUUտBp رc+VH @ ??޽{~Z<>>>ĸѣe(jժU¶sȐ"yuuutte(~!A2a2ӧO'ƳHc$UUUxLԩS555PxIJh .RV e,U)ϝ;rˑWHM +4[ׯrkIɽOm:rd6m~OIˉN97hٳӦYWHH{,sϗTԐE^2^^5w[ {ƌϟ/(*שkjRifb"ۄ+dĿ^\o֐}onl,ݫA`^feƻ[YIse>|ڵ-  Ie ׯ_766vsssqqڵ+s.ZL*++e3UUUa%VUU%2 iHqTTTPSS*[[[o۶[ؤW^M>z"999&&֭[?Θ1C$^E69VLI"߬Y233IW!,,,""bذacǎڵ+@z?{LIN޽_SSs~7o\v }3&//t߿8p`رNx_sǏ|D?8NOAq8Ҹ8qt[Er .\pܹsE6ڹs ׮][krEAzÇ555]\\#Vݻf͚w:99ɸ[uu rr,CGeXZw=x1c}Fׯ_WZ2~Ç79Fqܴ{ݹs'%%Fm޼YYmڴٵkܹsdddxyyL2E{NcB***iw񪫫+**JJJ >|Ӵ4QY,VHH\ꩩ eƍՉ'O2ekPo߾9rȑ#۷wss4hwHs1b/ J+2#`ƍ>t萰޽;`1cӧ֟ׯߏ*..666^rC# V4;wLi߾}֭uF ++48͞3g΢ 'N\.˗/_622rtthҤ&[^^^vv;wd,JğE݉d㝜Dkjjzɓ' ˗/!!!!!!ݻwo׮ZMMMIII^^۷o_|ٳb"*ZJDc&LP>۷~8 w9m4<oΝW\>}ZGYʮ])@^ڷo! W^zUUUή{]tСݻP̶l"sΝ;wN ?srr:pʼn3$==D]nݺuRF?m4-,,=:uTyyy?m۶mkkkۮ]͛7nܘbUWWeff(@2''%KX/%%eɒ%jjj=zwN>55UUmCjj?^WӧO>}ZCBBlmmUϏ`yeѣǩSMVZ-X`ǎMAAANNN-Z h۷7mںUVա***?~y5a.]jgg'cI1Μ9#]Ir\fڴiR`~~~"n@;wܹsGSSG?&kii1/_{͛7)))! W6//˗!>.8ˬ, KX,͛'NH{._`awEސ--E&$D&$XC0d{ު*ҴE;g驋8Pmn'N府@];P|>kBVRRr͛7o_CCCccc===p|])VQϞ=m0nF1bZ\nRRRRReTUUңd5iѣ,((r.\.Cg۷VYY@fLFYXX?~Μ9ʒN}``eˈϬYf͚5ffffff޽#! Y[)x{{GDD.4< ߿蘚뫫x_^ȑ#7^P8++Ϟ=;''G_~55U<<Wun4!!!**":ɲX;v181YXX5J)@DM_GIIɫW߿ݘѝd[n}Ysss* j*44tС.O߾}/_ܩS'eR߿K.ʭt֭ǎC'.Dx)Kh`@{}rsss>},ѝd}||/_NOlt :dhhZWt͚66133Y5kFz6,,^?~ ޽._5f(.WWׁn?Nq=rDMRym„MGSi&-LLMMMM5e(N;ztk+OiUu;3c߾ esWJR:ZZ>=EcI[Wx<1(ѯ(Σ4cw9#{MQ~XT8ɂihh\rϞ=$ YXX\paʕzzzʮd2}||pСrY7l+SwI_̲13hhh[;vki C>}yRg077?{!CX~w-q4nܸP@GGglժk˗/ET:xbeG[ns玧'ҥɓI7mݺݻwаa将ȑ#=o߾SLQģ"l+DS6233_~ݴi'. Sʱ*)5khii7R_@Cu_n57x%Ka9~q#  ѿ"jo ꪭ)|>ȵk~-cE+ܶMr##CVأ!nNf} Ad~RFE93GҭFv/[sM IDATFFI>G /ePG矽zJHHHLL,--%5jMLLUAL&sʔ)#G]PP u*uu>}ɱº1!!siiiYZZZYYu_oHfSQQbŊ'NNߟpOC__SNUӭP:::+W>}CCC+**Nehh8p:8A/_ܿ_ 8dEkȐ!7nq6ݮ];EQ }K.q\urrZre-TDLLL-[zj2wW ; uubPKCcϢ"qO TU7eԩCz3"X#ѿ?Gܹ]TRREE. سm;k;w5bE gѴ)ѣ۷k11ܹ8D=;sf‘#MX@E3jE P6#Рh ُfw mw9R񈛜{\2eDݟ#o޼:妧'%%%''y Ųuvv:t,+hH#KZuСm۶jhڴiESYYy amDr qqq>}eeeeggؽ{w5!y%x Nc˰0IHvڷo/zׯ~bd|&&3wQ3CKXedDqm]k+uzkkk]ZZm]jouֺEV\PPT6(*a @r?r_.7L&d&|>fy+xQGuܹ&ݦm߾wޙVUs#FϮ׭'6m*جYjloև~8{*ԳgÇ9㎫M'NXX+2իk tIqu_|7XdI=o| .hӦM|=WvǨ`ʔ) w Z^^_c@⥪ V^=}ӧϞ={ɒ%) k_7=zԤӧǾ ;92>ϯvH~ 6Z}߿&=A0 &L2e… s`#''&G6Ʒo߾[n6ᄏye ē&M׬YQ={Z!J_p .VZ5srGqD׮]+;> 9ԤI-Zm۶cǎ[.2\ZZZ=(;L4)2#͍ ,lx9^#@a~ڻwYO>s9sx`VFug=:333 {クQswǏ Իw$},Fロ#D-[:tĈ{Aj= .\`A**3q|tǞwyn3g5yu_o^ݻA{ӦM;xkMһᄏ{rq")SGvڗ^ziĉrZhqꩧ^r%5yqc٧rJ}N0:t0jԨ ԅ3<3##BLb}̘J^o[_ ƍKJ=ZpwپZT ow-$.6-anrn-޼YGogs'kј1zkz}O1cɯ~-[ܥC/V1<8cw[Nx㯵ؾkכ}OTѣ_*h~?rO??B1D3裣>:>|x }ڳ5p`뮛ېVeWxHϚuWD֕  [|׿{Or?\{mT $[;PnRl,ZhժU7nܲeKAAݻpӦM[jվ}]ɝ@ ֭[7+V|W۶m+,,,))iҤI[nݱcnݺݻW^˗/\l0)IߤI-[vСs}߿ggy&W+馛]E-عs,Yf͚͛7ڵCzhϞ=۲ed?|ɒ%֭۾}}222rrr:wwGV.uǝIu*^#gϞŋXbÆ 7o.(((.--mڴiY*bnnn>} ЪUdKm߾/XdW_}Uo߾p8ܬY޽{ei(**:`|r͘1&?\6m*[ ^nݖ-[ pfff˖-۶m۩Sݻ8}X6@tV0[=PvSƍm۶cǎ){={ٲD.^o_"$$ '~g>ވϴǍWTB~ݻG.\rIIz&VtkެwN?;ҥyq'WU&۽sT˖N JA#ٳsCl#4/#dv'@ bϭ ȥ#@@ 0 @5*lϟ;7e<[3Ld5.'dsun>rڍ\N*,*.hQ- ̝wBȭNVeM!=9m޼F8ԿgϬiiiG_a+P#G x '$H*d{}R^aΎlOpg|8 ]wUiҥU~GT."&w?ܽO6,q֭3.̌GO1c޽6lO6$ + /NmpB9ғ]qڵÓ]EZnnnKa Ba+--I&. 9:w\jv駎9nr[jU^n ! #V{v钀בS>oV,׾i۶*=~ذّ c'L+2 .x#۷(R`gcF0;nʌ<3LX9|ۿO/xǏ~J*Ԭ۴]}Vlo#9G,@Cue]veɮj*33^HvE]tE% REƘv>_%=EE{D&uϡF7,׾pS~kFWnn|SN8ۯۼ\zWɩ N9?A~z"R/9 ǎt萰J"j޼ٕ6h "?i $;vxףз[ k6lxaҤ(; wGuep3dH]P[* K Wb$^:;k~˖rOƌ޿]P`ŊvUZQWMKKܾ},x̘0ٴ?裏N$MKK Bm[M~[w=Dw?j{g5_*>{Ѣ'^}Gc$sN8ЎlP}߿ ?dr:iSa;RGґ|vUFez(cN>9a+֭^\0ٹG/W]%Lh@(\4ԵԻ?f!4Z7\zid{ii酷ܲi۶. iӬ>8N x^~Mը^h…c 9qūV]yݥ5Qz|={6ʊk.yf폏ĉ5-_a?}.p;>I٭7l_W/&ڴjUa+_UZZzO"7/)8H\0`F /֩Sd5kNʥkCЀ=#oú͛Ͻᆟ{oi; c/fƗ_>svH{|;#377NOCPds'^v{jRa~A $,^|OF}1СM_\~y.oxFI'E)*:'?g GchIKK{篫dXHP(ԯGvkIM+ߖ^ĸqrݺSW>ZҎ]jZ"$0Y T$Wzk/[6GKJ䓋~ًE픑#+l1eJw{vyNlyKƞi۶xi]rmqPnپk|MW}۱3b/Mc'LvMVkwxM?ۙgᇫ<`ڼy*27[ߊ sKKJK~kV2K׬oO?tS# v}U{ָB >u;v„XHO7i8O/+\reN138#+1S>|᝻w?},^\ Q8t* %H&{p^snڵ{1cl.(-۷{߿NrƵ׾8iҾŹV8]7xxժKKKϟq7; thʗ{/[\o~5N9劻o!xc|4sfּnݪ[vu ݻM W<'?ڕW<ʮ Xz/|u2zOfϮV1Inz\0#L1#M۶#_qŦm"x̘>]^Cn}ww_rm$;|%9;ו^Lu鯷V y^s#,Y:vi7<@s> ;wX1C*Y?miwz{']y8cl ໿UgJKK#ʆӮ>⫯n/)-͟;g3o{F/_@I5@{w ;Kw&ԑrxEU<hʦUՍm&{^ %-gn]v8fȐcܧONڶj^;oٲt͚˖˖U}w>k x}C٥KӬ;Xs }K׬)xo~|1'ܲyWg80¶l_믇Bz9`>}zvn>Ep8{M۶-Z/x룏V91* !LvӦ?1_q wpƌghռIGyA{=䐖M{nپ}ƍWdg_|njZb~O?438={vܹcv͛5 `GaM/_>ʂH+3_㎋o­/t#F ߿O׮۵n4 V[pŊً}6~=-^,^ nߵ랧}C٥K6mff/)QXa˖_}`Ŋ \0ztt7^zk} /)yמ|ݻ}ĈaЦMJm޼l˖}`ŋKKK`g{lڴ)rs'>7q6lP;wnd޽;wnܶmKVr9?|͗sNTUw0`'#7/)ǎ֩SΝ23n߾`Ŋd{.[\㘛nWs͚6]jU?j0YJHBz'{046=_SRZYϚUg?9sʕvƵ7FYV_nuĀ?ܦŷ<-]}{.>q. =rM;6nV2Ժ̌o͟;7a$_~O[aCe;Z~Qzho=Q6ڽ{-Gc?juSOÖsϝ?wl۶jB_nkJ_]w+#|דּ.?dW8ڴ裹?|?ѣG\堷pŊu0zm3PXr ,R$///M)&3#n۷l,+zt2g-UgӞ}v@^e-xcvӦQYreiiieRjF~cհ;N}Gn}ֵRXZZZ몮ul׮ B?3泱c->䣎Wӛ47`PΛKpB-ٺe8YgK8L6B'~$t'a=z|' ^[>dCMKK B/2ӣXpJd)e\($RLP!AHDd/9iϟ{nCuqqgeɹ_M?wXiߺKsXUܩ]ۯro=8so/ESTa 7:͓'衫/ *saO=տgϸ;9Xp /\>~#7<_K.詧bMoSi6N23q IDAT{Ys^|[om]+y\7}{Mm3dvѫRXRsoYViح43όC;v{_^tQMuq??HonOfjեC{;%kǎy5Ə7 oΰ~^7kVفn-ܳv*Zŋ}=40^s `VU<8 znlMP%r3f$Xzuܹ[5oOLJ=mݱcG_xѩh_~`Ŋ;+3E~ݻ޿>wAɀ?cϙpŊ;vҶa=zx񞢢&p]4#Fqc$ݴm۴yf|EZl͚=EE$;_ ;ztǿ:vKVٳ-ZzuQqqe{vn߾ϞC1`qCڱcVׂ Xzy>_`ޒ%W^~}Iii;7:G~#BPvy̐!5jȐ()mqNtiQqqFzz]G|QGv1q;w~ЭS8ظu(;뺌}4sfwp>u]  Z& @3 PW,@Z5 @L[,QXn悝;w[ZZޤIvӦ[خ]eb/))*.nդA-.Ȉ;ஶܹqv)**))HOoݡMNuo6m۶q۶vA";]NNǶmk7*)-ݸu悂{Ҳ23[5oޱm[}߲}ᬌV-ZoݺC6q|=EE6o޺}{=KJp-i۶mVgkWQq͛l߾knٲc۶[Ld%iii߲e{BfYY7oӪUۜͲ `OQQdPH,a{9 IH xDa*%A,^R0YN  Z" EyN8 E?FbxPN"ehpLp*d˫:hp\$KLpCE +/]pC^,@ 'HYj:L֛T0-2DAr#e]d/ѥ'^gT ŋ' l{'BPYLU Dz"@ $ U<(WѡP$^Sl9hl\;@U165A#eMO,HHaҦU)bhTMF.15L֛($˳͐zP!xPsdkwd/[xq-@u ?Ov @ $ :Rvu]YnlY+pj^b}WN!g*7Em@+(7ʋ^ FMss@)C,6xU<HyubQ1 @&I ?#ORG-xHAi^Fd/R0Y7' %ȓTRncfU<H*#ek_hL]d#9 HA~dHdkq d/@3 @u&$y$J,6x=H "Rd/@b8&$FC,@' *@#a*#e\^d&VIOH^ݹ- ȓ&b Y)zl 1&{R^j"0ٴJ֥ܟ*s[@ʐ' B.@=AGʖc kx|39 - #C#5B!B (AT8/Z^;TK]XHy.RR~(ZR)'I^Hy.ZMR\0yL0{pɓ{ġvd҂ db\4d!H=VW@1 @j-L+Q' -]5U:Pw}n eSXа#C#5B!sJD4r. 1We4@p ?1 !D.kI,?G,@|\f d`B!I &y& P*cd%ĨpU,& P1u( & PP(=I6("Ԗ:VW#C#k+$zjY8DQ @9dPD/ @,@RȖa+ EOMd1$rtrPA,\z ]UC8$_$ $P("G฻ LhBP$D"jb8 =$G 4مX(2J8]@DI @F!@moc#*=$TA1u0 5Nv  $A vEﮖ!;B,(Hȁ). EOMd1Ԣ(;$a@ʪ2FV,@~keȍD,Jz cdY ^dPB$Y$2\0^z%=Ԛybd r(74NdP; A I ɖ& 81 ZGpddh ғ]@]# \a]dP;, @!@BD.rOpx.H AQ 'eHmd sɓ 6%,Ia/ %ڤu$=PEƶFF&@d ! 'eeHIdGd`kIS˓ $1Ih@\3 RRz Q{7+xquYIQ(Jv APP($- J,4ÏA saz^!=$S6-_AqQ{w*\aՆE([ϼ5Q'r LA'+O,Ш5okΏCiIgWyƇO YuP ;8W-) p=':uT>Vo¤xA]K\0Y 5ۡ­& -a:CnѬ%-h\T ?ԣ#ۋ}4dKO}m hTTO#WA+dкVm[ռ[wYf[7nݹmgўp8ܬy9w9g.k~AaՆ WnXa{v)-- BYͲZhӡ!ҹgvڅBD0YW/1$+7ނ X>IfMŴ/6\!uu֨#ql*ɟLddzwnY[53a|a'kש]iݏoM].R)aճfGl{'=wޛW֏˸q77BlX4k3q/?=/Ŵރ{3CZUh^pk[i'OJ e?< 5jܥ &=?'2I6#+~X.&ҒȬՌ/?ZغVٵ}׍g߸rʚw5Y Vdol;j+c a0.][qUcҦvO4umi,+; ޝ59/lY=3[o6-krZOD_wu'_pr:u+=tCwioaϡPhQ?z٥g歚m ` f,䷞ydI??'}>ίKK*ԭ G# ӮsP(T־hUW.Xh֢ykή!Gpq& k6?UVg^ڧ>q ҥ)].ܶe򻉕.f=߳&7jU]MHo~p7?@4<󹟾Ghɼ% )fwVv;ۍݸ$dvlƄ/vڨv:Sbgv:ew^vIGeOt#͍Z=yv$hjQA]U 9Wsc7fdfPPcڵ;݀,x}VtMmq{o`L`"7~3b؈8vϕ};Wk٦e4mpɭ<9ɨy^nޚk5oT^y%]p<:[_n8bf5{hC5jE@)j% p`s_6_~UK-5y+հYۇ~i$|obMvN2@ z WJܥ>WKr-K/g7~DzNE(He^??_37]ہͬRMר^TP5][sF?1`KAUV7>zc0L<ngGͫר[5;YJR@((:sSNjBkVoRCJKJǽ0.Oޜx]۞0zB0 }mMI$HPT r7}1߸~c223bE?aG>v޾k;&lԘFozvOzHeg<>C8d墕)  { i:;DG_|}<iJuչoWpi`Ku֬X?,fQa~aq/{w.+_Y;3+ǗS0ef7\|O$ݕ>xmJ"@J(j5jOܥ_̟2n#_bMjiIGFN7{vv}TZR녱:Vn):r{†5 e+-)͘Ա{&-$?\(`0x߯9Og.Z۟ޛ֯u'>&@ P0fɌQe.KFOxqITJ'Ν>w cE7^9[Ȳˬ٤EשWFj`0&y@ _&|d%Ca IDAT;;w#nPIs`Bd*&=#}S<ʨy8~n~P\T|w~qW֯۽O{ݺs&-嫩_LlZzBŅ YNW./_%92YDtIܟsYTƷ_zWދp :3ӫWNۧ>q竗WW]qϊTTP4𬁋.Nr$HehӹM¯kӆMOTk@+'܎4m4|7eO`0x?n;p ?SS@J(HD ^sƽ0nú Qì}]ҪUZ'A8LSZRT-_GmqW׬Xsé7l\1ɩ $"ĝl)(c#?yu׭Xvonڰ)_&9IyԨY>.d`INIL kV;ޖ-y[mӉ*-V㨸VSux& ~w_twiIiS@2)H7~wްYmYxe$kXb[]sF\2aʩ{Zuj]͏]3,g d*,L|mbܥC]yŝoٴrbUDw:S켴m+Y:aoKwu/b#@(OtќEM`?9ifΨ^ۺjU?YqW'<'_t; ҷ6t g}4kgNY\TY+BI** >_;Įp |vi H`$Iu ~,UIvGK+N[Opyw> iڲiMfN5'_tr瞝yK9혎;ܼk{\o~,Vfέ[np U l)]|EEQO4iC;^c{@k{\;wܸ-۴>ux>Y9 2t vC\rE$oLVQUOKU=Cyr6AO]ǟy=/߳녱eHdǿݬ53S&Oy?^cg}4kGN+d|퇯8-]]2ǒgJu*Ozy˭ڶ*uk?9C:u2==hظauЭÑY˿ӯT2zzR+A߸~կ~uEw&9T:eɿyg {7?|9? sK0<:~ o~{f2v-vYS.㲑sG|ɡP%5URNMZ4yǶwo~/:N^:B$Oqv>i@vmim_?ʃL;9\Nu+,`qgP]ۧ:' :PD"TGrFDM8ܼLOJ"HV:{IoL$ ID5)^2oI{,emK0jx7תS+ '$%$O4ʃ\xSMIuM`z#?%%쁔Zr5/|vm' 9v}xɅIJ^?o}8]{w_rI zzPsըY#%/'\fŚ$gxitPM=!jry'߉Zߔ?itOR8ؕ_zi7^:>n%VZj꼫+R0eܔ<ٓg>69wj}TTdJKJ?|èa {=;שWgQ9rv]l/&~1oƼDk_Zw9{mNKU?ȷvf,`U6l uivPÏ>I]jތy7zC@ С[\vO{5kW {|sw=yQ[W7쨓JIB`ㇿھܵN7߾k@ vy'Q[=5S~fJDfL1ёOq B97qّn!)㦌~|fm^~凴?Iv= nknQ: |j` {P<>c'?xՃZRθw' T eTm9r&=..[&[_8)'_treegX8޴iM;sn|ƌ̌Z 0tތye?VR\2I;-?ZZ}T1?| $6CڰYÔ"t߿w)=#=aB`ݏ8;9{m gtX{P@ 'wr԰W^[߱[zn0zBeeK;\&m>n\1d7"7yd Ag*RP3fZͻ錛rF͏=g&=Ivfq5j({'z5ߜ??9%=2YLvxƇw9|뇡j}{n޴;ݙ9'_y}wL6xhjQL`_%n_}բu!cľRZZ#>uOyS c:$005#:ݱoߟW5sHf7OC.eԫu97?3lΘ0#lgLܥMߟԚY57mK/:8fN 9V.Z-OE 3231v)ĕ? WٶI6Ԯ[zͫ7?ysfJ@rD"a+/_-ZHE",h|7F CåDdrFDMz cկW찴|#dޒqg'7`KNՈsxbWowcu3~w7fegmocZ}_*w>Ϣv<}Sء/=  f.xwR(Jڒw>0Nil hݹuӖMcFOHܼx9v~u_3wxBf g[㿱;j;i/ l/ϻ %_; :$b[S h}8 _C5Ѭuo\BW7]2yHFfƐ1CIoL[|ddiIi0=#ޕ^ޟ]=z7Ϝ43$Ǥ7&-(jر{N'vJI@ a݆3/hVZZkfUqF7j~p[6i$څH}>_/\bMQAQkնUr`K,`׮Z. BZujkTpUW"o/`]ypi8TQVZ 4mдEݿUVYTg_NrW W-]a݆¢P(TnlӲ1tnGڻqSYu%''Q& @4aI {=^ep?{mD;vN:Vs@Zz\XlwnjڲiQƟ詝 IDATI,,xpDz_y&};UVm8N:1mӫ'v{Z[`cr'6q=ÛߞR+ ޛ6qޟknBBu<3{푞rʗ uҌ^Npxxw>Yolڲi.m۟G7;dؼqǽۋn^o)rw~;sc9]r%Qe@&c(͛޴a~=rp˾]5S2ٹ\?]CJw?ݼqscs͙~.Ɍlc> ݻyg̘8^lwL?}>Q-N~׬XS/ek\Nvn. _|ųw<{߮< [j]ܪrTS{#y/-4?.%?~0@ pw8tSc^ż_|Ϲ=1}{_̬vy_}%?~']{wݥW{UUc?.).vrHC82^?Saf=;۟$31v;=J?-.,~g.j{фh>8YYF:`޺&٭5^oO¡^oO?47qSϹ=-[l5FIt؍Ivpix/x⹋`?􎝿^`L'gTNԤg;ܵg&6W1^=H$2cy`b?;NBlYDEAG+jb]u[gJZ{[\l!#2a$wy=N:'9x\a~_ ;yHmE_3k.8z:\ZjjپuOc`nLswF".ܴ~ӘcT(nܧ҄={jqyo; `58U}~{ݭʅHr L͛VL~uEQy y9xGoEה+׮^ 1xmE]zvi׵]N_8Y~6_foP?oٴeʵ˿[^NII)).RE~g;`/ٶyۥ^:onHKOҳKm4jPe?.z;o^xEG_4q;uɫzEO(j߭}mrKQc㺍?VT3a6ox5I۴uP( ^P&u<ҴU8O9,-)O~_P`L$3If6Y;!esY݊]rˊwPkJ/V 1&=96BaK\RNlN#6 E]D"kWܙgN4}yH:ddbTgq;G(r-e5ɶٻYם5!?mEς bWYWl\f̊pc:й"w.ӷ?ȝ?(I655uF3Oƍbo().Y2w)g|< _] 3^luTsn8>i?pϿ}?}+Sqƿ/=X/*8|Qe)))ߙLe$򢒡ڡ }Obן?,!0oݡuud caA}niI ްr-r/RRRBP-Zm1p 7L~gė'nZj#ef}/?rT|ؾ0sÚ ~ɷOς [iN9^/èK$#3#R:Q7,fW?xCWWd珨m)))_2_/uϐ>W>pez]#D~?{1o9JKJt寋Ӯ|_q}oޗBh[Q _Nr;<ȊQ)/r^GQL֮ZՇ_EG ]{_΍??"e ̛jZjDX|u0 #G?ws3>xgwzweG5iߞtoO-񬠕߯{vN223ο.;mF&w8Db񌊔ɮ^ ts9/OMK=3׍;+roٿ.ǨSo-EŁWukG# uս{zގ;*ꙻ0-=9#_;!9s.;pit<2}٧^Fϧoz`8 ~Id˓:ppv-+ӇƆf.Z2w.)5nT@̙:'6l׵]VNV\_m^:x:lTA-3mUk]vON{rwnY(:ϺY\[FSdqaz .-wͿ9뺳*$P(TUKV=q]&ٟ̫όͿ勖Wd*Q/4vδ9@Yk{ *?cɝNEۊλnٴ%0rE1HE3ņ]s}wli{^'afإ=yn}/󥭅[?}ؼYfW>xe*SG_3> 铦Ի[ծ7?}Ӂ\xJbxNJ0Qd5_ O?|ѥg_΍ ]vK=TTY͊ mޖ4mX!p/D"(>#%%%5-/p@b,$6l߽}\]nzM}.dSRRf7q="5n"KrĆ%W.^ՠ WB:!6/Q<)U0%s]r3C$yޏjP(n]{v 7o2\h"6\rMvKdH-ܸ1H2e[6mD0ck bvԚiuܷs+qZҠQذ')))'MпR숓YuMYV"תY4sQؼUحe˪[E4n86ZxGqNv?=THދyQI>|IL W.^9˹)#ipIwܷrQٹ x+-)-V7jCՁ#&0 9KRRp쩳\~Ҿ[k LS=i䙁6× _v \cv/-6ύӡnF,a㣒׌>7UmM7}ǥ%S&ۢm|URzUm*(kI\-hG`C\tA;4RSR&xذe9͂"BPM0-p8\9i̅an;$lΐݤ*U/^`.]v ;b) ,Yddf >6nڸq&39*Kֆ]ږ5^ƈD"UXծk|%}훰12ۄB|]whjذ'׮k2٢mEVohֺY57ҒI 2Jpin;M7Ua(s4ݏ2Y@ yQ'0aurڐ2WlAG]SfWm{ 1"H B;t]0-=eۖ;).*.,([iQ͝i_Wd mn1V- (ݴ~ xOٺyk ҒoZzZNv?o^uG$-zjn;pzg,Iv}:>_}UvUJJJ};5j(675\PW~g߫[FfF`^(~nڰ)6lܴqjC7n̳rsvnv`eӖjouSRRr&l~kj8}4߫Uke =vcu.o}۶-۪6O>p_&|<_Dz{SR\R1m[,_<*\rMN̈ c2|RRR*Reٱ}G`^Ajo3'LFZZZ`լu<^kp^:6lٮevKdNjɪ~4j0`زIL+cF>zӣZ3 }݋-7jsN|c/Z7Ae]{!^?,a\j]mg|4#*\xeNlܤqli'fܬ;n!+'ĺ#Iv`a0a`~IGm :wm綉H^='_U~iCbM7M˛V֒Qixދ-X[)xꎧFvslFfƭڸi@7GEuYy4SkeӖKkV U_$$nYjcW{wlXw'f{5 #H` *^z3!U;4߫y5w湩i? !-=-eݪu xvگS؞$p8b^T85xިqռvxxqo̻ޫ{k fOG_FyՃWuy+eg_[^D"쫏]{vlnպYϚ9yf8nnٮ捛6qw߉ Xо[[״v㺍QaS/ ءAV_Uw?Df. {f!%v:k7bnuK$@J7f fNfŚf9#223:>ܱ}GYF? ˺c_{ا|jڄiM^z󆧤T˺pk y+>s3_ulEw^4`H$Lnگ~P(*\.Y *rq:nڻޱڕk TyH$𛅱y.A_6oռα?X/ ye =H0@R& @ݕB^T2W)COnٴ(kI(:۱j'xΉ:-zm[TmWyugm+. lotb=M?z=!RÂ=*l6eTҳ|+6g|4#a3T٬fE"UV;y$L:tK! Ӹi|Y$ u9Ke}?=ԴԔpixkZ>y" mlڄiac7E,uW}afثJ~cbOdۖm,lަc;+xP}uqjlRiciOyґmMzW熂] ?gp8N'580OKuh]Ӿ[v'2džeD">nl^A R뚽߻~b?3Cm=6?O UD&D(Gzmf}6>OYÐӆmԏ??:6o .ǡQF>sF'NjU7 홯)u'w<9T7EK_nR7(*-)a]0#5%%/ѷGŷ*ė'0ꆨ0Yk߯7o|rǓ7o{=PYCY3B=wW|ݣ<I̓)4Q3M9 є}8~WAؼxG']9o8[4k N u]{'d SM ug^FP(3yy$~s䝧މ_&~ )vn;pgߋӉڷ:/mZ颣.{!:(9ssc /NxG+'ycY IDATԴKﻴ#&W{^O3&i~7KKJwyizĵFy>S{euYם>{iެ\C>$Ҷ-n<ƫG\xJ홿,_ }kbƸm{?ԝOwZ%%nw9^=3ԭLz:{38點޶y[ gN?cDԿ驛RS~Ph[]O`J~RZR:oh[GZΩS=Gzm.ȓOw/Z~xz wwy+_x'o~ɛ6!9tСmn ëռgN˛˪/b/;Sޟ+}6o|K ':[:oi=<2qu;MOt7^ݶy? =}{٨q۶oL3&L۴~Sf8xckxuٗwY}:=chkz޶pw3mΌgL8E|_Qa=߯Od|g7>:WrqbM{AZ{:uԔ64i$AfII­׭Zj-aBu^,vzh۹mme7ھu{eҾ[{^^F@iz-}ݑH$‚WyG^MIIiݱusm/*PzWWs\Weݰto}[OKOkץ]65n -Xbͺu՜aڀK4|"$=:zɩϏ}~p]}Gӡmn_7Y*‚_͏+#3ooS67ЊW~EEvr@mҲI X;N䦭:֭[˿3i~  ]1f9nW%K/]:iϰKx}a=L0!B.; _{O$~6k_;.̈)u\׎B_7q-ڶjپBй7`ރmnS[3clT<]V+eɡavÛ9n[2wIM~ 缼Q0iw)-=.~':*,_۞avٻƎ}olUjjjzedUva}_ު}jΐ1`K/Ϳ#HT~s[V`^PQ}ȹ7mn޸/GZwh[kyW^xDž^O4}˦-\,={իq:97J-6baMna'#?2Yo:S>z MMKz`׃}Өux阰1[ݬV|bnܺykIqIjjjFfFvnvVM[uhվkNwjվ. dN4[`ٚk KvKkԸQ6-umO}=tzp낯,dՒUn(ܾu{8NKOkհq4߫^wr@ k|H$z,a?.q [6mQ#dh yO3ޝ딑Q^.[q7_wOn]+Sf<`O=ښiwz`רWw)XSP+k˦-c;1Mwx45B,@ić?lKo?Ȯ#6&~0lۼȮ#|$ڥg?|y2 ~C v5 {~~;X!_۶e-7>1xY2dk`o+:{vWuTy$F2;$'zؽ"HW8P& O,{evd Egd5eQ& ^K,{eP& ^N,{eP& >A,aL}>YBM,}2Y-d)esFd t' @(`ߥOQ& >M,a "GU O6VyD/S& dB@,@&z9x8JNtg:'g73θ+u='g(\{}g,+%%/MW^pCW[H..)ٻ}2Yj=h漼ʽW_}?Б[w1hO^f/^5+FNr֬ =/8M}rW]5uΜxm$\j{~„kGH:C<0^6⋷O_lY}GznT/\xŐ!>gw hi8uΜٳ/yՠn؃IIII[.¢fZ7ME"S:{@^+׮h4CQPX%//毭 RIIIIw>ز05%eCu:ğ}=5%dd ?cM> zu]=rؘ[ǎ/}vD/@҂KG߿?aBՁ2YDڔo.3.?` $\jا bu2o~3Ą3,onѢ*h4V[aӦmIIIiiխ۬If''''zGHF֭[6m*,*JINޯnݖM7-Ytim4iР<-[j,|ssIIM23[hQ7##w/d)SoN8!F=C5ifML>woݻ*իߜ6:kWihtɊΚ,b[:\ficֱaZU7Mn꘰yVVO訲,Zl91\P# ~ܼwy}s߸1xNF u983soH|}3wN"ȵ]s_6샹swֱʨ1oހ>lGҾM=㪸m4hI&͜૯KJg~ղ>}{Ս0E۶M93yUD8g|3&@6-[eG*⒒i~ʔ)Oʕ|+`Ƽy3ͻ'>pxE*eZuĄii߽v?gdg=c9:\wqϞ]O. .'+zQ.]Z6mZѷ[aЧzb]'%%mGv6mذr7R[С;'u32v.7yr;,,*ڑD'N>w{Ow _qEFoL'hѮO.YbɊx60.9H$RK;e …m>Cv2o-,LOKv80$83sˆc}k+ ~,3ịz|;{wRW.qíOWeuLlRRͽzU}ٳzeҥ *JRRiG]2h4䫯ޔ[ 'N۟ܫs^JUn_O>n+i3v߮^}~{Nߓ+7 V;Flռy+RΚ5WsOϮ}7nF HNÚiix`Շo&YRRRrϞתYGiռyG}|yd+,*|WL):Ww6fLѶmU^Ik㎫deka {+$-[~?hЀF ulo^Fv .}lܸ|w֬?\PXX}Ιs䥗VIv~xTϞ,ė2Y-SSR>FٻL#^81&LNN~qc;lݻkh\o-%9ps֬)#mYooĄ7D8Dۺܾ}0ss Lv[qq;يcDѤ7]4njݺקN-2oMc_ Zv^U@\&z}gKV͛exڵgfᇘ|eq1c|`>OV}~$i{~xۃnբE&M2KMI),*Znݢe&͜;m|wSnw碖M^x&OɇݭcǪ-W vN4٩SUf@%%nu'E".'ӏlݺIff4]nݬŋMYN>zmV3FuLg`05%^6m9uN΢ekj`vuv +ٳ/>l2c޼ ,(, >EƗurGj<#=}K^ޗ+VϽV7^t-{.=-`NQѵVשʕC|Gr?зoG{Ӱa1?^0oQ#䬳FL۶|~Ai5k$[#5>}W-[{I^O?}SOļpҿ>W\QM$ƚ~KJJjڨQhڰa0\jUv[4ɻsoؔ䞝;һwL).Ԭ>}>;7Q~A>Юݱ…1#/0ʹLq'\v$=-.ܴ}̀^Ux`ر1gСo3g>0jT0뮻jǷmѨQ8i̘Gk6lxl?_~y\7eO_w|ruw]}oߏ_3vҤף_o9$%%]t=:uz7v>?CIIIZaCp7}#(Ōӡ]  (,*y3޽cIN5kJ_hXڨk[_bɊ,=}Q''WZ;k2Mۥ=~>DMToܮE矏 {w83H[`~)<=x.d҃جY?^~9=/AW]Sڷ9O=c/8$чs~ƍ?ÿ)|&:x⣷ܼg_`Q& eWn_N0ZXukXaCn z>pʹJO>[5o^w}AGn̗gƍcmŏWf̟?kѢϞEu߬\f5h0jȐ(׫]}7+W\ +R ٺ?yfؤ ::ݠ~c[v};F w]sMʶ,Wwz!'N,veqRx]QVR˸rowM60bDfzqr,q͆ P#5n݂/_PP}{Im8s6>3/}4Ǿg5h/G=;k9))驻׬Q#&V\FwqȱcCL<+޽f$''`ٹ@|)Hܭ[K̓,1ܮ]Aۊ/.kת棏r~EUߨQ0ܔ[!\xa5c7>?aB,_)Sb›{_{m-1a˺tМԔN'?;QmVyRc ))){m{yC,]N>9n+.`AGdh۶RR{+¢*NF} {}Ԙ<%9yoӦ.#==VhH{V:|h4Z9#^|͏GrG]e>~`حcDzw /j~:w\yeRK1Z5k>зo#H߯_{u\9III͚4){@(^*XQ%UoӉ'Vqx\VZ}w .'地9/W_ o3IKVs>h 7ZӨ>xխ[d_4nM ۷iSj-*#+1 P& et2SSSR2vo:bD0+ڵ*_t9:Gs¨^۸eu?8,G"LRUbZRe99Fq~Wz˽h}DO8 ,# @d#fRmuEAaay2.wgͺ=>}*=:߳g0|X< 1Mݻըϛ [deWn%eZӨ>4e99߯_|pHD/Wvy~AA(kT \t7=c4(TnlEEќ5k>ꫯիoܸ97[}|yxx⯿aĘ1rˮ}cڴ}s~Ek7y_~ 6jՕػ8o53Ìeh2(NGHr8$QIE-:-B%DT$cv]5c|?/9*++|݆ eQ?@`ڵ FJrrm*[F`u˰mo:tsDv&֨Qc;>yguݺʾ`\p?67yW^YNb}&uVj? ځwpaO6E dʨ#GV-n BUV @aqJKM:ڼذeKQJJ|\mN6ȣ_w]u׏i>w߭&]r[ybvѢi|SxQUoڔ[m߱ naS5=UEDR& EUQWd_ƍ˰gdIԣwdB}C/iJejպOc_PP[L69[͛Wp8Іz.N&jxeeQ+11=Z/VQWn]fʰjeun6ѪCVFOWwƎݾsgOLLLo-rH֭;nݤA su~ aWOӨϯ]SÆXb9UUkVE)@l$:m˖#c^]!W]9?E2lS;K?>'&M kuԦqGYz?;!{e1ӡu*lx YGΏ(Ʃ?xK.ʊRR;8 D*#ztvUq*CUCο/zaƍ˳Ȍ7zFBB'_{.@ ps-Z"l?yԨ+;/VM\{ᅑ×N-+BQLJ=3hT#ԩ9_fMՇH͚4:_vm'(9tΜ/.]ʿ?酻.'oy^|1r>r*"|9s †9Oߝ7-]ZjҧOU䣒# -0)3# Q& Kw?sf9fmՂ]Wz7P[|RoN%;;lX')˶b%?r>vĝ99=jAg]nJGx衑U@@ 0s*N@%Q& KzjpKW*ӧ ԫwT _rIԣ#F|1o^~בnGY') *޽j ]>KV| PuIqqUwuތU85-"Ϝ#e:m9vʔ}歷"XFyֆvљgF=ڑs5,\;Xەq֙9\Qvb`_wu~qG_ˢVd~lWmOz  7:tq|7Fsʞ W_ w>|a0<ۋ:MU붿R /r}ʸAzungsgΟߥ}ʸJn@ϞBg=裃`GjI+֬);/fbb.6r̘տVxnÆG&Lq饥 F9?۴lYE0NY FcϞ~:%9<,XFC QHoޱvVrW^*AzBla͕tݕ}F?gk$KґGkՂag͚G}O>9&"/+恑)9Z^3f C1W;+xɾÇ/ `07LL<} 7m*99oLr,]9૯\rɜ,P(4ac.ӧFlZaAvDF[F%֨_?^ݻ7i }>{嗿;srΘ7?3{衍[;e7pӦUtdJ$!oY -)fs}w"97ҾڲMJݚy_f+,]}Z~ݺS~.+y7Vbbta͛7mذNRR0ܶ}7dyK[8'7_.?.3qb-٣Ə5~|E2g#l_(...H|rQϿoK۶jر=]aCQx֬Ȓhl`o,4ǖ^|*K@9+P-ԯ['ֱcQ,_￟hQMWwsw.ѡҨGQ|g0|<ަe/{\PfͿP~%/?l ?y'Լi2@5bKCtUWyNmڢE1-_WDZ&[_ڱPuR?y;u{[<3zF))V W+}NRRjrrEmTqk7kz1 O4w`0XIJ䩑#O?بl1tb6}s'Lսn,>d)Sn4VbZ}:[WMðIn׮<;+PJݺP|uW+C|\-2e:찲-IQΝ}?M<+6!C<{K/sfii{~G~n;VlBJ( b}WXAFOٱJ+PyoN>=kbl~',˂am֯_U۶olܢNlҤmV%ٳb͚7?䳹s,]vm۷תYQJJFGvة]sqqEƴi?p…kׯOLHhuڴ9sӺtiR~lիWn^^?ʼ>5+'7w׷ߘ6_/|zǿWY=G(d{3f|w ._#>ջ$ǧ5h,-yӦ7kѼ=8ĪロxUśܢi6-[qaծ]5so1f̛y۶-ZJ1i|3''lءu Ve5Æ=v-sL\>[yy_̛7}?O?\67]HK;YvC=∖_5_~-[y䣏N :@d%eO ,]:wѢ~ wu6lxȁvn۶b;(Gz OZ/yrV٭cӅ'^xC]0{7nܲeΝyqq5jWvuۯl߹s݆ mۙ k%&oF))+^(͛6mޱ# !>~/2A,{?HUJ.>.CZu͛z5ĪI6\6lrL 6NMkub'əxqat*N,O>y۶“*O :cƎ“U*2Y()~:W†,--&y@n^O=Ux*=^bʠLJi 6LU!Cb'lξ;][nHTX`O c- :T99g;i[|yzW7n\@n<}[|1@Q& @q4Ǝti ߶}_jU^~~ԇO:k ڀg ku[ U*2YI  CPS@̘?.ɓ͛rqqq$ZYݺ:B,PjӲc4W/j&&f4o~֭W5 b? >Y>p3^n`l3P5PRٳc!ةS>Y۶eРwZ\ T e@' @uѹmۄ@  k׬ܴa͛sc}2YJZwXGpqs`#T?d=;rON,@)>YrR& T}L:dL,P2Y(e@ (-e@l(e@(9e@,(!e@( e@-eA,@{ }P& A%!\S X P(TUT\{deՀ2Yj !MhZԩVTwq)TdeՀ2Yj@,@5LP& P (TdeՀ2Yj@,@5LP& P (TdeՀ2Yj@,@5LP& P (TdX=mc_<Z믯^& X` b/eudFƑN{-}Đ&Y(d=/@ 2Yj@,@5LP& P $:PR[[tʕ͛YPP k֨Q'))%9QJJVFk.Yj5mذq˖;BP\\\RZ o֦e˶Z%WFJ( _-XtժM[un֬K7kVIffe}9+3ׯߑ85vi>qJ)d`O>;S>tΏ?䕴#22:m{wi߾Aze=/? 嗟Ιo+uN9昋8nJ~װuݺa]]EϿ;w(X7M{ۿQG I{%eZyӣNxP(T3ޟ93w}SO-ՆܼjL>=kR%;?~rS#Gݮ] _LMN~Æ_|EgQ<| Sx_TiA(Gu?|]߾/MZ&Hw,+g]sÆ=;eJid dɟ//=yo~I3+aN>yF*jUW^bQ]4r̘d0sc.xUe}Ν[omXA`ߤLDϿv߼m[ɩ=y->+$e>5+l8+dyիWo|1o_<ɒRuVضO:tԩ}`^r9eqd`ܹyg(*F))7?젃27Ooܸfbb 57nӲeVl$!>/^շop -+cBFjؤ㡇vi߾c^:@ {ol)Æ%+W裥}od6iҶUQ?Ͽ]fݺ^ X3 (y.9'йmۺIIBІ-[^ -_/\i֊JU31{ǎ:vܶmV54/?ҥoL/Gm5]zɓ<^tB͛/Z"l>fotRX}ma GddqU~}os#ywJR& <6w6 5n; OԫwJb[~senRy7[VVؼUz'Թmۆ~>$OJ /(xo2.X'LLȑO?=fbbuxW6v㯾Z‹Hd`|B {|J'>.nWu-Yݺ2hڕw}"pg-\}5kvEg1Š_fg% S<{Q:gNI<:cF#6m磯.al(}x[̻ɣѵ뀛oޒ]0wIʖ;ca5R5ɖߋw=yԨR5R')//l_={FǼj(*m]ޛ1c/]ٷoٶ!5n\3rOQѵ绥KKx3Ϟ99hPM8>}F))%(L, IDATAVY6<]*Nr@ZZ=^ѕkזpUj^,[ߖ-c/6}6iRm{;ﬓT3'}tOoܲe֭%<9lvӥ0g 8"#'H4$jgOR)M6ߴҾ}|ĉeȳhŊg ߿ L^{nӢmذw-^Ƌ/]n_/W L(Q{c-U<֯9ZU}F_裵ח6㯾6iwA;v,=ӭ_?p` >CK{#kּ3Kx{aua>,ӾLB E.YtqElXP_Ut+ZbYž b[A.H  !㺙!1q|"N=Hƍ͹sO8rKKM J?>}7=3aBl[23ݷ]VՋ`$?ujtޯg/Te٧vZE/M|?,[J5~BZJGO_}{8i_'uk:J?aյCgnL[q3k֌[`eWG}ɉ.u<{뭧]{m wok=k4teʷZ ?￟dժoٚW}nV)?X_lYVv1c%'}tf*[Rl'm6MNJ{,=N޽/H$ReW;zGwn?oݴ)5/?)Sƾ3w+b:#4kdbo5ה+'}IpAٯXjUtشa7,@ C x]ee۝r^oͭ&sso}w͓>$d@R(tGϿVm%?qŶu>VH$6;;:WN@uf,Nkq KԿnw7wĨQ990~SUnzjjJJpxښo 82bCAaa~AAt^3--e:KNt`6lʝw;vo(Fל[=N<T 9_/ZF4mڸ~uLKKNJ'MZ~}Y_SuwK'7SOOwݸyZ5kIR),*lLtm3g1eioXsr.OnJ JƂ]7OclxqףǡO1LZ&pd,\8c޼8 ~$5fLp)ԪY*Y`7L)5j=ȾGM7}oL2~UOY\SE"#&MwU=Li+8c9 d?={nVVwW.-55f977M'8KNJݽnX;?lܸGz}])8yѧwcǞ٧O7`0X`ǾQcKydVvKj 9)vFFt_3d`o 8zoO}3 |_y43)혻άYu+㏯Y,/(x?Y3qڴbAv_پFKV,^(:C{q9䐘w윜]zaҤ32椝o O>9:'v͛=ۯk׼ytnÆU֕o`$X#,S6<}D}.'}Nh>Яbyftd/}„bw.80MU?3"K'WTG> UJR(Ĉ驩G//¢ g>)W1[aE珏ˏ7niʹ!Uup@͏?.Ǵg&N/+#diPn[rsK~pڵEE1O4%+WH$2j̘btRZQJխ[0'LZl{.Y#+T/@T;33:LOM-M[7oN^o0<5~| og/XPXg)uq/X9s^qE^~~Uj2Yj" Fm5+1/-S͹6lpDKNJӣg'NVP0jbڹ] >C[g:ή^PX& {#H1eʢˣ]`ӆ c/Mm[)>7+_reѶR)%''% o?ˇT7~2ӣ¢Ӯnڵ%<[}?gذ7HRRjgd얢PuY& {}]s ss=d^V;{uָ~ݧv͚Ek#|uAa=G2x%Kb^Xq.R {lt>GouҪI{١nZ8̘GKWq}yQ8'=~e]ۯ`0XINt?,\88س~?<5%g/rr?L8w^y%:tغj:thrRRiaQ߿-_^3gP(T&%Ùgba8Twk7[o\ jݺ?sSNmd۰aoNxbls}׊Ueݶwyw2ksa:kըEEf5fӧǼpbϞ'YW_>p1co>3ϼz|pFj$'߸qESg|}ʔu6r{_xaĐ!iR^ݺ߶p'-%+"Wc7x֍7yfΛWiiygR(򧟊ԨQ -L\[rs'N6qڴ@ کMv͛7iРNffrrrm6lX| lHM`0X7kλg蘧 .J9kN-E#pٲ8bǖf?y[s_NY'K;nُWkM|ڵ[;S-Y& { ^LO5ok>af~3X?N;nudY͚i))X&;?=ּ]~YKi68iRYlިީS XvmiiP ˡ;[,S5|~GU7|[9nZڴxq%_ti9jgdw;;=vPRrRnSSRJݜW_ݱI6;22*"TQMӦ_NG̼+?{*=5$'%]v 'LSoڰa O-\vv;;>hPR`9}&v:;jנn_kFܧVJ. UKr پ?=5_~ɜ97iHN; O>NffE$BןK/8y򆜜lӬ9}^zыkׯ?g.ifE?zPǎHer`=:ӯ.7w{c eծyWk?u3fe-_&/?? կScVGtisX׬ب~U0${b#V} 2cƯ|Oë#+,]bŊkfgoڲ%7/o{8\#99#=Aݺ5вA;<& Tz}ݗs.Z|ƍEE5k6m`mڵcV=O_xӖ-ukM=t9k״G": &B)ھ=: 9*)*Jhp8 r_?~b#_?|РJ5?/~1zt.]*E>U%2b.v͛k<5Rjխ[n=l߾+퐐;[$'%큣b*~KDZ:PA:ؒpٲSq{dϾ& ׯߺiӄeSD'}t0؋$'}>5{-f;bȐ"V~q~J 8ۚWLlnVܬڡe ϿbyWn|cj%{ 99WݢEx6/?n>{v=W^ +8d(%V3:d࿽kKT89gԩѧpq^M JNtKWYϚ?8]^ݺؾ}f͚6lXvii@`k^-o%+Wؾen1~?؛Y& кiUdB+ $֒[&YYs7q@d,@EmYzụM[Ĺ PUY& e=aٲ/^z윭[ @zjjZgm޼}˖i)). @a,ʪuOg}2g]B[ީS.]:蠮:$Bq ū֭+6o7['T)@I"dzg?˓?4p8pٲ˖{@Nfʳ' /8?)!}*Lv~>꫊ڴeˌy*>j2Y趧wp@ `,D[}u}7٨^}KQ(g5o+(CC!d,YwÆ-Yrg6l8uԡ;Q$Y~ŋgΛ7}gT XС?YU&w\v}HA`0شaæ w7]tQ^~{{ wgq!C.;㌘GԉsLּzgdt5J?0=5c9coĉߕ41͚5m5;gҠnuOjݺba#ѥKe} FII/q9}{ruoH$Rv7nb;2zzY ֪e,TI@ :ɓ`0]w٧O_ +>j+x[rsw_̣[MPA@ojݺ%ĿDLnKn'FH -AjaZJJ@ロTw~{CNNt~N߾];tثFiioUOr @"E"'x#џ.0]v!gϾz…?,[6;{Ӗ-p8))Nff :lٽS#<0#==MRRJee,ڜ eeE=:~ſO¢S>Sg,ھ˩))SO=WP(> gY gsgZ(+d^Ø'&"ȸ>KV,#oOڴuTĉF}UfϞezK ]wܫWYH bEE&۸θ>|g۶E7<_/IIe}*ZZJJ)C,Z'aR(tڱƿ̯mɹ'NV/YrW^pIxcFzz)***ښW>e}ږ__¢¢gbMOMzB. nÆ-Ώڵ^eW~$&Mu%?egWF)(X{cc@Y%'m` IDAT$|3aŹɯ^СKWمԔ:thդIZjjmKVoyy} 3ukm?RRJee, bGu&/(8kvI~8dH޽k:ٺ?gcS-Z4hĈɏ< UWR(txnWHKMc@YY& @ݢE1:vs_c3͋ytǰa# INJ>qѩ{ ~^~9?⁗^ J~{.]F;;}/>b~-Z\z%ֹm۲>5R^^qX&{ѩ>䐝WWS^zuV2%-%%Fh,Td~Qz ֍@ 0{^y%oओJ~<5%nڰ=}z˓OѧOfJpPǎ%,ݒLU&םw^Ũ[3z챇sٝJsol @Y]f5~-ZĿ=P$G MfD7=hA)k,PVPMl7w _|G_}wn.+ӨEF>aٲr҉L62Y PMmɉ[^k>FrrFe1lXtD|R76Y,ښ3&@ /?GE[:gr uxHɪ N޶ ii %HKM-K,:RjԈߚ>=5ff7n\rrbuvFi92Y$P}} ʊ[FW__ɫcMVf NberKNtH{O1#:t'o)>j5_?˳/E&i, Vp2SN9C~rmXd;fʻgp졇'ig=}zC)5H$RiT V,խۑ]);?qO׭UkvHQcq}„by֊oM޿w|q7m;`0Xj׎7oZiTߔ77(Gew8骫&i vMwwC5~  gHNJ5P @{3y??۰aww|Q 4jsnmO=''%]z)& Dk=MY~}qGx̘!''? 9w_:ܫӧ,]zO{啥DUE]p-7.wMF֭q͚"5o~H$ ƿ%KVyԡeZŹT1@ 4i[npu1O\y矟:wU}yaQQ5o>t FvtY]Vzm5m[sݡP").G/^¤I|r0iҽG/?wM䒘GyQ Kx|k^ް<yUM6HPȮ]>#mPϡRFt~w|Ţ_ },a.߿ V;;}{윜;Hdպur颋9䐘G}OMŎGOc>{F>~z93N9媑#?3My-Z#D,({η7aB hިQƍծ5/o?/]jsnn $'%>rdK.};/-[nڴfZږ%V-ZGxn kwO/_6hsn#c<2fL(jݤIFdf&BEE[7mZSv/uVNW!C&|Q̣+nx~8=5]4HOn]q㚟޴eˎ˝۵颋(;d࿄BG7[_EEb[ڵ< W\1g˜"e.[ViܱBRR喾W\Dvv'/^rʕFvΝ<_.N^~ܬ6{P ' ^>p̗^:dRQzsޝ/-Yh_eY{Ǐy܇N}>11{J_Ё'^zcƊXѱ959ɓ={?{OGG\VΜ/77-ktpp|llу ًx9~S>9}ᆱ/MΝ #O?[mk0c3Zћ~ֽbE FZ?_ג6+?ݸh(Fc 8>FS~ z͛wffwm޼gx׮FG8}M9݊]tK]qwޛ{0?_Ŗm;z{}}C#{GFvȍ~+ׯ߼}ÅΎ-]]=[SC wwuս[q?{uiW2i]u1Y$&33evnnӦM[nؾ,~nbEw;ʲl5P= & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1YbdPTUUTQuO{uiW2e]<91Yb,'\,ˑM4{N AYOL ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd! IDAT & @L ,@1Ybd & @L ,@1Yu @QuO IUUuOXʺd%Y & $\L4,zYbLVuOM)EQTUc֫zVE 1YdBLh9=Yl|A=YUOOdГh.1YUdHLh!=YfZKO)dӓX=1Y; *`( jKPk-BED+UqEV) uAEP@H G}f!f<9rfɠOIO6ɣOƔIOfɦO)O`g)RC,NQ& >YS& >YjLu`O;ԻO6~+=C81ei@,@P& 2SH{тDm;;Q[a©)HdҀ2Y4L (HdҀ2Y4L (HdҀ2Y4L (HdҀ2Y4L (HdҀ2Y4L (HdҀ2Y4L (HdҀ2Y4'(Xxsrs7qH?/Bm:tP(ՉQ& F.\:{x++2229e- nܼavD#>M6|d aJ~*yצ?4}¥YϪY}~soz# :$d Jʦ3۟*P,*TΞ>{ٝ{tleND@2S^? r7ܯI$ߟ =q#~XC2YIOC?zuUdq\cN;&cdfgd@ms͜WszdR#FST(aA UIvO[7_wn{|{fNr65߬]jԴ99%?{/>۞ܬg鉛 H^H,TE,եL*/eA]0OI0_|WiKGt'f ]) )(Ƶj٪Kdy㧍zxהb/<9)D"R3NxnBN8%@UP]dcXtbE`wvgvš%?m, diҲɮ8ظncyiy`X/^ 7憒||M@B( )(D"kOv9dɹ rwѹ/=Ҥ&{ӧwщ{}^`8WL"a_8ġ}}y'%?Ԓ2ٽ2YJ]pr@mm۹흯ݹdan t勗$+(jjWSFM ջc 6HI$تz_veaYIMiKŖDN,X$pфR޺CDs=q=%-ygRH8ec=N!NIox膬n]J"Lvd)aNx=BPJ"A\ڷ:g9aIqC7=<@be:^ko۹m*."_\|uKCP}7kլmyRn{Ӷ}mCZwhT@B()/-gbCFI~AяEoL{c9IIqIkԽ?yK>Zwҫn_Hd=JKS.8Mk)Qޙo;TFSF*h4 Bж JB> { tpяEKMkO6n޸mܽu]aSV`6ʜ_ ;utHCv6Ec.kwm~8-HedavW}a <vޡ>Adt*2YL6##^,`q`x ;_O=}_ 2g\zF n|7n 5wʬj^?wN.RzMp8SV|bK˷6jO޷žUuۥ<:ffѼuy׾;w]=A=ve{!eP;Jث,_AwONb˘cM?xvd]|bf L7cGztVNVäD?|-hGFת\~]w?=ќ>ٿ? ~$yґu%/f7)$2YҜW'IR"N|Ҭf.BcN;fc_ٛg-귯].P^ݸ-U[%Q^R$=h6Vg# n>aLrv=vΫ$? (kɗa(q\UAZ- 32[kŭgʋ__w䎇tocG ucvv(+)3xL|sطžyR֮\[40ko}ws@:;{Ku:׻3 Ypsvq@h3 B>,*IJzn֍o wkKr|iev]=ӻХ.=yQ'5uνh'c?ύ~L~vq.!#l{]g]u{W?@U<|bgAWO:lxW=a]$"ȭ$ۥW O~lFFF('v魗ڰvC\qU'|RՍw[h8ݧuWpptsO$P{d />"vO|oʳ訸KߏD"k&Y&0 ҫN\/^ @b(86m??aݙ~0\'yO3ȫ< F_S\W-[$@B(8歛'9IÞlӝݪsα kV٭te222.7qddyrLVv\~6IP& q99Le%e^;?5mvǝ;UVv-niаAR22х ]6v]{F!kH d E '?I}'e%enGtn)3nw5حUVwi7kƞ7v3~ҡzήq{ݻE?ymiA,Q4v$I`ނv]`:u22cVn}{Ug]zwn0AF[n0nY0|.TFb{oZU>W~2lZ} ?K5gtɷ"I? 5 bh4I,nilqaC$6T{r?7~ȡC er)5>hDO$O@ٙ'I'ᔲު]޸k6ix_o[ (/ͮ$@B(8Տ&?Im,J)[*kq9+#lҲI-OIw;W/IP& q4l0vX0$?L2վ:h7ԽSUKpYf?"n(>O$2Y~McHdӺMCo|sǕwTDF Ju~a@Lu5i$3+sKŖ|Ս7NI[nۑݮ86WU8d;׬f͸M>) 欴5Ö-HeGNM6/W~K.) 4-͈fTĉ]^6wiL;g'!ۯpO$D8`7վ[al-b^$Uʜ|5/ˈ,~.ͳ;[a6@|]zv~OLm;[pCaҳıvq CЈG6ȍ~KŖX()v/׬_ݺyHeG;\ޢH$0ISU $VуFl ?3nx膸wz G]p݅6bB,ש{ 7~$'SN:aq>?i/vdo8##A'd@~|bÄc=$@_TQ-haNn6ȍaKŖ{cƢDEk;,\ki+\˫)$2YRS6.zoѪeqƱ S/:5\''~EnG$y׬Ϻc?rȸ| Mm<)oJ7c엓0@(*9'3FEcUE'wrhc3GT+ݎ|aC:^uUq9<.[/bBR}RV|"!R2OH,lYW;|WTlIZ}[(/vwщ|qn]qkWHW.zo| 0̭{˴[fWuװ{>=/yv6v'_.zoQ7wkqG<@(i߭ }_+iBP瞝c珌{O}WجU+']wi9VQVQ/^|vu..X4Q[*.ܘ3 qp++?W?s\P'N|#ZU-7ם H ec-9u:a22c=O >F;$Z=g\vƱT\X|>w˿V%?ށ:^:i'^2q+!,zs IDAT叜22Ҫ&\<:?ȭ۽_~8kW>K.Mf0D` }X-HUgO^wug^~frn(vU]мu:]v-h(/3+tsi k7f/W.[ܣsu\c/]ђ_vg[7OppYI٦~X/W.]tg#moɚ[:w۾<.-0lӱy^^Șpψ4g\zF5kםvv.xH.tBVnw,f+SY$[E"z_Łė&=oB$C_~VwՕP%e[}շ(v(ϞmҲIr2Y8ᒏ\r7k deg5ٯ~wxH=;32wן}]R\6ݯiVjl٢eqfyl/Wh٢e^]0쬆M6ݿi6-ZТM6ޯ{Ry|rf`򀖏~hk3JPm ' L*)kة鯁a8ꎫفZD"EK7n) ֫ 79yKˋ J2ٙKѻ-[ K+T넳ss$E.GfzP;$,He{!ePeT2ٸW}/.}?x}&?{pyc׬XL9cMI*=e{!ePT4 ;ό];co퓷=YR\`=V-[stycd֫{kwj=R @UBж JFSN}xqW6i8^xj=X$x/Oyy" 5ktkwu% zzo_=<U]ueTI9/)..=9O{v)+'+cl\w>yΘWuA4ʼn/`WP&R& UQ& @u)_y͋_ˎpm/ߖHIJ~*oׄBA]62@U慎S ?<g}s>,vwZ0uC5O,VNzq|qfR{|Ip^]RHT=D^ tgOϿ7ޫ(Hu(@Mo~'N;: Td fp '}BIqs>^0Ͼ :n;n߭}]=Ўt B@(:@aeA UIhT(5ߏ|{WC_Ig*UR3^9VT`ǔei@,@P& ei@,@P& ei@,@P& ei@,@P& ei@,ٻتPmq1+:,#DCdu!(I(*q.EjQ$>ĸe!#AAm:UFtC9?4ܖ>sn_v?0sd @Ld @Ld @Ld @Ld @Ld @Ld @Ld @Ld @Ld @LBY@N^/@M:zKV{0vb & 8z&4H`4]dwQ 4 Z^(1Y(1Y(1Y(1Y(1Y(1Y(1Y(1Y(1Y(1Y(1Y(0BR).jgIK P ìCqgH1BY $,'q+,GLJ@0&q'&ɦK/#& @S90V*k!MFÕ&dtO6W%ٜ/ c'& @UVd8dkW9)ddt5&YMLJ{JdsԄ,.5$& @8NL2ɦM/M@L$ @rrk 48Ȑ,UId&]p71Yj,x'& @8NLГM^,@I'& @ [!Wכ@>PlK ;2$& @@d8d7L MILS`uBn^[@PclMKu:ILKw`8NLɦ?>,ţ$ $& @]{!nUP/l%ٚ GLIa18NLՓM9}C d( %Y&21Y+] !c|Lb]'{Bl7f"& @ҭX`tb4Blj(=` d$& @ä!1Y'ݓMHdO d @LNo&& @N)pb4J,T@L '+8 bЖI!ʿ&q+Pxby'Sbb%# |>') @Z^d/{T@L |W[ 4RV,o@̞=;= !qe((?^ȗ%YYP6)~IoH$+# dd;1YR8IY @Zd)L(P1Y<hO1Y L@Zd2Nb4%_1Y|P_Od$,@DQ 4ZK 01Y$2aJ(d c $:F"ȇ$@OH<{Jbyd N4 & PbB26 DL"sd @LB_FQ&@@$7x,@4Na((1YȗS$`(1YlDQ @4H!(  MLΓ (,@(z`d! @@EQ4 & Pb & hQePOϜ9s8餓BYo@fd(^z)N=%K nٲ/^+/rbp3<3?uɓO9唏}cӧO?3N9唊?⏏ Kgó>{՜СC_:;;G^{[ݻwiii8/^p3gά~ dz.0eEYm_jڵ.pYgܹ3˗?7|߽jx~hѢÙ3g>SLIW_˫+\`t<93zzzV\N68pekkiqر޽{w7?͛7WyJd!5kִU'xb߾}5<E$[*n%Z9ps=ݽxk~g5_fWZU}IT*[Qe OgLχ~7|sÆ WՋ.VGe˖x㍃ ;N񁁁lw8rHWWW_ח&4,@~O6-=wuÇO&Mtuw}G|Ce IDAT;Stҍ7q<ۦM6w܋/x_.\8gΜcڲ^`;Ӿwuu/[n˖-׮];wL :Vl޽]wݓO>9eʔL'|r?;o1:::ꪯ}k!8^y啗^zwϞ=uVEg>20zH&gEEQ&@~z뭉Yo^坏9re_J_OY:xDx駟>G૯z嗧ߧN :tow{nΝG˗/[_cǎpŊ7|s7YrHWꪲ ?lmmݻw?;00p5߿Yf^.J7dG߿m۶M6۷o/?4lT G-Yq&MZzuK7nLdKҚ5kF/Vuԩ3f8o===7Fz>o߾`7onСCF*^{[n^-J+W|g{/5ȅK.d޼yTv|01 /n8.3f;^=v?܀5رc{o#OK[[[ׯ_zɓ'Wv‚ dr!f͚Bң>oTpu}'mmmk׮-{J|я~TҶm>ܰMwݞ={s /OյdɒƬ@dbΜ97G}?}pŊfͪpJ]g}vz>88{/Ss 8ѣwqGK˗/+ȑnaԩ?>G;3fZ*ֶtҲLڵkOٴi?sϽ馛}:9!& PG!/(j(N?z֭}y}%rKRm|+_);O/YDsOІ ^'O\1Y|YlY{{{zgϞ͛7oߟΟ?.~g>3iҤ_sM_|s[`A odeʔ)W.{i'CWW|0|ںvBmVO|X82wEׯN׿.;kt"$& ;-:󁁁 6?O۶mK -[vlXnHY^~?8n``7=ooo_pa=Nr(W dr'f͚6n?=$1<rԩSL:::{hhررcW^Y L!W ~N G_?G?'7|󩧞Z{O~_z?_Yv*;_hQ drꦛn:m۶޽;=/}KK,~gϞ3g6x83;;;ȑ#5o>4i矿xbRO9p@mݖ#1Yjnn޸qcK7op•?<%7L0a͚5o`~_;v,>;wORVZ5f̘ɓ'%3g&rDL -ZO:>?qĎ;?{Wrƍ[[[k\t￿$k+WϷmݝ#=Zv^,9|ȴ({챲o~SNkvŋkmݺl}W}# Ϝ98?N891Y={E/nٲeݽB;N7^~;waÆ:~/ć'Od(pU6l5jT|k,.]t̙u٫OgϞַ㏗}J5C۫?ܹsa[[['Gb9pׯXb wuttzw^z=ӦM{駣(n52jԨիWv].\njSTc 0 __Yظvu֕_y啕+Wx>.d / i/@-^d_W(](dc͛6mڹs+^pO|%Þ{c>3ȯ,~& c=| ĨQ.]vŊ iS+B#>׺*>%><|ki/ ̝;wofɼuTVE .\xq[[[{Bxg^xlkk;uT~({K|'& 3gώdNba^{78mڴYf͙3g4d?E/_^2o~׿u֬Y9iҤc*\wJr,@f0{mii=9rȑ#'S~+wvvܹLߺuka͘1f䟘,$#>_?qG}S(>߷o_%sbӧ/Z(>uTkk{vwwW%& [fMCCCw}7{Լy˗/ٳ-1YH7|睝===:jev !Td yW9rdȑ#w9fͺ馛Cw|? Wb+ b뮻ϟ{˗/(,YRK.U9$& PC!W 5_WK׿Ν;7s,Y?gĈߟ} odX,3a„/}K՟3nܸ~;v䂘,ʊ+Z[[?gժUcǎCk֬yw'& ںjժikk[n]K.\xG?W2NLjhٲe'Nzhܹe/uww/_?iYbPC_ת?P(lٲeܸqe<˗/?|p d,Yr 7T 7/|aÆ qdM` )ڿvpϞ=e%z믿_M?;غukGGGOOO>ݻw޽{ƌ?̘1cر9wܑ#GΞ=[>@^ԈEgΜٻwoe=vرcǮzŋ+;/޶mۑ#G?{mjjZvm}5)i@V^` !khhH+7t?{#G{」G*䗘,@]ŴW  ,9sfR͜9__lYCCCRgqbPBaݺu 8zWբEIΞ=sƴȍ)S,Xd8y:1u7|sO;vl)#F( >/'N,@ZZZÌ3y]wݵlٲ'NVWz뭷vvvnڴW_ݳg}cwq=3kM(C\EJk'O[8|?SN;wkfҤISL>}9sۛ_Xo +%=91Y Bڻ;&MG`!|ꩱ !zdo7E0b9 & b!W eb`ŴW [dR  ,@@օ^@ŴW sd$Gu0`Pdr@L2- d,@0b1"1Yh'& BH{BL_XL{2JL MJa@F^ 7E0R,^H^0b %f &d,@ J`%081YȜ_?#dRR+im@xCU䀘,@jB,im@J Q+d M~$& Bu(yu ,@BTJ@ob' 0LSbWdwҕ' }:1{m?Ƕm',JLX,d<&;' ,0,)1Y` zωГxBLX,Wd|JLX,#1Y`Ezғ,.=YdV' && $ zʼnz%zegnyɎ1~~ `M8'&  &  & p=c:oN,6{ddddddddddddddddd_yp?sN XQUDm^,V[]\ۑ+XZ\k+vj?S"B1A#D4, "kBs`ơ89 I/~ Gf2 !L 2YP& dB@,@(e!L 2YP& dB 痕%r,#֨q-6o<3gE BZv{wmUShM6]t= :qn㓱PGDxm@F}ꭎWWϫMN_BQ22_1[323e1G~g6 ;Xm@5*+-[׍zmkz (H+[>2 ?í.@uR& /\>zUKV"@ɬ_hh,D"e}/ܶiۦ V_Xs`kpqdQ& Puڡ{hѲ7-o7~2cٌS{zrjN EM/%|3ߛ^{`ܷƕ*jL zKx.xz5P ~! |:뗳>^ ^dDFfC~SNJxxFͯT#eqnq>zO|G T#eiKξ䛗$eeϫ}Y PF?0zыWyqDO~~7o߻{oYIY$nѶEn{opU[?m|w Vl߼$aV6hؠ-Py?yͻ*>XDs[oեW=fo~ ?*,/ڧke~/N}q w|ob3/:\yMWddW{Y+fefcKKJ_s{`u:׭?o^Rx7}a ϒ8]:#ۧnR& 2H$Z^d7nK|b+3~<5%%l8*lШQ?`[nuc}kS=yLkekyྃgΟ?s7)ܮ Poj{ߐk+^x)w~3U١c dz᭛ߴEUYzN,@ԣS5$s|m'5PiU_?fИ\]!5iNzV.^*w}xj?{o)@=L E>$_lb﮽?'6qތyyM6 z! sn}_늻+hҼI$)/+%6dypPѡ{GܛZh4nhuC X{X<ޞv ;@fm/Iѽ/-N7op3Ϝ(;'{G^s5]zu92/=TZIW,Zx@я3vj[6ύD"OUlb}jS\#?+Zky؛wxV; 6;mnEyEGn}d·sZmQU IDATb'E[s?Ss$Df]|O,|b¬ 7J<{ ߙPQЬf9y>*oڲ?~I훷?zG&cƏ[8wUvޱu=ۿ9=o=XV,Z db~m_6=>djm͝6w=ɯeeq:Lq~wgj̽/?aքGO@Q2C~e.3&ŵht]nQ$Kc|ЁXğm׹Oib^R\7\S& l{ 0//+<M5QF_=W~|s8799_[޶i[-zq>*j5ň$ |_;;1_$'adSCK^^ز:{ի1̬'㕝voݡ lҬyq^bwd.O}cLJ`%JK*5zN,@zjаA`^^V>nLv]>|MkMaaOm?)Z5K}&9vL{ٹaiIG+>(3e)&.}eibs@#60;'y:#eeɜ}Wngt ,=S$;1/X]PQgdSEyE` +1__(1hE)/p lP}R8n66j(1'<ߞvb~s+ahmuS@L =* ]6.>+d;_b<<)4fdfpQ$\yۦmyA}S<71ܶ1 8eྃy`5_yVϪc@İdˆ-UX,ŏӮךK. jܺ3iO'žm'}YbبqZVe=;}Yʭ5㓵f[) ޻ko [wmNiڶ+1l}Jh4ZZwh]-/L}o)_TH{^bmy{$sCn )ﮁ[[HdӦMyÒC%af*Рa/[*+j҆2YT 1حcvNv/:j^sAPUTT]61=w 6H JʪFIqI`ްQ*N>b0kաU5ҴEj@S& >Yɞ{|?YNb}U\X4oR'[NӀ?96ϫeఀ[|Ёyv-UYc;V[Uel 徒⒚_S& ny#1oݡuϳzحcbX|xUd[SN;*ck6L |AQb/vF|Udua솁ݵuJ~u+#dͬGgv=o`rʔ(+-[5ycuS [^ o_fRV,Zʼn+w;۱N~M ɪ%;s9)Ϭ1OmߩG|Kk~(H'E&~ob~qffe^8>OS[f\v^޾{ыj~(HȦ =+se`>{ן}=1ҫKA}RXFOٴ^"dCEycw/MR& bELsC^ |t$]{۵=-1_ڇnyxwO.>X\ݵ*WL<Zg_hC#:|=;yp O|bA}/oŗǟ oqO|4p\2uݓnӱM9SO/+-̭[zNuXz! > xEPI~o> 'cƏ/C?9G}U|T^V[=;٥ mѶ:i˦K_ZRtƭyy⊛r3?av~<ՒU5N!euђD^Nlvq]>Ԏ~`uZh}S:{vҫKnܲҲXjON_uNmSۧϞك~"[w~|g2b{vpjY%vَ-,H?dyqc)OF㦏Ƣw6ߴid6_q)S ~XF Ey00^TCL^0ykS${XFftWVuvf 2C2s3/:ʬH],;/~.?i89~o_pS+Txn[2QY.< p#3>~*N;iCڡkj }~Rvn}whFfFM5iѶENm;ܭ_g>٪ڧ?z{v9ᑾ#Fh8K4lб{ǀJi8]v)D"Xlط]ve/}9o,tˆ-oѶŀs9ܹg~+nȺ:^}p_A^<6!TW_~Ͳ56mۻkoIqI4mܤqNvJ3{ h@-j{n-ܺs}+9ٹ-r[whݮKS{ڭ_5U78:/~LTZz2YiߙjkL 2YP& dB@,@(e!L 2YP& dB@,@(e!L 2YP& dB@,@(e!L 2YP& dB@,@(e!L 2YP& dB@,@(e!L 2YP& dB@,@(e!L 2YP& dB@,@(e!L 2YP& dB@,@(e!L xwFG~Y>V׫+8!?ߙjkLxmPW(4}@7e@]HY n8Xm/)e!L 2YP& dB@,Ar@EQ[b":gUTbd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L , IDAT@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & @L ,@1Ybd & $Zk'<-q3Ue j&n/1YfM dm0 1Y8T-^.K"& @cVH'& jOvLrdSͶHIdD ֓mYd$[,y֚pPbLdGPc`dbL\S`1YfSS}{lH$& ;%ں%dP ֨+d`N'{Zad8v=,`Z5zdO{ɈjIm)b̬&\k"0ړ[p:5+%Y6!& juZCL6SKc p 5Zï021Y΢dU{""& y놔dؖ,'R[dw,dk^$& kMd8ړ}EmnELaWS`'bQMnXd`ZZ؊,'Uï5 jOvZ}d[M.$ d8]ғ]ݜd9$[91Yή`k.;=W5{,{Yړ]X=J? a,|[ޓU`b4%Y'& `LbYh`' qvd2aY,ydd,x,, ,=zP\.G.|~~}v^Ľ wKpnUV_B<;'& @ZjRVb<>k,$ Ϫ7zg04%YXGO#& @%YX= &#& Zk'T ,1.K U f"& Zk?_j^:7DL}Du}ߟaAPT@ETX{%fE(`MLFwW&X$nĨQ%bEc *<^ssL_9}iLP)J_Zj |M7eU? (T8JR\F'.J2Y(T,JRbm|/@G,T*RTKmTI #L*dTK@&dR%/"L*dTK@dR%e;¢L*dLPᤦZzp@eD,JW5GT @%@,T*TKT )JKm@O, A,T@%@,IMM @5wʅ2Y%)JK 2Y(J aӦMOMFѵkWKՍRZz@T*ujMP>}J>``-Z& B-4V@w}ϵZRwrrrqqqssstt(rrrnݺSFP(윜ܜ, ,=zyqqBxڨQ# (_~mU(|O۷ogff* [[[GGzկ_UR4;wܻwɓ'/֎u֭_~Æ yTOyyyw}annnAAByҰaCWWW+++KXhڇ޽{7;;ٳg/tppx ;V}7o|˿[[ۺu뺻7i҄WK$55UOVT' TX,FTZzHKKK9v؅ ju)W֭[˫E>>>ZWL{tyKVmFܽ{'O>>mڴСvRƢ_nIk^^%}1Sy;tբTT/?o߾}y`߸>ď3<81ۙXǟ8qܹs\ikk٢E6mڴo߾m۶Uej/^o5^f̘1c̙y999h޼yn׭['D}L0O?-2F-###;t`͘1t87oy[YYmsqqy.Ydҥ.fO.]4hI]n]^JÆ ﷱ}/_:u͛7_&X[[o߾W^}n޼)bkԨQ!++k۶m۷os玼 zz뭷:w21L>}ށn1c|W2j߿tRÆ >}z dئMDRŽk2KP[zj!C-tIPqv֖T(EEEV?~OΛ7ψ롂(((ظqU=zdȜ'Oťɢ\V\;888XzB\dIddF1pNTTT *KlT*W\)Obbb>CG>P&kK/,lڵ%5 ׯ}STEEEO>LOOyfJJٳg?~իWK@Ru޽wm۶mڴi:u sss߿֭W;wܹsS*ԩSF%]T*t'/^}vݻwK'OΚ5kŊ*ʐ233Gu-[k֬{;~jժꁡVO<o߾<]x{{;($eZv޽cƌ1ʹ "33?]97|ÇT:Ϟ=[fnbgg7a'|75j̞={ի2QէO>zJzO?5JKfV bYQFCȑ#'O.3@ L2tЗ흝[jP(Z˗Xɮ󀀀 k7BA9qrqqnnn|Mݽ{]v#F(^^^zn(/̞=[ٖ-[dɒn֭ԩS{iee%ťUV}U(Zƍ*jQQQ\p2?Kvvѣo޼)yȑ#Ǐ߸qcMvvvNNN={|dff:t*Ѐ IDATh޽ǎׅxc?wpp(4kB.k׮]VT yh\/++&٠5 \]]v:mڴo}K,3g;dѢEׯ7p,%55E~T*ZPd!LMM&ŋEAAAᶶzqww>|jѣGwm=cVF- $۴iݺuscPPPPPZۼys~~Æ cwF)T(=zѣG׼xBL_~ڵ3%Ϗ$555:::66VnРA㮘-4III>}dĉO.nT*[nݺuӧlݺu׮]iӦM60++K\&j?:u3ҥK2٦Momڵ^zL}m۶$`@O2T*&Oex۷K&&&?4։&M*I{|󍇇\]]_~ k׮͛7ުBի!Cԭ[$yMBBdZgff?|ܸq<;ڣG &bŊ޽{wŐN8ЫW/CJ'i{̌M6oeoolٲ3gcAX؎;N7겷:thddҥK [ But*%%E|R/-[&~ذa;v4ikJepp8OLL|NS#R?nbkk[ju։Å $KTlrΜ9qqq2ӧSNU׮];o<=d|||,Xp1ooo׬?j*sxܹ۷ogϞ =d|||.\xa_ɻF .LNNi֭ӿIVĉ'@>>!!!.'oCU֦Mzeڵkb7nyRs̑ԩu☘2/xbZZQN߶mM|wJe|JդIC&T/^#CCCǍgg˗ׯE,Yw 0s)WBB 2hgϞ;5. Ƶk׮ׯ᠜ZYf : ;hѢEŹ>vٳQC?nbmm=i$'$''jA8d[Q5$%%m߾][YY-[+$˗/7Y˖-s8www_re͚5ͳFwٌ ݤb7_7=zĉ믂}BW_{ʔ)<""B]^-*..6p$P}]tIx{{רQ"ˠB9s̾}y͚5mmmͿJQ\\xb3cq޽{w"*Ty!!!0))$"##333u~M )))Pi^?W^feu떩OIOO_v8 [,ɠAڵk'Rf*æMy->sC}Q:ud 8q?;;;22R~:^*~2Y)VA(UҥK%Yfy#..… &="++kӦMz…*ʤb0`dg7o zH  ^Z7QT}QH a ĉO7>}A4͏?hSVXQXX(GݱcGS^Yh4XA"𞚚zUC駟$o!f{ BGGǑ#Ghoo?vXqqFV+o+/.**2p@eTSFZdT(ׯ_?z8oܸd̯v0<<ܤnذŋ|ȑ&=WVyLLodXoǎ*!CO\VC>l@P]V2nggge &I#**ʕ+;4++kΝf͚T 믿233unݺK^/~K-lqܩS'C&CHq {oT*͛7bkk[F Ax~MbRP& @5emm-/_\PP`eP믒)Slll̼ $M2E9r$11D'I>0T*դILt(*3gdddȞjahh(+W&VVVr٣ڵk3F/]TQ D,ՔJrrrϟ?߿EAh nݺ6>ԡC}0CJqlq԰aCSʢǸ83^zu. @@׮]uzxxwpR| 7ʊZ!(ʏ?X8pܹs8QرCrѣGJHK4$$D|yǥIhBLvAF#vjdqxr_Ox5jdΜ9011ѣ8.&&F2뭷Lq*OOOoooq-{z2FYbnT*N*cTfccce.*a޽n_%ڵk޽ELq˗y=7nl+'O& 4ԩݿFfoʒVB;C;vLvݻw>rC֭;aqbŊgϞ_ 2Y.]ô/"8xdN[E*Œh4NNNqBe"eLjQQQPٳg۷u`Z27/oܸ!g9T~ΝlOHHHHH0Yy†*uuulj2}1 j'O۷7|8?uT|NNN֭@ eT_ods>"8~8tuum׮AfΜRaJJʾ}{Ѕ rssy>} 0`d+cܹ888r6Ci4A8uTy\]];w,srrƌsycQC`MPvEk&-EI T߿_ *%%KKٳgϊs???;;N222y֭ nccӼysq.(G}$駜RSS-Q& @վ}6mH޴cǎw}ڵkf^ K.ݻ+Jyzz[<<<\Dψ|71-::ZRZg67oMZl){1c$w}wŲ2:ud޳gO3o}̙3G!ܹs4)UZruu5AѣG>}4nX#|Mɮwɏ<[-"%%E22|9/^,ѣGyyyW,oeKMM ՔRK… ̹,ZVw@ӦM7nߍxʅ $]Tjw)לbqlHHPp0`M .>|ٳg 9dghƍׯoeP͛|iӮ^jA %Q͜9SR +W˗/Km۶5UZ?ahhJ̤rxEqhccӪUŕQܺuKe8h45|ƍ +V\ B,W_}եKR.x-[m1%͛y>۶m3|F#]բE J&M$ 5'**J߿RV]l 6m}||_1:w\Toh4e˖_zjҤɻ+/^\TTd#nܸ!u!ZxzzJO򭳼oʒ_4kں\s`...ƚ,gddwͬY-[dT$z;;;~iʔ)'O,岇~嗫W1cU*?eˈ:wmC4|}}}CWWך5k>2nܸ+V˗[.33S*0ǏY~r%880%%%--^gF!!!ZCҝ;w h۶kTp|noexhhhQQѧ~*Y$VݳgOLL[o5}t777]3g899ɻo͚5%;+BC&͚5+2O C0iӦo/^ oݺoI̖WImz}TPɞZjg<../;wsCqXNc/033SƴZڵkaQQђ%K;9_5kM6~ݻw?UV͚5VVV*!!!!!Aqڲe˖-[d=::e˖e^v=qh:?b„ 6m 6l0i$geeI52p2L*))i̘1`#G. l2e>N<#ť[nZC۷o.;vJZӍxĐ!CfϞ]ҫKf۶m;w9r)SL ?m4޳gC^4 O?' 6*;K.2dH5 /Pvm'Wyyyd=ztI???}N|IAA8oذ>w=yD֪UXK%ynT*՜9sov4i]&?lJfݸqc֬YjfX摝-]]]Ϳ ?W4PRQŸo^9!**JT*C79ryݤW^;v4)ݻw߳gO~ʼp}rrr,HB0uk0 7i$A͛7>\4t/^MKi۷8Ֆg+_XJ^^84LI#00m۶72YFmeU?:|Iy睓'Oa7Fy8UAyε5k8ٳgyڵ '88X^|͛e޷`߾}0$$8TZvٲep8%""be^VWZէO˗KC)_ݺu'L W\YGIU:998*$﹯W۷PyV(ł6lmmud/oRyllŋB,899͟??*** @Ϟ=;vI&x5VAAdngggM ԩS5k \R _2)ǎ5jԨC :qٳgu]vD)ʠ{~'E-^8 `& !Y=?~|:uG֭[g䢢"qXF V999GeK^{1}ZhZ\R눒d%z֭8=e@B-V\٣G} YrF1n0*PY1MUV8++i5jԱcGqOlTT af˖-$ӧO75jԘ8qb||3j֬YYYY}ѣ)JT*ͼ dptt|811@Ubm@աC 6$$$%''~qAAAXXÇ/^ܠAOOe*'>\7n\5%5CQ\YΜ9>6m?~gVOX@GϞ=ϟ/rAxk׮5o޼{Py;{G}Fk ԩSnҩS={Zj͜9s̘1WްaCAAA'&&4hC 1[lWe N:תUKJe3fڵk>|^z޼yJ~+2UF;w ٕ~4jfJP2/k=uܹO>aaa7odLW^={?ï^ZʼnC_Znm๵jj֬C gggSiԨQC2W&=F4tիW߼yS7,((X|_-ofI]c<0*8_;cƌu!#^1Vfx -[&HMfrz}ƍ[|ydddxjz޼yW\?aU}^6kkk3x@x{{ BF?*?|P@].Jmڵk:tH}1-_\L6Ͳ5aaaQQQ_~_<[X$?i(QH6oݺUvC#33(EƆ  l}zƦ<>>Y2j)uFznҺukKe˖\޿l[p4 ۷ͼ 1p@tQQђ%K tssj:;;[*R,m/Ipp8?x`w]v͚59VK_84`('%sС|ѢEP Y[z@uj%kPM4)88xΜ9gϞf'Nl֬wlǏj:==I&Y 2ӧcǎgΜ ZŋW\Yi5JOO׮]3hKT]ڵSyLL̬Y%!!!&e˖ ӧWOh:tضmۚ5k$j.\h djM`J5gΜiӦ ]vM4˫_{5ڵk...262bccZnbgg7?RT*===/^(SSSr"$;5Q%׌3~"w hѢWL7޴iSIZvƍf^ hժd~93oC(ʏ?X%U?Nqʕ/^Ș 88X޼yʕ+ɓ'GɚAJJJ||nҲe~YjT?_~`Νf 988d/CmV/^Xƴ>^pAƴDܷV#..NrÇ|lٲ8|QFF ! 2޽{PReQ?XXXh#ZL6,,oqqqZ+A6mH7w'-Z$c-NPɫEude%bcc޽{5nۤI.BP,_\L:Uo"[lRŋC}#dee%a#%%Rʥ8 >>>OjݻUXXxүP(N#q5|zzzddꣂ]PTuߔf S֭> ">>s'NHHH(Ν;K8$ݺu111h5& Brʾ}tf͚R7ޘ0aMN220D.]aqq#G̿ ׵kWq^Q?TrbbbL:_,P'`u֒׍2ڵkⰤ5"/Η-[VP-t*&dԭ[?Iu̿ (ԩS5jhF#s?Tu0--˯~yI Ja\dԩ*"oʔ)|Ҩ\z)ٳ̛@JRȑ#֫W/qV,W%YjǏ?~ %^ÇsssM5nܸ^z\l:t0|'֪UKfffnڴɸ@G,0!CHYYYf7olM`ٳg+JA|rͱ{{9"?TiAAAVV~M/66VPmܥK |뱱Ghh_ڵyffl;w7 *CeddK.}2% b֭2n޼TnruQ|Fٻwo);T*ɿ/^ڵ˰MTJ؏s>ҥK[n4qDqrʧO,ڬ-"ׯ߰a yϟ/;wn&M,iٲw--ZgkTHH!WZէOqe-ܣGA|˾T)*M+)ST*):DEE B>iT.666&_~/VmΜ9G-לN:O8kȒQLL j*yӴZzz#FrKƮYfȑ666<^P$&&j4O<)ׯߪU+CJ7nܺu=z]g@&][8Q7l8h4-2>0̙3RW^1.00v<)))>>^~҂;w.^P(O>{Jz7ʹ\u&xIII 3f̨܇AgKz\gϞ1Y#ϖ< lhSzL2%w}wӟ|͚Wm۶mǎe+M60`@$=^,--}*11//+\3߿?8[_^:+X~}x;N$mp]w]pǏ?}@uĉ{ wܹ! +\裏:t!B'OnҤIb ޽{O.p={ /]4 -^\œ;hϞ=/Çj(xZeqqqǏpʕ+_zZuoA۶m\ӧڵ&Oᑚ}MIIg.]*Z.Y nj~' y䑒HlU-*,,q^{-lξkjܳjM6}PT0Y :?g w)PsqqqSLpї_~HDu֣GJᄏE .|# O{]h\0pښ]z„ FMPhŊnݺ0Dh;vpO~իW@k7ܲerůkְpѣGÇ{P a@t,X اO8P|^{m>ɓ'8qG"w5ky|0==EQ 4Xӧ'--Vjl%++kĈ֮]g+>}d"WXn]-D$ڶm[YYή,tǎƍk y]Vr0L6,] 'MYᢗ^z闿Zh*))^ݻw˖-jS~\¿P#'&%%EgԩmڴpƍG*,Y\妛nJNNE]t%Tg大?䓕-wwEEEG5UV劇Ozo>{ĉrUm]t9{h@#|>}̞=/վ}yW\ѳg+O即RYXdYIIIaw+[9ra&MzHBM6/wIgbkƌņ˖-+**AO>dԨQyyy`0ȑ#GQÇ?cǎ]~ٞч~WG5ǤZ7o`0,;;6l0dȐGU0 0 !!u߬YڙqzN:U2~s;vM74gΜ={ԠɩSf̘1o޼E "IOO={vRRRe+,\ps=~Yu.))YnݓO>ymE<&5a„\~SiO=>ڷo_JͿoGeK,9ӧO*[uÇO6??۩wΜ9r˖-[viС7xcx龎'_{;w,W?x-}ꪫjg_ L-=l߾=S4R%%%ロڷo+## /bqqqq]s5 wŋGҡ^z`ÇGk`0XY~mٲ%:uϸZ01cF^^Yf͚5kvzwׯC%m޼yڵ+W\ZZ T?D-ٰa޽{^\D7V\y8 ΅ѣGϛ7糒6wܻV/|Æ j!No;==߿?cDrݻHlhOѡC+ڵ+Zc4o޼O>\9//W_-[9FIi3gΜ9sW^9`}v%..JJJ/__O? _0?y6 7ТEljժ%톻˪‰'x[o5>>]~fzJJJ*\!???77wC[w޴i B{V^bŊÇVZE+W{o3)cC,,^t%\rUW]z饝;wnӦMfffrrr(:zݻ7mڴrʕ+WVիWγ~Gַj&sΩSFe&.ZhѢEQ#=:)E@ !!a֬Y={Wj[lٲeO?եKvedd$%%߿Ν;v쀩ϟ?fV'LK.&v(W^Y3<DX0@ зoYfM2ɓSPPꫯ꫉]tԩSVRSS<{m۶=z4zZo߾G}W cǎ}9a`0SO*d~ۺu]vСEM6-**:rȾ}>ztQRRj5teȐ!a|Ɏ;.ªʚ?}WaiP[@ бcNj/ .HOOOLLO?Vv-1cgB%%%9ӫ6e˖կƎoo=x_ve-[LHH8v؎;֭[o|g۷C=TG_ 7^ P *u֭[l=z̙3'!-4III>~_|+#GΝ;fXdggk&MڸqcklڴiӦMzꦛnJLL ;;mРAM6Ç/Xl%--{<N*L&M̙s5D}*beϟo޼f͚z)>>o~ӲeXBDVMZzW8DU7o~7VtȐ!9Lc3o޼²/555VDe˖ӧO!ʺux[n%փQFn:*߿`Gadɒr[n%_ѦM:xP(Tw /0m4Y17py曟LuʁdOi@#Ǭgy 5T_l٤I3VTmֹshukҤɣ>o;Z=Ս:b{U4/RJjj1cjm6>|%Kuݶ-Z5kּyv4j_ӦM'Oņݻw_tԩS#OF|R\\o+:\;wnݺǏ;vm`0*SW^ofkyRRO<1k֬X%ɞ6eʔhH0Yh >~رccz6,3/P(hFl߾=aÆ޼y֭[⋪OJJڵk=׭[H^XXxԩH:Q|||JJJ1θ_]RRrNHLLq7BЦM{um޼U~%\y\sM=exر_i֬Y)Y#3|P(tرrŨ@pժUVm۶*VNNNKw޳g=z4mڴ ~ "*B:uZpax\RgUĉ'O}=ٓWX0֒۷oذaƍg}v_uU{0`@/pqVxSN*,,IP=^ַo^˓@Uٗ0ɗzgPT) TF_>59^YH a3/:BiU X5MF0$PwȐM,t X<0IENDB`python-telegram-bot-13.11/examples/nestedconversationbot.py000066400000000000000000000312611417656324400242510ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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 Dispatcher 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 Tuple, Dict, Any from telegram import InlineKeyboardMarkup, InlineKeyboardButton, Update from telegram.ext import ( Updater, CommandHandler, MessageHandler, Filters, ConversationHandler, CallbackQueryHandler, CallbackContext, ) # Enable logging logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) 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 def start(update: Update, context: CallbackContext) -> 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): update.callback_query.answer() update.callback_query.edit_message_text(text=text, reply_markup=keyboard) else: update.message.reply_text( "Hi, I'm Family Bot and I'm here to help you gather information about your family." ) update.message.reply_text(text=text, reply_markup=keyboard) context.user_data[START_OVER] = False return SELECTING_ACTION def adding_self(update: Update, context: CallbackContext) -> 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) update.callback_query.answer() update.callback_query.edit_message_text(text=text, reply_markup=keyboard) return DESCRIBING_SELF def show_data(update: Update, context: CallbackContext) -> str: """Pretty print gathered data.""" def prettyprint(user_data: Dict[str, Any], level: str) -> str: people = user_data.get(level) if not people: return '\nNo information yet.' text = '' if level == SELF: for person in user_data[level]: text += f"\nName: {person.get(NAME, '-')}, Age: {person.get(AGE, '-')}" else: male, female = _name_switcher(level) for person in user_data[level]: gender = female if person[GENDER] == FEMALE else male text += f"\n{gender}: Name: {person.get(NAME, '-')}, Age: {person.get(AGE, '-')}" return text user_data = context.user_data text = f"Yourself:{prettyprint(user_data, SELF)}" text += f"\n\nParents:{prettyprint(user_data, PARENTS)}" text += f"\n\nChildren:{prettyprint(user_data, CHILDREN)}" buttons = [[InlineKeyboardButton(text='Back', callback_data=str(END))]] keyboard = InlineKeyboardMarkup(buttons) update.callback_query.answer() update.callback_query.edit_message_text(text=text, reply_markup=keyboard) user_data[START_OVER] = True return SHOWING def stop(update: Update, context: CallbackContext) -> int: """End Conversation by command.""" update.message.reply_text('Okay, bye.') return END def end(update: Update, context: CallbackContext) -> int: """End conversation from InlineKeyboardButton.""" update.callback_query.answer() text = 'See you around!' update.callback_query.edit_message_text(text=text) return END # Second level conversation callbacks def select_level(update: Update, context: CallbackContext) -> 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) update.callback_query.answer() update.callback_query.edit_message_text(text=text, reply_markup=keyboard) return SELECTING_LEVEL def select_gender(update: Update, context: CallbackContext) -> 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) update.callback_query.answer() update.callback_query.edit_message_text(text=text, reply_markup=keyboard) return SELECTING_GENDER def end_second_level(update: Update, context: CallbackContext) -> int: """Return to top level conversation.""" context.user_data[START_OVER] = True start(update, context) return END # Third level callbacks def select_feature(update: Update, context: CallbackContext) -> 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.' update.callback_query.answer() 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.' update.message.reply_text(text=text, reply_markup=keyboard) context.user_data[START_OVER] = False return SELECTING_FEATURE def ask_for_input(update: Update, context: CallbackContext) -> str: """Prompt user to input data for selected feature.""" context.user_data[CURRENT_FEATURE] = update.callback_query.data text = 'Okay, tell me.' update.callback_query.answer() update.callback_query.edit_message_text(text=text) return TYPING def save_input(update: Update, context: CallbackContext) -> 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 select_feature(update, context) def end_describing(update: Update, context: CallbackContext) -> 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 start(update, context) else: select_level(update, context) return END def stop_nested(update: Update, context: CallbackContext) -> str: """Completely end conversation from within nested conversation.""" update.message.reply_text('Okay, bye.') return STOPPING def main() -> None: """Run the bot.""" # Create the Updater and pass it your bot's token. updater = Updater("TOKEN") # Get the dispatcher to register handlers dispatcher = updater.dispatcher # 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)], ) dispatcher.add_handler(conv_handler) # Start the Bot updater.start_polling() # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/passportbot.html000066400000000000000000000021131417656324400225150ustar00rootroot00000000000000 Telegram passport test!

Telegram passport test

python-telegram-bot-13.11/examples/passportbot.py000066400000000000000000000101711417656324400222040ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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://git.io/fAvYd for how to use Telegram Passport properly with python-telegram-bot. """ import logging from telegram import Update from telegram.ext import Updater, MessageHandler, Filters, CallbackContext # Enable logging logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG ) logger = logging.getLogger(__name__) def msg(update: Update, context: CallbackContext) -> 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 = file.get_file() print(actual_file) actual_file.download() if ( data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport') and data.front_side ): front_file = data.front_side.get_file() print(data.type, front_file) front_file.download() if data.type in ('driver_license' and 'identity_card') and data.reverse_side: reverse_file = data.reverse_side.get_file() print(data.type, reverse_file) reverse_file.download() if ( data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport') and data.selfie ): selfie_file = data.selfie.get_file() print(data.type, selfie_file) selfie_file.download() if 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 = file.get_file() print(actual_file) actual_file.download() def main() -> None: """Start the bot.""" # Create the Updater and pass it your token and private key with open('private.key', 'rb') as private_key: updater = Updater("TOKEN", private_key=private_key.read()) # Get the dispatcher to register handlers dispatcher = updater.dispatcher # On messages that include passport data call msg dispatcher.add_handler(MessageHandler(Filters.passport_data, msg)) # Start the Bot updater.start_polling() # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/paymentbot.py000066400000000000000000000132431417656324400220110ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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 ( Updater, CommandHandler, MessageHandler, Filters, PreCheckoutQueryHandler, ShippingQueryHandler, CallbackContext, ) # Enable logging logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) def start_callback(update: Update, context: CallbackContext) -> 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." ) update.message.reply_text(msg) def start_with_shipping_callback(update: Update, context: CallbackContext) -> 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 provider_token = "PROVIDER_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 context.bot.send_invoice( chat_id, title, description, payload, provider_token, currency, prices, need_name=True, need_phone_number=True, need_email=True, need_shipping_address=True, is_flexible=True, ) def start_without_shipping_callback(update: Update, context: CallbackContext) -> 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 provider_token = "PROVIDER_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 context.bot.send_invoice( chat_id, title, description, payload, provider_token, currency, prices ) def shipping_callback(update: Update, context: CallbackContext) -> 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 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)) query.answer(ok=True, shipping_options=options) # after (optional) shipping, it's the pre-checkout def precheckout_callback(update: Update, context: CallbackContext) -> 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 query.answer(ok=False, error_message="Something went wrong...") else: query.answer(ok=True) # finally, after contacting the payment provider... def successful_payment_callback(update: Update, context: CallbackContext) -> None: """Confirms the successful payment.""" # do something after successfully receiving payment? update.message.reply_text("Thank you for your payment!") def main() -> None: """Run the bot.""" # Create the Updater and pass it your bot's token. updater = Updater("TOKEN") # Get the dispatcher to register handlers dispatcher = updater.dispatcher # simple start function dispatcher.add_handler(CommandHandler("start", start_callback)) # Add command handler to start the payment invoice dispatcher.add_handler(CommandHandler("shipping", start_with_shipping_callback)) dispatcher.add_handler(CommandHandler("noshipping", start_without_shipping_callback)) # Optional handler if your product requires shipping dispatcher.add_handler(ShippingQueryHandler(shipping_callback)) # Pre-checkout handler to final check dispatcher.add_handler(PreCheckoutQueryHandler(precheckout_callback)) # Success! Notify your user! dispatcher.add_handler(MessageHandler(Filters.successful_payment, successful_payment_callback)) # Start the Bot updater.start_polling() # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/persistentconversationbot.py000066400000000000000000000135271417656324400251740ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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 Dispatcher 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, Update, ReplyKeyboardRemove from telegram.ext import ( Updater, CommandHandler, MessageHandler, Filters, ConversationHandler, PicklePersistence, CallbackContext, ) # Enable logging logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) 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']) def start(update: Update, context: CallbackContext) -> 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 " f"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?" ) update.message.reply_text(reply_text, reply_markup=markup) return CHOOSING def regular_choice(update: Update, context: CallbackContext) -> 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!' update.message.reply_text(reply_text) return TYPING_REPLY def custom_choice(update: Update, context: CallbackContext) -> int: """Ask the user for a description of a custom category.""" update.message.reply_text( 'Alright, please send me the category first, for example "Most impressive skill"' ) return TYPING_CHOICE def received_information(update: Update, context: CallbackContext) -> 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'] 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 def show_data(update: Update, context: CallbackContext) -> None: """Display the gathered info.""" update.message.reply_text( f"This is what you already told me: {facts_to_str(context.user_data)}" ) def done(update: Update, context: CallbackContext) -> int: """Display the gathered info and end the conversation.""" if 'choice' in context.user_data: del context.user_data['choice'] 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 Updater and pass it your bot's token. persistence = PicklePersistence(filename='conversationbot') updater = Updater("TOKEN", persistence=persistence) # Get the dispatcher to register handlers dispatcher = updater.dispatcher # 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, ) dispatcher.add_handler(conv_handler) show_data_handler = CommandHandler('show_data', show_data) dispatcher.add_handler(show_data_handler) # Start the Bot updater.start_polling() # Run the bot until you press Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT. This should be used most of the time, since # start_polling() is non-blocking and will stop the bot gracefully. updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/pollbot.py000066400000000000000000000140771417656324400213100ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # 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 ( Poll, ParseMode, KeyboardButton, KeyboardButtonPollType, ReplyKeyboardMarkup, ReplyKeyboardRemove, Update, ) from telegram.ext import ( Updater, CommandHandler, PollAnswerHandler, PollHandler, MessageHandler, Filters, CallbackContext, ) logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) def start(update: Update, context: CallbackContext) -> None: """Inform user about what this bot can do""" 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' ) def poll(update: Update, context: CallbackContext) -> None: """Sends a predefined poll""" questions = ["Good", "Really good", "Fantastic", "Great"] message = 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) def receive_poll_answer(update: Update, context: CallbackContext) -> None: """Summarize a users poll vote""" answer = update.poll_answer poll_id = answer.poll_id try: questions = context.bot_data[poll_id]["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] context.bot.send_message( context.bot_data[poll_id]["chat_id"], f"{update.effective_user.mention_html()} feels {answer_string}!", parse_mode=ParseMode.HTML, ) context.bot_data[poll_id]["answers"] += 1 # Close poll after three participants voted if context.bot_data[poll_id]["answers"] == 3: context.bot.stop_poll( context.bot_data[poll_id]["chat_id"], context.bot_data[poll_id]["message_id"] ) def quiz(update: Update, context: CallbackContext) -> None: """Send a predefined poll""" questions = ["1", "2", "4", "20"] message = 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) def receive_quiz_answer(update: Update, context: CallbackContext) -> 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 == 3: 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 context.bot.stop_poll(quiz_data["chat_id"], quiz_data["message_id"]) def preview(update: Update, context: CallbackContext) -> 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 update.effective_message.reply_text( message, reply_markup=ReplyKeyboardMarkup(button, one_time_keyboard=True) ) def receive_poll(update: Update, context: CallbackContext) -> 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 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(), ) def help_handler(update: Update, context: CallbackContext) -> None: """Display a help message""" update.message.reply_text("Use /quiz, /poll or /preview to test this bot.") def main() -> None: """Run bot.""" # Create the Updater and pass it your bot's token. updater = Updater("TOKEN") dispatcher = updater.dispatcher dispatcher.add_handler(CommandHandler('start', start)) dispatcher.add_handler(CommandHandler('poll', poll)) dispatcher.add_handler(PollAnswerHandler(receive_poll_answer)) dispatcher.add_handler(CommandHandler('quiz', quiz)) dispatcher.add_handler(PollHandler(receive_quiz_answer)) dispatcher.add_handler(CommandHandler('preview', preview)) dispatcher.add_handler(MessageHandler(Filters.poll, receive_poll)) dispatcher.add_handler(CommandHandler('help', help_handler)) # Start the Bot updater.start_polling() # Run the bot until the user presses Ctrl-C or the process receives SIGINT, # SIGTERM or SIGABRT updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/examples/rawapibot.py000066400000000000000000000031601417656324400216140ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=W0603 """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 logging from typing import NoReturn from time import sleep import telegram from telegram.error import NetworkError, Unauthorized UPDATE_ID = None def main() -> NoReturn: """Run the bot.""" global UPDATE_ID # Telegram Bot Authorization Token bot = telegram.Bot('TOKEN') # get the first pending update_id, this is so we can skip over it in case # we get an "Unauthorized" exception. try: UPDATE_ID = bot.get_updates()[0].update_id except IndexError: UPDATE_ID = None logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') while True: try: echo(bot) except NetworkError: sleep(1) except Unauthorized: # The user has removed or blocked the bot. UPDATE_ID += 1 def echo(bot: telegram.Bot) -> None: """Echo the message the user sent.""" global UPDATE_ID # Request updates after the last update_id for update in bot.get_updates(offset=UPDATE_ID, timeout=10): 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 update.message.reply_text(update.message.text) if __name__ == '__main__': main() python-telegram-bot-13.11/examples/timerbot.py000066400000000000000000000074661417656324400214660ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0116,W0613 # This program is dedicated to the public domain under the CC0 license. """ Simple Bot to send timed Telegram messages. This Bot uses the Updater 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 Dispatcher 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. """ import logging from telegram import Update from telegram.ext import Updater, CommandHandler, CallbackContext # Enable logging logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) # Define a few command handlers. These usually take the two arguments update and # context. Error handlers also receive the raised TelegramError object in error. # 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. def start(update: Update, context: CallbackContext) -> None: """Sends explanation on how to use the bot.""" update.message.reply_text('Hi! Use /set to set a timer') def alarm(context: CallbackContext) -> None: """Send the alarm message.""" job = context.job context.bot.send_message(job.context, text='Beep!') def remove_job_if_exists(name: str, context: CallbackContext) -> 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 def set_timer(update: Update, context: CallbackContext) -> None: """Add a job to the queue.""" chat_id = update.message.chat_id try: # args[0] should contain the time for the timer in seconds due = int(context.args[0]) if due < 0: update.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, context=chat_id, name=str(chat_id)) text = 'Timer successfully set!' if job_removed: text += ' Old one was removed.' update.message.reply_text(text) except (IndexError, ValueError): update.message.reply_text('Usage: /set ') def unset(update: Update, context: CallbackContext) -> 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.' update.message.reply_text(text) def main() -> None: """Run bot.""" # Create the Updater and pass it your bot's token. updater = Updater("TOKEN") # Get the dispatcher to register handlers dispatcher = updater.dispatcher # on different commands - answer in Telegram dispatcher.add_handler(CommandHandler("start", start)) dispatcher.add_handler(CommandHandler("help", start)) dispatcher.add_handler(CommandHandler("set", set_timer)) dispatcher.add_handler(CommandHandler("unset", unset)) # Start the Bot updater.start_polling() # Block until you press Ctrl-C or the process receives SIGINT, SIGTERM or # SIGABRT. This should be used most of the time, since start_polling() is # non-blocking and will stop the bot gracefully. updater.idle() if __name__ == '__main__': main() python-telegram-bot-13.11/pyproject.toml000066400000000000000000000005771417656324400203610ustar00rootroot00000000000000[tool.black] line-length = 99 target-version = ['py36'] skip-string-normalization = true # We need to force-exclude the negated include pattern # so that pre-commit run --all-files does the correct thing # see https://github.com/psf/black/issues/1778 force-exclude = '^(?!/(telegram|examples|tests)/).*\.py$' include = '(telegram|examples|tests)/.*\.py$' exclude = 'telegram/vendor'python-telegram-bot-13.11/requirements-dev.txt000066400000000000000000000005121417656324400214720ustar00rootroot00000000000000# cryptography is an optional dependency, but running the tests properly requires it cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3 pre-commit # Make sure that the versions specified here match the pre-commit settings! black==20.8b1 flake8==3.9.2 pylint==2.8.3 mypy==0.812 pyupgrade==2.19.1 pytest==6.2.4 flaky beautifulsoup4 wheel python-telegram-bot-13.11/requirements.txt000066400000000000000000000003561417656324400207240ustar00rootroot00000000000000# Make sure to install those as additional_dependencies in the # pre-commit hooks for pylint & mypy certifi # only telegram.ext: # Keep this line here; used in setup(-raw).py tornado>=6.1 APScheduler==3.6.3 pytz>=2018.6 cachetools==4.2.2 python-telegram-bot-13.11/setup-raw.py000066400000000000000000000003061417656324400177340ustar00rootroot00000000000000#!/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-13.11/setup.cfg000066400000000000000000000040211417656324400172520ustar00rootroot00000000000000[metadata] license_file = LICENSE.dual [build_sphinx] source-dir = docs/source build-dir = docs/build all_files = 1 [upload_sphinx] upload-dir = docs/build/html [flake8] max-line-length = 99 ignore = W503, W605 extend-ignore = E203 exclude = setup.py, setup-raw.py docs/source/conf.py, telegram/vendor [pylint] ignore=vendor [pylint.message-control] disable = C0330,R0801,R0913,R0904,R0903,R0902,W0511,C0116,C0115,W0703,R0914,R0914,C0302,R0912,R0915,R0401 [tool:pytest] testpaths = tests addopts = --no-success-flaky-report -rsxX filterwarnings = error ignore::DeprecationWarning ; 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 [coverage:run] branch = True source = telegram parallel = True concurrency = thread, multiprocessing omit = tests/ telegram/__main__.py telegram/vendor/* [coverage:report] exclude_lines = pragma: no cover @overload if TYPE_CHECKING: [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 [mypy-telegram.vendor.*] ignore_errors = True # Disable strict optional for telegram objects with class methods # We don't want to clutter the code with 'if self.bot is None: raise RuntimeError()' [mypy-telegram.callbackquery,telegram.chat,telegram.message,telegram.user,telegram.files.*,telegram.inline.inlinequery,telegram.payment.precheckoutquery,telegram.payment.shippingquery,telegram.passport.passportdata,telegram.passport.credentials,telegram.passport.passportfile,telegram.ext.filters,telegram.chatjoinrequest] strict_optional = False # type hinting for asyncio in webhookhandler is a bit tricky because it depends on the OS [mypy-telegram.ext.utils.webhookhandler] warn_unused_ignores = False [mypy-urllib3.*] ignore_missing_imports = True [mypy-apscheduler.*] ignore_missing_imports = True python-telegram-bot-13.11/setup.py000066400000000000000000000106201417656324400171450ustar00rootroot00000000000000#!/usr/bin/env python """The setup and build script for the python-telegram-bot library.""" import os import subprocess import sys from setuptools import setup, find_packages UPSTREAM_URLLIB3_FLAG = '--with-upstream-urllib3' def get_requirements(raw=False): """Build the requirements list for this project""" requirements_list = [] with open('requirements.txt') as reqs: for install in reqs: if install.startswith('# only telegram.ext:'): if raw: break continue requirements_list.append(install.strip()) return requirements_list def get_packages_requirements(raw=False): """Build the package & requirements list for this project""" reqs = get_requirements(raw=raw) exclude = ['tests*'] if raw: exclude.append('telegram.ext*') packs = find_packages(exclude=exclude) # Allow for a package install to not use the vendored urllib3 if UPSTREAM_URLLIB3_FLAG in sys.argv: sys.argv.remove(UPSTREAM_URLLIB3_FLAG) reqs.append('urllib3 >= 1.19.1') packs = [x for x in packs if not x.startswith('telegram.vendor.ptb_urllib3')] return packs, reqs def get_setup_kwargs(raw=False): """Builds a dictionary of kwargs for the setup function""" packages, requirements = get_packages_requirements(raw=raw) raw_ext = "-raw" if raw else "" readme = f'README{"_RAW" if raw else ""}.rst' fn = os.path.join('telegram', 'version.py') with open(fn) as fh: for line in fh.readlines(): if line.startswith('__version__'): exec(line) with open(readme, 'r', encoding='utf-8') as fd: kwargs = dict( 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://git.io/JtLIZ project_urls={ "Documentation": "https://python-telegram-bot.readthedocs.io", "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://python-telegram-bot.readthedocs.io/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=fd.read(), long_description_content_type='text/x-rst', packages=packages, install_requires=requirements, extras_require={ 'json': 'ujson', 'socks': 'PySocks', # 3.4-3.4.3 contained some cyclical import bugs 'passport': 'cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3', }, 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.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', ], python_requires='>=3.6' ) return kwargs def main(): # 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-13.11/telegram/000077500000000000000000000000001417656324400172345ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/__init__.py000066400000000000000000000254531417656324400213560ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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""" from .base import TelegramObject from .botcommand import BotCommand from .user import User from .files.chatphoto import ChatPhoto from .chat import Chat from .chatlocation import ChatLocation from .chatinvitelink import ChatInviteLink from .chatjoinrequest import ChatJoinRequest from .chatmember import ( ChatMember, ChatMemberOwner, ChatMemberAdministrator, ChatMemberMember, ChatMemberRestricted, ChatMemberLeft, ChatMemberBanned, ) from .chatmemberupdated import ChatMemberUpdated from .chatpermissions import ChatPermissions from .files.photosize import PhotoSize from .files.audio import Audio from .files.voice import Voice from .files.document import Document from .files.animation import Animation from .files.sticker import Sticker, StickerSet, MaskPosition from .files.video import Video from .files.contact import Contact from .files.location import Location from .files.venue import Venue from .files.videonote import VideoNote from .chataction import ChatAction from .dice import Dice from .userprofilephotos import UserProfilePhotos from .keyboardbuttonpolltype import KeyboardButtonPollType from .keyboardbutton import KeyboardButton from .replymarkup import ReplyMarkup from .replykeyboardmarkup import ReplyKeyboardMarkup from .replykeyboardremove import ReplyKeyboardRemove from .forcereply import ForceReply from .error import TelegramError from .files.inputfile import InputFile from .files.file import File from .parsemode import ParseMode from .messageentity import MessageEntity from .messageid import MessageId from .games.game import Game from .poll import Poll, PollOption, PollAnswer from .voicechat import ( VoiceChatStarted, VoiceChatEnded, VoiceChatParticipantsInvited, VoiceChatScheduled, ) from .loginurl import LoginUrl from .proximityalerttriggered import ProximityAlertTriggered from .games.callbackgame import CallbackGame from .payment.shippingaddress import ShippingAddress from .payment.orderinfo import OrderInfo from .payment.successfulpayment import SuccessfulPayment from .payment.invoice import Invoice from .passport.credentials import EncryptedCredentials from .passport.passportfile import PassportFile from .passport.data import IdDocumentData, PersonalDetails, ResidentialAddress from .passport.encryptedpassportelement import EncryptedPassportElement from .passport.passportdata import PassportData from .inline.inlinekeyboardbutton import InlineKeyboardButton from .inline.inlinekeyboardmarkup import InlineKeyboardMarkup from .messageautodeletetimerchanged import MessageAutoDeleteTimerChanged from .message import Message from .callbackquery import CallbackQuery from .choseninlineresult import ChosenInlineResult from .inline.inputmessagecontent import InputMessageContent 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.inlinequeryresultgif import InlineQueryResultGif from .inline.inlinequeryresultlocation import InlineQueryResultLocation from .inline.inlinequeryresultmpeg4gif import InlineQueryResultMpeg4Gif from .inline.inlinequeryresultphoto import InlineQueryResultPhoto from .inline.inlinequeryresultvenue import InlineQueryResultVenue from .inline.inlinequeryresultvideo import InlineQueryResultVideo from .inline.inlinequeryresultvoice import InlineQueryResultVoice from .inline.inlinequeryresultgame import InlineQueryResultGame from .inline.inputtextmessagecontent import InputTextMessageContent from .inline.inputlocationmessagecontent import InputLocationMessageContent from .inline.inputvenuemessagecontent import InputVenueMessageContent from .payment.labeledprice import LabeledPrice from .inline.inputinvoicemessagecontent import InputInvoiceMessageContent from .inline.inputcontactmessagecontent import InputContactMessageContent from .payment.shippingoption import ShippingOption from .payment.precheckoutquery import PreCheckoutQuery from .payment.shippingquery import ShippingQuery from .webhookinfo import WebhookInfo from .games.gamehighscore import GameHighScore from .update import Update from .files.inputmedia import ( InputMedia, InputMediaVideo, InputMediaPhoto, InputMediaAnimation, InputMediaAudio, InputMediaDocument, ) from .constants import ( MAX_MESSAGE_LENGTH, MAX_CAPTION_LENGTH, SUPPORTED_WEBHOOK_PORTS, MAX_FILESIZE_DOWNLOAD, MAX_FILESIZE_UPLOAD, MAX_MESSAGES_PER_SECOND_PER_CHAT, MAX_MESSAGES_PER_SECOND, MAX_MESSAGES_PER_MINUTE_PER_GROUP, ) from .passport.passportelementerrors import ( PassportElementError, PassportElementErrorDataField, PassportElementErrorFile, PassportElementErrorFiles, PassportElementErrorFrontSide, PassportElementErrorReverseSide, PassportElementErrorSelfie, PassportElementErrorTranslationFile, PassportElementErrorTranslationFiles, PassportElementErrorUnspecified, ) from .passport.credentials import ( Credentials, DataCredentials, SecureData, SecureValue, FileCredentials, TelegramDecryptionError, ) from .botcommandscope import ( BotCommandScope, BotCommandScopeDefault, BotCommandScopeAllPrivateChats, BotCommandScopeAllGroupChats, BotCommandScopeAllChatAdministrators, BotCommandScopeChat, BotCommandScopeChatAdministrators, BotCommandScopeChatMember, ) from .bot import Bot from .version import __version__, bot_api_version # noqa: F401 __author__ = 'devs@python-telegram-bot.org' __all__ = ( # Keep this alphabetically ordered 'Animation', 'Audio', 'Bot', 'BotCommand', 'BotCommandScope', 'BotCommandScopeAllChatAdministrators', 'BotCommandScopeAllGroupChats', 'BotCommandScopeAllPrivateChats', 'BotCommandScopeChat', 'BotCommandScopeChatAdministrators', 'BotCommandScopeChatMember', 'BotCommandScopeDefault', 'CallbackGame', 'CallbackQuery', 'Chat', 'ChatAction', 'ChatInviteLink', 'ChatJoinRequest', 'ChatLocation', 'ChatMember', 'ChatMemberOwner', 'ChatMemberAdministrator', 'ChatMemberMember', 'ChatMemberRestricted', 'ChatMemberLeft', 'ChatMemberBanned', 'ChatMemberUpdated', 'ChatPermissions', 'ChatPhoto', 'ChosenInlineResult', 'Contact', 'Credentials', 'DataCredentials', 'Dice', 'Document', 'EncryptedCredentials', 'EncryptedPassportElement', 'File', 'FileCredentials', 'ForceReply', 'Game', 'GameHighScore', 'IdDocumentData', 'InlineKeyboardButton', 'InlineKeyboardMarkup', 'InlineQuery', 'InlineQueryResult', 'InlineQueryResultArticle', 'InlineQueryResultAudio', 'InlineQueryResultCachedAudio', 'InlineQueryResultCachedDocument', 'InlineQueryResultCachedGif', 'InlineQueryResultCachedMpeg4Gif', 'InlineQueryResultCachedPhoto', 'InlineQueryResultCachedSticker', 'InlineQueryResultCachedVideo', 'InlineQueryResultCachedVoice', 'InlineQueryResultContact', 'InlineQueryResultDocument', 'InlineQueryResultGame', 'InlineQueryResultGif', 'InlineQueryResultLocation', 'InlineQueryResultMpeg4Gif', 'InlineQueryResultPhoto', 'InlineQueryResultVenue', 'InlineQueryResultVideo', 'InlineQueryResultVoice', 'InputContactMessageContent', 'InputFile', 'InputInvoiceMessageContent', 'InputLocationMessageContent', 'InputMedia', 'InputMediaAnimation', 'InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo', 'InputMessageContent', 'InputTextMessageContent', 'InputVenueMessageContent', 'Invoice', 'KeyboardButton', 'KeyboardButtonPollType', 'LabeledPrice', 'Location', 'LoginUrl', 'MAX_CAPTION_LENGTH', 'MAX_FILESIZE_DOWNLOAD', 'MAX_FILESIZE_UPLOAD', 'MAX_MESSAGES_PER_MINUTE_PER_GROUP', 'MAX_MESSAGES_PER_SECOND', 'MAX_MESSAGES_PER_SECOND_PER_CHAT', 'MAX_MESSAGE_LENGTH', 'MaskPosition', 'Message', 'MessageAutoDeleteTimerChanged', 'MessageEntity', 'MessageId', 'OrderInfo', 'ParseMode', 'PassportData', 'PassportElementError', 'PassportElementErrorDataField', 'PassportElementErrorFile', 'PassportElementErrorFiles', 'PassportElementErrorFrontSide', 'PassportElementErrorReverseSide', 'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile', 'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified', 'PassportFile', 'PersonalDetails', 'PhotoSize', 'Poll', 'PollAnswer', 'PollOption', 'PreCheckoutQuery', 'ProximityAlertTriggered', 'ReplyKeyboardMarkup', 'ReplyKeyboardRemove', 'ReplyMarkup', 'ResidentialAddress', 'SUPPORTED_WEBHOOK_PORTS', 'SecureData', 'SecureValue', 'ShippingAddress', 'ShippingOption', 'ShippingQuery', 'Sticker', 'StickerSet', 'SuccessfulPayment', 'TelegramDecryptionError', 'TelegramError', 'TelegramObject', 'Update', 'User', 'UserProfilePhotos', 'Venue', 'Video', 'VideoNote', 'Voice', 'VoiceChatStarted', 'VoiceChatEnded', 'VoiceChatScheduled', 'VoiceChatParticipantsInvited', 'WebhookInfo', ) python-telegram-bot-13.11/telegram/__main__.py000066400000000000000000000033631417656324400213330ustar00rootroot00000000000000# !/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=C0114 import subprocess import sys from typing import Optional import certifi from . import __version__ as telegram_ver from .constants import BOT_API_VERSION def _git_revision() -> Optional[str]: try: output = subprocess.check_output( # skipcq: BAN-B607 ["git", "describe", "--long", "--tags"], stderr=subprocess.STDOUT ) except (subprocess.SubprocessError, OSError): return None return output.decode().strip() def print_ver_info() -> None: # skipcq: PY-D0003 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}') print(f'certifi {certifi.__version__}') # type: ignore[attr-defined] sys_version = sys.version.replace('\n', ' ') print(f'Python {sys_version}') def main() -> None: # skipcq: PY-D0003 print_ver_info() if __name__ == '__main__': main() python-telegram-bot-13.11/telegram/base.py000066400000000000000000000120341417656324400205200ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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.""" try: import ujson as json except ImportError: import json # type: ignore[no-redef] import warnings from typing import TYPE_CHECKING, List, Optional, Tuple, Type, TypeVar from telegram.utils.types import JSONDict from telegram.utils.deprecate import set_new_attribute_deprecated if TYPE_CHECKING: from telegram import Bot TO = TypeVar('TO', bound='TelegramObject', covariant=True) class TelegramObject: """Base class for most Telegram objects.""" _id_attrs: Tuple[object, ...] = () # Adding slots reduces memory usage & allows for faster attribute access. # Only instance variables should be added to __slots__. # We add __dict__ here for backward compatibility & also to avoid repetition for subclasses. __slots__ = ('__dict__',) def __str__(self) -> str: return str(self.to_dict()) def __getitem__(self, item: str) -> object: return getattr(self, item, None) def __setattr__(self, key: str, value: object) -> None: set_new_attribute_deprecated(self, key, value) @staticmethod def _parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]: return None if data is None else data.copy() @classmethod def de_json(cls: Type[TO], data: Optional[JSONDict], bot: 'Bot') -> Optional[TO]: """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. """ data = cls._parse_data(data) if data is None: return None if cls == TelegramObject: return cls() return cls(bot=bot, **data) # type: ignore[call-arg] @classmethod def de_list(cls: Type[TO], data: Optional[List[JSONDict]], bot: 'Bot') -> List[Optional[TO]]: """Converts JSON data to a list of Telegram objects. Args: data (Dict[:obj:`str`, ...]): The JSON data. bot (:class:`telegram.Bot`): The bot associated with these objects. Returns: A list of Telegram objects. """ if not data: return [] return [cls.de_json(d, bot) for d in data] def to_json(self) -> str: """Gives a JSON representation of object. Returns: :obj:`str` """ return json.dumps(self.to_dict()) def to_dict(self) -> JSONDict: """Gives representation of object as :obj:`dict`. Returns: :obj:`dict` """ data = {} # 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 `[:-2]` slice excludes the `object` class & the # TelegramObject class itself. attrs = {attr for cls in self.__class__.__mro__[:-2] for attr in cls.__slots__} for key in attrs: if key == 'bot' or key.startswith('_'): continue value = getattr(self, key, None) if value is not None: if hasattr(value, 'to_dict'): data[key] = value.to_dict() else: data[key] = value if data.get('from_user'): data['from'] = data.pop('from_user', None) return data def __eq__(self, other: object) -> bool: if isinstance(other, self.__class__): if self._id_attrs == (): warnings.warn( f"Objects of type {self.__class__.__name__} can not be meaningfully tested for" " equivalence." ) if other._id_attrs == (): warnings.warn( f"Objects of type {other.__class__.__name__} can not be meaningfully tested" " for equivalence." ) return self._id_attrs == other._id_attrs return super().__eq__(other) # pylint: disable=no-member def __hash__(self) -> int: if self._id_attrs: return hash((self.__class__, self._id_attrs)) # pylint: disable=no-member return super().__hash__() python-telegram-bot-13.11/telegram/bot.py000066400000000000000000007757221417656324400204160ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=E0611,E0213,E1102,E1101,R0913,R0904 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 functools import logging import warnings from datetime import datetime from typing import ( TYPE_CHECKING, Callable, List, Optional, Tuple, TypeVar, Union, no_type_check, Dict, cast, Sequence, ) try: import ujson as json except ImportError: import json # type: ignore[no-redef] # noqa: F723 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 import ( Animation, Audio, BotCommand, BotCommandScope, Chat, ChatMember, ChatPermissions, ChatPhoto, Contact, Document, File, GameHighScore, Location, MaskPosition, Message, MessageId, PassportElementError, PhotoSize, Poll, ReplyMarkup, ShippingOption, Sticker, StickerSet, TelegramObject, Update, User, UserProfilePhotos, Venue, Video, VideoNote, Voice, WebhookInfo, InlineKeyboardMarkup, ChatInviteLink, ) from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.error import InvalidToken, TelegramError from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import ( DEFAULT_NONE, DefaultValue, to_timestamp, is_local_file, parse_file_input, DEFAULT_20, ) from telegram.utils.request import Request from telegram.utils.types import FileInput, JSONDict, ODVInput, DVInput if TYPE_CHECKING: from telegram.ext import Defaults from telegram import ( InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo, InputMedia, InlineQueryResult, LabeledPrice, MessageEntity, ) RT = TypeVar('RT') def log( # skipcq: PY-D0003 func: Callable[..., RT], *args: object, **kwargs: object # pylint: disable=W0613 ) -> Callable[..., RT]: logger = logging.getLogger(func.__module__) @functools.wraps(func) def decorator(*args: object, **kwargs: object) -> RT: # pylint: disable=W0613 logger.debug('Entering: %s', func.__name__) result = func(*args, **kwargs) logger.debug(result) logger.debug('Exiting: %s', func.__name__) return result return decorator class Bot(TelegramObject): """This object represents a Telegram Bot. .. versionadded:: 13.2 Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`bot` is equal. Note: Most bot methods have the argument ``api_kwargs`` which allows to pass arbitrary keywords to the Telegram API. This can be used to access new features of the API before they were incorporated into PTB. However, this is not guaranteed to work, i.e. it will fail for passing files. Args: token (:obj:`str`): Bot's unique authentication. base_url (:obj:`str`, optional): Telegram Bot API service URL. base_file_url (:obj:`str`, optional): Telegram Bot API file URL. request (:obj:`telegram.utils.request.Request`, optional): Pre initialized :obj:`telegram.utils.request.Request`. private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data. private_key_password (:obj:`bytes`, optional): Password for above private key. defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to be used if not set explicitly in the bot methods. .. deprecated:: 13.6 Passing :class:`telegram.ext.Defaults` to :class:`telegram.Bot` is deprecated. If you want to use :class:`telegram.ext.Defaults`, please use :class:`telegram.ext.ExtBot` instead. """ __slots__ = ( 'token', 'base_url', 'base_file_url', 'private_key', 'defaults', '_bot', '_commands', '_request', 'logger', ) def __init__( self, token: str, base_url: str = None, base_file_url: str = None, request: 'Request' = None, private_key: bytes = None, private_key_password: bytes = None, defaults: 'Defaults' = None, ): self.token = self._validate_token(token) # Gather default self.defaults = defaults if self.defaults: warnings.warn( 'Passing Defaults to telegram.Bot is deprecated. Use telegram.ext.ExtBot instead.', TelegramDeprecationWarning, stacklevel=3, ) if base_url is None: base_url = 'https://api.telegram.org/bot' if base_file_url is None: base_file_url = 'https://api.telegram.org/file/bot' self.base_url = str(base_url) + str(self.token) self.base_file_url = str(base_file_url) + str(self.token) self._bot: Optional[User] = None self._commands: Optional[List[BotCommand]] = None self._request = request or Request() self.private_key = None self.logger = logging.getLogger(__name__) 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() ) # The ext_bot argument is a little hack to get warnings handled correctly. # It's not very clean, but the warnings will be dropped at some point anyway. def __setattr__(self, key: str, value: object, ext_bot: bool = False) -> None: if issubclass(self.__class__, Bot) and self.__class__ is not Bot and not ext_bot: object.__setattr__(self, key, value) return super().__setattr__(key, value) def _insert_defaults( self, data: Dict[str, object], timeout: ODVInput[float] ) -> Optional[float]: """ 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! """ effective_timeout = DefaultValue.get_value(timeout) # If we have no Defaults, we just need to replace DefaultValue instances # with the actual value if not self.defaults: data.update((key, DefaultValue.get_value(value)) for key, value in data.items()) return effective_timeout # if we have Defaults, we replace all DefaultValue instances with the relevant # Defaults value. If there is none, we fall back to the default value of the bot method for key, val in data.items(): if isinstance(val, DefaultValue): data[key] = self.defaults.api_defaults.get(key, val.value) if isinstance(timeout, DefaultValue): # If we get here, we use Defaults.timeout, unless that's not set, which is the # case if isinstance(self.defaults.timeout, DefaultValue) return ( self.defaults.timeout if not isinstance(self.defaults.timeout, DefaultValue) else effective_timeout ) return effective_timeout def _post( self, endpoint: str, data: JSONDict = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Union[bool, JSONDict, None]: if data is None: data = {} if api_kwargs: if data: data.update(api_kwargs) else: data = api_kwargs # Insert is in-place, so no return value for data if endpoint != 'getUpdates': effective_timeout = self._insert_defaults(data, timeout) else: effective_timeout = cast(float, timeout) # 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 self.request.post( f'{self.base_url}/{endpoint}', data=data, timeout=effective_timeout ) def _message( self, endpoint: str, data: JSONDict, reply_to_message_id: int = None, disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, ) -> Union[bool, Message]: if reply_to_message_id is not None: data['reply_to_message_id'] = reply_to_message_id if protect_content: data['protect_content'] = protect_content # We don't check if (DEFAULT_)None here, so that _put is able to insert the defaults # correctly, if necessary data['disable_notification'] = disable_notification data['allow_sending_without_reply'] = allow_sending_without_reply if reply_markup is not None: if isinstance(reply_markup, ReplyMarkup): # We need to_json() instead of to_dict() here, because reply_markups may be # attached to media messages, which aren't json dumped by utils.request data['reply_markup'] = reply_markup.to_json() else: data['reply_markup'] = reply_markup if data.get('media') and (data['media'].parse_mode == DEFAULT_NONE): if self.defaults: data['media'].parse_mode = DefaultValue.get_value(self.defaults.parse_mode) else: data['media'].parse_mode = None result = self._post(endpoint, data, timeout=timeout, api_kwargs=api_kwargs) if result is True: return result return Message.de_json(result, self) # type: ignore[return-value, arg-type] @property def request(self) -> Request: # skip-cq: PY-D0003 return self._request @staticmethod def _validate_token(token: str) -> str: """A very basic validation on token.""" if any(x.isspace() for x in token): raise InvalidToken() left, sep, _right = token.partition(':') if (not sep) or (not left.isdigit()) or (len(left) < 3): raise InvalidToken() return token @property def bot(self) -> User: """:class:`telegram.User`: User instance for the bot as returned by :meth:`get_me`.""" if self._bot is None: self._bot = self.get_me() return self._bot @property def id(self) -> int: # pylint: disable=C0103 """:obj:`int`: Unique identifier for this bot.""" return self.bot.id @property def first_name(self) -> str: """:obj:`str`: Bot's first name.""" return self.bot.first_name @property def last_name(self) -> str: """:obj:`str`: Optional. Bot's last name.""" return self.bot.last_name # type: ignore @property def username(self) -> str: """:obj:`str`: Bot's username.""" 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.""" 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.""" 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.""" return self.bot.supports_inline_queries # type: ignore @property def commands(self) -> List[BotCommand]: """ List[:class:`BotCommand`]: Bot's commands as available in the default scope. .. deprecated:: 13.7 This property has been deprecated since there can be different commands available for different scopes. """ warnings.warn( "Bot.commands has been deprecated since there can be different command " "lists for different scopes.", TelegramDeprecationWarning, stacklevel=2, ) if self._commands is None: self._commands = self.get_my_commands() return self._commands @property def name(self) -> str: """:obj:`str`: Bot's @username.""" return f'@{self.username}' @log def get_me(self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None) -> User: """A simple method for testing your bot's auth token. Requires no parameters. Args: timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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 = self._post('getMe', timeout=timeout, api_kwargs=api_kwargs) self._bot = User.de_json(result, self) # type: ignore[return-value, arg-type] return self._bot # type: ignore[return-value] @log def send_message( self, chat_id: Union[int, str], text: str, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, ) -> Message: """Use this method to send text messages. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). text (:obj:`str`): Text of the message to be sent. Max 4096 characters after entities parsing. Also found as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`. parse_mode (:obj:`str`): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. See the constants in :class:`telegram.ParseMode` for the available modes. entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in message text, which can be specified instead of :attr:`parse_mode`. disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this message. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of sent messages from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent message is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = { 'chat_id': chat_id, 'text': text, 'parse_mode': parse_mode, 'disable_web_page_preview': disable_web_page_preview, } if entities: data['entities'] = [me.to_dict() for me in entities] return self._message( # type: ignore[return-value] 'sendMessage', data, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def delete_message( self, chat_id: Union[str, int], message_id: int, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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. - 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.ChatMember.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.ChatMember.can_delete_messages` permission in a supergroup or a channel, it can delete any message there. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). message_id (:obj:`int`): Identifier of the message to delete. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'message_id': message_id} result = self._post('deleteMessage', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def forward_message( self, chat_id: Union[int, str], from_chat_id: Union[str, int], message_id: int, disable_notification: DVInput[bool] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = 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`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). 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. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {} if chat_id: data['chat_id'] = chat_id if from_chat_id: data['from_chat_id'] = from_chat_id if message_id: data['message_id'] = message_id return self._message( # type: ignore[return-value] 'forwardMessage', data, disable_notification=disable_notification, timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_photo( self, chat_id: Union[int, str], photo: Union[FileInput, 'PhotoSize'], caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, ) -> Message: """Use this method to send photos. Note: The photo argument can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). photo (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.PhotoSize`): Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. 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): Photo caption (may also be used when resending photos by file_id), 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in message text, which can be specified instead of :attr:`parse_mode`. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = { 'chat_id': chat_id, 'photo': parse_file_input(photo, PhotoSize, filename=filename), 'parse_mode': parse_mode, } if caption: data['caption'] = caption if caption_entities: data['caption_entities'] = [me.to_dict() for me in caption_entities] return self._message( # type: ignore[return-value] 'sendPhoto', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_audio( self, chat_id: Union[int, str], audio: Union[FileInput, 'Audio'], duration: int = None, performer: str = None, title: str = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = 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 50 MB in size, this limit may be changed in the future. For sending voice messages, use the :meth:`send_voice` method instead. Note: The audio argument can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). audio (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Audio`): Audio file to send. Pass a file_id as String to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. 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): Audio caption, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in message text, which can be specified instead of :attr:`parse_mode`. 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): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): 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. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = { 'chat_id': chat_id, 'audio': parse_file_input(audio, Audio, filename=filename), 'parse_mode': parse_mode, } if duration: data['duration'] = duration if performer: data['performer'] = performer if title: data['title'] = title if caption: data['caption'] = caption if caption_entities: data['caption_entities'] = [me.to_dict() for me in caption_entities] if thumb: data['thumb'] = parse_file_input(thumb, attach=True) return self._message( # type: ignore[return-value] 'sendAudio', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_document( self, chat_id: Union[int, str], document: Union[FileInput, 'Document'], filename: str = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb: FileInput = None, api_kwargs: JSONDict = None, disable_content_type_detection: bool = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, ) -> Message: """ Use this method to send general files. Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. Note: The document argument can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). document (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Document`): File to send. 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 using multipart/form-data. 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. caption (:obj:`str`, optional): Document caption (may also be used when resending documents by file_id), 0-1024 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): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in message text, which can be specified instead of :attr:`parse_mode`. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): 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. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = { 'chat_id': chat_id, 'document': parse_file_input(document, Document, filename=filename), 'parse_mode': parse_mode, } if caption: data['caption'] = caption if caption_entities: data['caption_entities'] = [me.to_dict() for me in caption_entities] if disable_content_type_detection is not None: data['disable_content_type_detection'] = disable_content_type_detection if thumb: data['thumb'] = parse_file_input(thumb, attach=True) return self._message( # type: ignore[return-value] 'sendDocument', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_sticker( self, chat_id: Union[int, str], sticker: Union[FileInput, 'Sticker'], disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> Message: """ Use this method to send static ``.WEBP``, animated ``.TGS``, or video ``.WEBM`` stickers. Note: The sticker argument can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Sticker`): Sticker to send. 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 .webp file from the Internet, or upload a new one using multipart/form-data. Lastly you can pass an existing :class:`telegram.Sticker` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'sticker': parse_file_input(sticker, Sticker)} return self._message( # type: ignore[return-value] 'sendSticker', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_video( self, chat_id: Union[int, str], video: Union[FileInput, 'Video'], duration: int = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, width: int = None, height: int = None, parse_mode: ODVInput[str] = DEFAULT_NONE, supports_streaming: bool = None, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = 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 50 MB in size, this limit may be changed in the future. Note: * The video argument can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` * ``thumb`` will be ignored for small video files, for which Telegram can easily generate thumb nails. However, this behaviour is undocumented and might be changed by Telegram. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). video (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Video`): Video file to send. Pass a file_id as String to send an video file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an video file from the Internet, or upload a new one using multipart/form-data. 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 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in message text, which can be specified instead of :attr:`parse_mode`. supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is suitable for streaming. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): 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. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = { 'chat_id': chat_id, 'video': parse_file_input(video, Video, filename=filename), 'parse_mode': parse_mode, } if duration: data['duration'] = duration if caption: data['caption'] = caption if caption_entities: data['caption_entities'] = [me.to_dict() for me in caption_entities] if supports_streaming: data['supports_streaming'] = supports_streaming if width: data['width'] = width if height: data['height'] = height if thumb: data['thumb'] = parse_file_input(thumb, attach=True) return self._message( # type: ignore[return-value] 'sendVideo', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_video_note( self, chat_id: Union[int, str], video_note: Union[FileInput, 'VideoNote'], duration: int = None, length: int = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: str = None, protect_content: bool = 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: * The video_note argument can be either a file_id or a file from disk ``open(filename, 'rb')`` * ``thumb`` will be ignored for small video files, for which Telegram can easily generate thumb nails. However, this behaviour is undocumented and might be changed by Telegram. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). video_note (:obj:`str` | `filelike 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. Or 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. 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 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): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): 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. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = { 'chat_id': chat_id, 'video_note': parse_file_input(video_note, VideoNote, filename=filename), } if duration is not None: data['duration'] = duration if length is not None: data['length'] = length if thumb: data['thumb'] = parse_file_input(thumb, attach=True) return self._message( # type: ignore[return-value] 'sendVideoNote', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_animation( self, chat_id: Union[int, str], animation: Union[FileInput, 'Animation'], duration: int = None, width: int = None, height: int = None, thumb: FileInput = None, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = 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 50 MB in size, this limit may be changed in the future. Note: ``thumb`` will be ignored for small files, for which Telegram can easily generate thumb nails. However, this behaviour is undocumented and might be changed by Telegram. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). animation (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Animation`): Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data. 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 duration (:obj:`int`, optional): Duration of sent animation in seconds. width (:obj:`int`, optional): Animation width. height (:obj:`int`, optional): Animation height. thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): 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. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. caption (:obj:`str`, optional): Animation caption (may also be used when resending animations by file_id), 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in message text, which can be specified instead of :attr:`parse_mode`. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = { 'chat_id': chat_id, 'animation': parse_file_input(animation, Animation, filename=filename), 'parse_mode': parse_mode, } if duration: data['duration'] = duration if width: data['width'] = width if height: data['height'] = height if thumb: data['thumb'] = parse_file_input(thumb, attach=True) if caption: data['caption'] = caption if caption_entities: data['caption_entities'] = [me.to_dict() for me in caption_entities] return self._message( # type: ignore[return-value] 'sendAnimation', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_voice( self, chat_id: Union[int, str], voice: Union[FileInput, 'Voice'], duration: int = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = 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 50 MB in size, this limit may be changed in the future. Note: The voice argument can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). voice (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Voice`): Voice file to send. Pass a file_id as String to send an voice file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an voice file from the Internet, or upload a new one using multipart/form-data. Lastly you can pass an existing :class:`telegram.Voice` object to send. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. 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 caption (:obj:`str`, optional): Voice message caption, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in message text, which can be specified instead of :attr:`parse_mode`. duration (:obj:`int`, optional): Duration of the voice message in seconds. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = { 'chat_id': chat_id, 'voice': parse_file_input(voice, Voice, filename=filename), 'parse_mode': parse_mode, } if duration: data['duration'] = duration if caption: data['caption'] = caption if caption_entities: data['caption_entities'] = [me.to_dict() for me in caption_entities] return self._message( # type: ignore[return-value] 'sendVoice', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_media_group( self, chat_id: Union[int, str], media: List[ Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo'] ], disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> List[Message]: """Use this method to send a group of photos or videos as an album. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). media (List[:class:`telegram.InputMediaAudio`, :class:`telegram.InputMediaDocument`, \ :class:`telegram.InputMediaPhoto`, :class:`telegram.InputMediaVideo`]): An array describing messages to be sent, must include 2–10 items. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: List[:class:`telegram.Message`]: An array of the sent Messages. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = { 'chat_id': chat_id, 'media': media, 'disable_notification': disable_notification, 'allow_sending_without_reply': allow_sending_without_reply, } for med in data['media']: if med.parse_mode == DEFAULT_NONE: if self.defaults: med.parse_mode = DefaultValue.get_value(self.defaults.parse_mode) else: med.parse_mode = None if reply_to_message_id: data['reply_to_message_id'] = reply_to_message_id if protect_content: data['protect_content'] = protect_content result = self._post('sendMediaGroup', data, timeout=timeout, api_kwargs=api_kwargs) return Message.de_list(result, self) # type: ignore @log def send_location( self, chat_id: Union[int, str], latitude: float = None, longitude: float = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, location: Location = None, live_period: int = None, api_kwargs: JSONDict = None, horizontal_accuracy: float = None, heading: int = None, proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> Message: """Use this method to send point on the map. Note: You can either supply a :obj:`latitude` and :obj:`longitude` or a :obj:`location`. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). latitude (:obj:`float`, optional): Latitude of location. longitude (:obj:`float`, optional): Longitude of location. location (:class:`telegram.Location`, optional): The location to send. horizontal_accuracy (:obj:`int`, optional): The radius of uncertainty for the location, measured in meters; 0-1500. live_period (:obj:`int`, optional): Period in seconds for which the location will be updated, should be between 60 and 86400. heading (:obj:`int`, optional): For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 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 1 and 100000 if specified. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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} if live_period: data['live_period'] = live_period if horizontal_accuracy: data['horizontal_accuracy'] = horizontal_accuracy if heading: data['heading'] = heading if proximity_alert_radius: data['proximity_alert_radius'] = proximity_alert_radius return self._message( # type: ignore[return-value] 'sendLocation', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def edit_message_live_location( self, chat_id: Union[str, int] = None, message_id: int = None, inline_message_id: int = None, latitude: float = None, longitude: float = None, location: Location = None, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, horizontal_accuracy: float = None, heading: int = None, proximity_alert_radius: int = 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 :obj:`latitude` and :obj:`longitude` or a :obj:`location`. Args: chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). 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. latitude (:obj:`float`, optional): Latitude of location. longitude (:obj:`float`, optional): Longitude of location. location (:class:`telegram.Location`, optional): The location to send. horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location, measured in meters; 0-1500. heading (:obj:`int`, optional): Direction in which the user is moving, in degrees. Must be between 1 and 360 if specified. proximity_alert_radius (:obj:`int`, optional): Maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized object for a new inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, if edited message is not an inline message, the edited message is returned, otherwise :obj:`True` is returned. """ 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} if chat_id: data['chat_id'] = chat_id if message_id: data['message_id'] = message_id if inline_message_id: data['inline_message_id'] = inline_message_id if horizontal_accuracy: data['horizontal_accuracy'] = horizontal_accuracy if heading: data['heading'] = heading if proximity_alert_radius: data['proximity_alert_radius'] = proximity_alert_radius return self._message( 'editMessageLiveLocation', data, timeout=timeout, reply_markup=reply_markup, api_kwargs=api_kwargs, ) @log def stop_message_live_location( self, chat_id: Union[str, int] = None, message_id: int = None, inline_message_id: int = None, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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 live_period expires. Args: chat_id (:obj:`int` | :obj:`str`): Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). message_id (:obj:`int`, optional): Required if inline_message_id is not specified. Identifier of the sent message with live location to stop. 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): A JSON-serialized object for a new inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the sent Message is returned, otherwise :obj:`True` is returned. """ data: JSONDict = {} if chat_id: data['chat_id'] = chat_id if message_id: data['message_id'] = message_id if inline_message_id: data['inline_message_id'] = inline_message_id return self._message( 'stopMessageLiveLocation', data, timeout=timeout, reply_markup=reply_markup, api_kwargs=api_kwargs, ) @log def send_venue( self, chat_id: Union[int, str], latitude: float = None, longitude: float = None, title: str = None, address: str = None, foursquare_id: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, venue: Venue = None, foursquare_type: str = None, api_kwargs: JSONDict = None, google_place_id: str = None, google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> Message: """Use this method to send information about a venue. Note: * You can either supply :obj:`venue`, or :obj:`latitude`, :obj:`longitude`, :obj:`title` and :obj:`address` and optionally :obj:`foursquare_id` and :obj:`foursquare_type` or optionally :obj:`google_place_id` and :obj:`google_place_type`. * Foursquare details and Google Pace details are mutually exclusive. However, this behaviour is undocumented and might be changed by Telegram. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). 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 \ `_.) venue (:class:`telegram.Venue`, optional): The venue to send. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ 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 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, } if foursquare_id: data['foursquare_id'] = foursquare_id if foursquare_type: data['foursquare_type'] = foursquare_type if google_place_id: data['google_place_id'] = google_place_id if google_place_type: data['google_place_type'] = google_place_type return self._message( # type: ignore[return-value] 'sendVenue', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_contact( self, chat_id: Union[int, str], phone_number: str = None, first_name: str = None, last_name: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, contact: Contact = None, vcard: str = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> Message: """Use this method to send phone contacts. Note: You can either supply :obj:`contact` or :obj:`phone_number` and :obj:`first_name` with optionally :obj:`last_name` and optionally :obj:`vcard`. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). 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-2048 bytes. contact (:class:`telegram.Contact`, optional): The contact to send. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ 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 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, } if last_name: data['last_name'] = last_name if vcard: data['vcard'] = vcard return self._message( # type: ignore[return-value] 'sendContact', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_game( self, chat_id: Union[int, str], game_short_name: str, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> Message: """Use this method to send a game. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat. game_short_name (:obj:`str`): Short name of the game, serves as the unique identifier for the game. Set up your games via `@BotFather `_. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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 self._message( # type: ignore[return-value] 'sendGame', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def send_chat_action( self, chat_id: Union[str, int], action: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). action(:class:`telegram.ChatAction` | :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.ChatAction` timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'action': action} result = self._post('sendChatAction', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] def _effective_inline_results( # pylint: disable=R0201 self, results: Union[ Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]] ], next_offset: str = None, current_offset: 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 if current_offset == '': current_offset_int = 0 else: current_offset_int = 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) else: if len(results) > (current_offset_int + 1) * MAX_INLINE_QUERY_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 * MAX_INLINE_QUERY_RESULTS : next_offset_int * MAX_INLINE_QUERY_RESULTS ] else: effective_results = results[current_offset_int * MAX_INLINE_QUERY_RESULTS :] else: effective_results = results # type: ignore[assignment] return effective_results, next_offset @log def answer_inline_query( self, inline_query_id: str, results: Union[ Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]] ], cache_time: int = 300, is_personal: bool = None, next_offset: str = None, switch_pm_text: str = None, switch_pm_parameter: str = None, timeout: ODVInput[float] = DEFAULT_NONE, current_offset: str = None, api_kwargs: JSONDict = None, ) -> bool: """ Use this method to send answers to an inline query. No more than 50 results per query are allowed. Warning: In most use cases :attr:`current_offset` should not be passed manually. Instead of calling this method directly, use the shortcut :meth:`telegram.InlineQuery.answer` with ``auto_pagination=True``, which will take care of passing the correct value. 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 :attr:`current_offset` is passed, ``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 64 bytes. switch_pm_text (:obj:`str`, optional): If passed, clients will display a button with specified text that switches the user to a private chat with the bot and sends the bot a start message with the parameter ``switch_pm_parameter``. switch_pm_parameter (:obj:`str`, optional): Deep-linking parameter for the /start message sent to the bot when user presses the switch button. 1-64 characters, only A-Z, a-z, 0-9, _ and - are allowed. 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 ``next_offset`` and truncate the results list/get the results from the callable you passed. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ @no_type_check def _set_defaults(res): # pylint: disable=W0212 if hasattr(res, 'parse_mode') and res.parse_mode == DEFAULT_NONE: if self.defaults: res.parse_mode = self.defaults.parse_mode else: res.parse_mode = None 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 == DEFAULT_NONE ): if self.defaults: res.input_message_content.parse_mode = DefaultValue.get_value( self.defaults.parse_mode ) else: res.input_message_content.parse_mode = None if ( hasattr(res.input_message_content, 'disable_web_page_preview') and res.input_message_content.disable_web_page_preview == DEFAULT_NONE ): if self.defaults: res.input_message_content.disable_web_page_preview = ( DefaultValue.get_value(self.defaults.disable_web_page_preview) ) else: res.input_message_content.disable_web_page_preview = None effective_results, next_offset = self._effective_inline_results( results=results, next_offset=next_offset, current_offset=current_offset ) # Apply defaults for result in effective_results: _set_defaults(result) results_dicts = [res.to_dict() for res in effective_results] data: JSONDict = {'inline_query_id': inline_query_id, 'results': results_dicts} if cache_time or cache_time == 0: data['cache_time'] = cache_time if is_personal: data['is_personal'] = is_personal if next_offset is not None: data['next_offset'] = next_offset if switch_pm_text: data['switch_pm_text'] = switch_pm_text if switch_pm_parameter: data['switch_pm_parameter'] = switch_pm_parameter return self._post( # type: ignore[return-value] 'answerInlineQuery', data, timeout=timeout, api_kwargs=api_kwargs, ) @log def get_user_profile_photos( self, user_id: Union[str, int], offset: int = None, limit: int = 100, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Optional[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 1-100 are accepted. Defaults to ``100``. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.UserProfilePhotos` Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'user_id': user_id} if offset is not None: data['offset'] = offset if limit: data['limit'] = limit result = self._post('getUserProfilePhotos', data, timeout=timeout, api_kwargs=api_kwargs) return UserProfilePhotos.de_json(result, self) # type: ignore[return-value, arg-type] @log def get_file( self, file_id: Union[ str, Animation, Audio, ChatPhoto, Document, PhotoSize, Sticker, Video, VideoNote, Voice ], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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 20MB in size. The file can then be downloaded with :meth:`telegram.File.download`. 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. 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.File` Raises: :class:`telegram.error.TelegramError` """ try: file_id = file_id.file_id # type: ignore[union-attr] except AttributeError: pass data: JSONDict = {'file_id': file_id} result = self._post('getFile', data, timeout=timeout, api_kwargs=api_kwargs) if result.get('file_path') and not is_local_file( # type: ignore[union-attr] result['file_path'] # type: ignore[index] ): result['file_path'] = '{}/{}'.format( # type: ignore[index] self.base_file_url, result['file_path'] # type: ignore[index] ) return File.de_json(result, self) # type: ignore[return-value, arg-type] @log def kick_chat_member( self, chat_id: Union[str, int], user_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, until_date: Union[int, datetime] = None, api_kwargs: JSONDict = None, revoke_messages: bool = None, ) -> bool: """ Deprecated, use :func:`~telegram.Bot.ban_chat_member` instead. .. deprecated:: 13.7 """ warnings.warn( '`bot.kick_chat_member` is deprecated. Use `bot.ban_chat_member` instead.', TelegramDeprecationWarning, stacklevel=2, ) return self.ban_chat_member( chat_id=chat_id, user_id=user_id, timeout=timeout, until_date=until_date, api_kwargs=api_kwargs, revoke_messages=revoke_messages, ) @log def ban_chat_member( self, chat_id: Union[str, int], user_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, until_date: Union[int, datetime] = None, api_kwargs: JSONDict = None, revoke_messages: bool = 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). 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. 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 api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} if until_date is not None: if isinstance(until_date, datetime): until_date = to_timestamp( until_date, tzinfo=self.defaults.tzinfo if self.defaults else None ) data['until_date'] = until_date if revoke_messages is not None: data['revoke_messages'] = revoke_messages result = self._post('banChatMember', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def ban_chat_sender_chat( self, chat_id: Union[str, int], sender_chat_id: int, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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} result = self._post('banChatSenderChat', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def unban_chat_member( self, chat_id: Union[str, int], user_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, only_if_banned: bool = 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 :attr:`only_if_banned`. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target supergroup or channel (in the format ``@channelusername``). user_id (:obj:`int`): Unique identifier of the target user. only_if_banned (:obj:`bool`, optional): Do nothing if the user is not banned. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} if only_if_banned is not None: data['only_if_banned'] = only_if_banned result = self._post('unbanChatMember', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def unban_chat_sender_chat( self, chat_id: Union[str, int], sender_chat_id: int, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target supergroup or channel (in the format ``@channelusername``). sender_chat_id (:obj:`int`): Unique identifier of the target sender chat. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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} result = self._post('unbanChatSenderChat', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def answer_callback_query( self, callback_query_id: str, text: str = None, show_alert: bool = False, url: str = None, cache_time: int = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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-200 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool` On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'callback_query_id': callback_query_id} if text: data['text'] = text if show_alert: data['show_alert'] = show_alert if url: data['url'] = url if cache_time is not None: data['cache_time'] = cache_time result = self._post('answerCallbackQuery', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def edit_message_text( self, text: str, chat_id: Union[str, int] = None, message_id: int = None, inline_message_id: int = None, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, ) -> Union[Message, bool]: """ Use this method to edit text and game messages. Args: chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``) 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. text (:obj:`str`): New text of the message, 1-4096 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. See the constants in :class:`telegram.ParseMode` for the available modes. entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in message text, which can be specified instead of :attr:`parse_mode`. disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this message. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized object for an inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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 = { 'text': text, 'parse_mode': parse_mode, 'disable_web_page_preview': disable_web_page_preview, } if chat_id: data['chat_id'] = chat_id if message_id: data['message_id'] = message_id if inline_message_id: data['inline_message_id'] = inline_message_id if entities: data['entities'] = [me.to_dict() for me in entities] return self._message( 'editMessageText', data, timeout=timeout, reply_markup=reply_markup, api_kwargs=api_kwargs, ) @log def edit_message_caption( self, chat_id: Union[str, int] = None, message_id: int = None, inline_message_id: int = None, caption: str = None, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, parse_mode: ODVInput[str] = DEFAULT_NONE, api_kwargs: JSONDict = None, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, ) -> Union[Message, bool]: """ Use this method to edit captions of messages. Args: chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``) 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in message text, which can be specified instead of :attr:`parse_mode`. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized object for an inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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` """ if inline_message_id is None and (chat_id is None or message_id is None): raise ValueError( 'edit_message_caption: Both chat_id and message_id are required when ' 'inline_message_id is not specified' ) data: JSONDict = {'parse_mode': parse_mode} if caption: data['caption'] = caption if caption_entities: data['caption_entities'] = [me.to_dict() for me in caption_entities] if chat_id: data['chat_id'] = chat_id if message_id: data['message_id'] = message_id if inline_message_id: data['inline_message_id'] = inline_message_id return self._message( 'editMessageCaption', data, timeout=timeout, reply_markup=reply_markup, api_kwargs=api_kwargs, ) @log def edit_message_media( self, chat_id: Union[str, int] = None, message_id: int = None, inline_message_id: int = None, media: 'InputMedia' = None, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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 ``file_id`` or specify a URL. Args: chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). 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. media (:class:`telegram.InputMedia`): An object for a new media content of the message. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized object for an inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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: :class:`telegram.error.TelegramError` """ if inline_message_id is None and (chat_id is None or message_id is None): raise ValueError( 'edit_message_media: Both chat_id and message_id are required when ' 'inline_message_id is not specified' ) data: JSONDict = {'media': media} if chat_id: data['chat_id'] = chat_id if message_id: data['message_id'] = message_id if inline_message_id: data['inline_message_id'] = inline_message_id return self._message( 'editMessageMedia', data, timeout=timeout, reply_markup=reply_markup, api_kwargs=api_kwargs, ) @log def edit_message_reply_markup( self, chat_id: Union[str, int] = None, message_id: int = None, inline_message_id: int = None, reply_markup: Optional['InlineKeyboardMarkup'] = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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). Args: chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not specified. Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). 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): A JSON-serialized object for an inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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` """ if inline_message_id is None and (chat_id is None or message_id is None): raise ValueError( 'edit_message_reply_markup: Both chat_id and message_id are required when ' 'inline_message_id is not specified' ) data: JSONDict = {} if chat_id: data['chat_id'] = chat_id if message_id: data['message_id'] = message_id if inline_message_id: data['inline_message_id'] = inline_message_id return self._message( 'editMessageReplyMarkup', data, timeout=timeout, reply_markup=reply_markup, api_kwargs=api_kwargs, ) @log def get_updates( self, offset: int = None, limit: int = 100, timeout: float = 0, read_latency: float = 2.0, allowed_updates: List[str] = None, api_kwargs: JSONDict = None, ) -> List[Update]: """Use this method to receive incoming updates using long polling. 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 getUpdates 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 forgotten. limit (:obj:`int`, optional): Limits the number of updates to be retrieved. Values between 1-100 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. read_latency (:obj:`float` | :obj:`int`, optional): Grace time in seconds for receiving the reply from server. Will be added to the ``timeout`` value and used as the read timeout from server. Defaults to ``2``. allowed_updates (List[:obj:`str`]), optional): A JSON-serialized list 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 list to receive all updates except :attr:`telegram.Update.chat_member` (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. api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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` Returns: List[:class:`telegram.Update`] Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'timeout': timeout} if offset: data['offset'] = offset if limit: data['limit'] = limit if allowed_updates is not None: data['allowed_updates'] = allowed_updates # 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], self._post( 'getUpdates', data, timeout=float(read_latency) + float(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) # type: ignore[return-value] @log def set_webhook( self, url: str = None, certificate: FileInput = None, timeout: ODVInput[float] = DEFAULT_NONE, max_connections: int = 40, allowed_updates: List[str] = None, api_kwargs: JSONDict = None, ip_address: str = None, drop_pending_updates: bool = 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 a JSON-serialized 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 request comes from Telegram, Telegram recommends using a secret path in the URL, e.g. https://www.example.com/. Since nobody else knows your bot's token, you can be pretty sure it's us. Note: The certificate argument should be a file from disk ``open(filename, 'rb')``. Args: url (:obj:`str`): HTTPS url to send updates to. Use an empty string to remove webhook integration. certificate (:obj:`filelike`): Upload your public key certificate so that the root certificate in use can be checked. See our self-signed guide for details. (https://goo.gl/rw7w6Y) 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, 1-100. 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 (List[:obj:`str`], optional): A JSON-serialized list 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 list to receive all updates except :attr:`telegram.Update.chat_member` (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 updates may be received for a short period of time. drop_pending_updates (:obj:`bool`, optional): Pass :obj:`True` to drop all pending updates. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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: ``443``, ``80``, ``88``, ``8443``. If you're having any trouble setting up webhooks, please check out this `guide to Webhooks`_. 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 = {} if url is not None: data['url'] = url if certificate: data['certificate'] = parse_file_input(certificate) if max_connections is not None: data['max_connections'] = max_connections if allowed_updates is not None: data['allowed_updates'] = allowed_updates if ip_address: data['ip_address'] = ip_address if drop_pending_updates: data['drop_pending_updates'] = drop_pending_updates result = self._post('setWebhook', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def delete_webhook( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, drop_pending_updates: bool = 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data = {} if drop_pending_updates: data['drop_pending_updates'] = drop_pending_updates result = self._post('deleteWebhook', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def leave_chat( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Use this method for your bot to leave a group, supergroup or channel. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target supergroup or channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id} result = self._post('leaveChat', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def get_chat( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target supergroup or channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Chat` Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id} result = self._post('getChat', data, timeout=timeout, api_kwargs=api_kwargs) return Chat.de_json(result, self) # type: ignore[return-value, arg-type] @log def get_chat_administrators( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> List[ChatMember]: """ Use this method to get a list of administrators in a chat. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target supergroup or channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: List[:class:`telegram.ChatMember`]: On success, returns a list 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 = self._post('getChatAdministrators', data, timeout=timeout, api_kwargs=api_kwargs) return ChatMember.de_list(result, self) # type: ignore @log def get_chat_members_count( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> int: """ Deprecated, use :func:`~telegram.Bot.get_chat_member_count` instead. .. deprecated:: 13.7 """ warnings.warn( '`bot.get_chat_members_count` is deprecated. ' 'Use `bot.get_chat_member_count` instead.', TelegramDeprecationWarning, stacklevel=2, ) return self.get_chat_member_count(chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs) @log def get_chat_member_count( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target supergroup or channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`int`: Number of members in the chat. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id} result = self._post('getChatMemberCount', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def get_chat_member( self, chat_id: Union[str, int], user_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> ChatMember: """Use this method to get information about a member of a chat. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target supergroup or channel (in the format ``@channelusername``). user_id (:obj:`int`): Unique identifier of the target user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.ChatMember` Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} result = self._post('getChatMember', data, timeout=timeout, api_kwargs=api_kwargs) return ChatMember.de_json(result, self) # type: ignore[return-value, arg-type] @log def set_chat_sticker_set( self, chat_id: Union[str, int], sticker_set_name: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername). sticker_set_name (:obj:`str`): Name of the sticker set to be set as the group sticker set. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. """ data: JSONDict = {'chat_id': chat_id, 'sticker_set_name': sticker_set_name} result = self._post('setChatStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def delete_chat_sticker_set( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. """ data: JSONDict = {'chat_id': chat_id} result = self._post('deleteChatStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] def get_webhook_info( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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. Args: timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.WebhookInfo` """ result = self._post('getWebhookInfo', None, timeout=timeout, api_kwargs=api_kwargs) return WebhookInfo.de_json(result, self) # type: ignore[return-value, arg-type] @log def set_game_score( self, user_id: Union[int, str], score: int, chat_id: Union[str, int] = None, message_id: int = None, inline_message_id: int = None, force: bool = None, disable_edit_message: bool = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Union[Message, bool]: """ Use this method to set the score of the specified user in a game. 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` | :obj:`str`, optional): Required if inline_message_id is not specified. Unique identifier for the target chat. message_id (:obj:`int`, optional): Required if inline_message_id is not specified. Identifier of the sent message. inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not specified. Identifier of the inline message. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: The edited message, or if the message wasn't sent by the bot , :obj:`True`. Raises: :class:`telegram.error.TelegramError`: If the new score is not greater than the user's current score in the chat and force is :obj:`False`. """ data: JSONDict = {'user_id': user_id, 'score': score} if chat_id: data['chat_id'] = chat_id if message_id: data['message_id'] = message_id if inline_message_id: data['inline_message_id'] = inline_message_id if force is not None: data['force'] = force if disable_edit_message is not None: data['disable_edit_message'] = disable_edit_message return self._message( 'setGameScore', data, timeout=timeout, api_kwargs=api_kwargs, ) @log def get_game_high_scores( self, user_id: Union[int, str], chat_id: Union[str, int] = None, message_id: int = None, inline_message_id: int = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> List[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. Args: user_id (:obj:`int`): Target user id. chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not specified. Unique identifier for the target chat. message_id (:obj:`int`, optional): Required if inline_message_id is not specified. Identifier of the sent message. inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not specified. Identifier of the inline message. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: List[:class:`telegram.GameHighScore`] Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'user_id': user_id} if chat_id: data['chat_id'] = chat_id if message_id: data['message_id'] = message_id if inline_message_id: data['inline_message_id'] = inline_message_id result = self._post('getGameHighScores', data, timeout=timeout, api_kwargs=api_kwargs) return GameHighScore.de_list(result, self) # type: ignore @log def send_invoice( self, chat_id: Union[int, str], title: str, description: str, payload: str, provider_token: str, currency: str, prices: List['LabeledPrice'], start_parameter: str = None, photo_url: str = None, photo_size: int = None, photo_width: int = None, photo_height: int = None, need_name: bool = None, need_phone_number: bool = None, need_email: bool = None, need_shipping_address: bool = None, is_flexible: bool = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: InlineKeyboardMarkup = None, provider_data: Union[str, object] = None, send_phone_number_to_provider: bool = None, send_email_to_provider: bool = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: bool = None, ) -> Message: """Use this method to send invoices. Warning: As of API 5.2 :attr:`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 :attr:`start_parameter` is optional. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). title (:obj:`str`): Product name, 1-32 characters. description (:obj:`str`): Product description, 1-255 characters. payload (:obj:`str`): Bot-defined invoice payload, 1-128 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. prices (List[:class:`telegram.LabeledPrice`)]: Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.). 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 (List[:obj:`int`], optional): A JSON-serialized array of suggested amounts of tips 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 ``max_tip_amount``. .. versionadded:: 13.5 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): JSON-serialized 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): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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': [p.to_dict() for p in prices], } if max_tip_amount is not None: data['max_tip_amount'] = max_tip_amount if suggested_tip_amounts is not None: data['suggested_tip_amounts'] = suggested_tip_amounts if start_parameter is not None: data['start_parameter'] = start_parameter if provider_data is not None: if isinstance(provider_data, str): data['provider_data'] = provider_data else: data['provider_data'] = json.dumps(provider_data) if photo_url is not None: data['photo_url'] = photo_url if photo_size is not None: data['photo_size'] = photo_size if photo_width is not None: data['photo_width'] = photo_width if photo_height is not None: data['photo_height'] = photo_height if need_name is not None: data['need_name'] = need_name if need_phone_number is not None: data['need_phone_number'] = need_phone_number if need_email is not None: data['need_email'] = need_email if need_shipping_address is not None: data['need_shipping_address'] = need_shipping_address if is_flexible is not None: data['is_flexible'] = is_flexible if send_phone_number_to_provider is not None: data['send_phone_number_to_provider'] = send_phone_number_to_provider if send_email_to_provider is not None: data['send_email_to_provider'] = send_email_to_provider return self._message( # type: ignore[return-value] 'sendInvoice', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def answer_shipping_query( # pylint: disable=C0103 self, shipping_query_id: str, ok: bool, shipping_options: List[ShippingOption] = None, error_message: str = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """ If you sent an invoice requesting a shipping address and the parameter ``is_flexible`` was specified, the Bot API will send an :class:`telegram.Update` with a :attr:`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 (List[:class:`telegram.ShippingOption`]), optional]: Required if ok is :obj:`True`. A JSON-serialized array of available shipping options. error_message (:obj:`str`, optional): Required if 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ ok = bool(ok) if ok and (shipping_options is None or error_message is not None): raise TelegramError( 'answerShippingQuery: If ok is True, shipping_options ' 'should not be empty and there should not be error_message' ) if not ok and (shipping_options is not None or error_message is None): raise TelegramError( 'answerShippingQuery: If ok is False, error_message ' 'should not be empty and there should not be shipping_options' ) data: JSONDict = {'shipping_query_id': shipping_query_id, 'ok': ok} if ok: if not shipping_options: # not using an assert statement directly here since they are removed in # the optimized bytecode raise AssertionError data['shipping_options'] = [option.to_dict() for option in shipping_options] if error_message is not None: data['error_message'] = error_message result = self._post('answerShippingQuery', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def answer_pre_checkout_query( # pylint: disable=C0103 self, pre_checkout_query_id: str, ok: bool, error_message: str = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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:`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 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ ok = bool(ok) if not (ok ^ (error_message is not None)): # pylint: disable=C0325 raise TelegramError( 'answerPreCheckoutQuery: If ok is True, there should ' 'not be error_message; if ok is False, error_message ' 'should not be empty' ) data: JSONDict = {'pre_checkout_query_id': pre_checkout_query_id, 'ok': ok} if error_message is not None: data['error_message'] = error_message result = self._post('answerPreCheckoutQuery', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def restrict_chat_member( self, chat_id: Union[str, int], user_id: Union[str, int], permissions: ChatPermissions, until_date: Union[int, datetime] = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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. Note: Since Bot API 4.4, :meth:`restrict_chat_member` takes the new user permissions in a single argument of type :class:`telegram.ChatPermissions`. The old way of passing parameters will not keep working forever. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername). 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. permissions (:class:`telegram.ChatPermissions`): A JSON-serialized object for new user permissions. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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.to_dict(), } if until_date is not None: if isinstance(until_date, datetime): until_date = to_timestamp( until_date, tzinfo=self.defaults.tzinfo if self.defaults else None ) data['until_date'] = until_date result = self._post('restrictChatMember', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def promote_chat_member( self, chat_id: Union[str, int], user_id: Union[str, int], can_change_info: bool = None, can_post_messages: bool = None, can_edit_messages: bool = None, can_delete_messages: bool = None, can_invite_users: bool = None, can_restrict_members: bool = None, can_pin_messages: bool = None, can_promote_members: bool = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, is_anonymous: bool = None, can_manage_chat: bool = None, can_manage_voice_chats: bool = 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. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). 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, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege. .. versionadded:: 13.4 can_manage_voice_chats (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can manage voice chats. .. versionadded:: 13.4 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 create channel posts, channels only. can_edit_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can edit messages of other users and can pin messages, 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. can_pin_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can pin messages, supergroups only. can_promote_members (:obj:`bool`, optional): Pass :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 him). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} if is_anonymous is not None: data['is_anonymous'] = is_anonymous if can_change_info is not None: data['can_change_info'] = can_change_info if can_post_messages is not None: data['can_post_messages'] = can_post_messages if can_edit_messages is not None: data['can_edit_messages'] = can_edit_messages if can_delete_messages is not None: data['can_delete_messages'] = can_delete_messages if can_invite_users is not None: data['can_invite_users'] = can_invite_users if can_restrict_members is not None: data['can_restrict_members'] = can_restrict_members if can_pin_messages is not None: data['can_pin_messages'] = can_pin_messages if can_promote_members is not None: data['can_promote_members'] = can_promote_members if can_manage_chat is not None: data['can_manage_chat'] = can_manage_chat if can_manage_voice_chats is not None: data['can_manage_voice_chats'] = can_manage_voice_chats result = self._post('promoteChatMember', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def set_chat_permissions( self, chat_id: Union[str, int], permissions: ChatPermissions, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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.ChatMember.can_restrict_members` admin rights. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target supergroup (in the format `@supergroupusername`). permissions (:class:`telegram.ChatPermissions`): New default chat permissions. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'permissions': permissions.to_dict()} result = self._post('setChatPermissions', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def set_chat_administrator_custom_title( self, chat_id: Union[int, str], user_id: Union[int, str], custom_title: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target supergroup (in the format `@supergroupusername`). user_id (:obj:`int`): Unique identifier of the target administrator. custom_title (:obj:`str`): New custom title for the administrator; 0-16 characters, emoji are not allowed. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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} result = self._post( 'setChatAdministratorCustomTitle', data, timeout=timeout, api_kwargs=api_kwargs ) return result # type: ignore[return-value] @log def export_chat_invite_link( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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 :attr:`export_chat_invite_link` again. Returns: :obj:`str`: New invite link on success. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id} result = self._post('exportChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def create_chat_invite_link( self, chat_id: Union[str, int], expire_date: Union[int, datetime] = None, member_limit: int = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, name: str = None, creates_join_request: bool = 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`. .. versionadded:: 13.4 Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). 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. 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; 1-99999. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. name (:obj:`str`, optional): Invite link name; 0-32 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`, ``member_limit`` can't be specified. .. versionadded:: 13.8 Returns: :class:`telegram.ChatInviteLink` Raises: :class:`telegram.error.TelegramError` """ if creates_join_request and member_limit: raise ValueError( "If `creates_join_request` is `True`, `member_limit` can't be specified." ) data: JSONDict = { 'chat_id': chat_id, } if expire_date is not None: if isinstance(expire_date, datetime): expire_date = to_timestamp( expire_date, tzinfo=self.defaults.tzinfo if self.defaults else None ) data['expire_date'] = expire_date if member_limit is not None: data['member_limit'] = member_limit if name is not None: data['name'] = name if creates_join_request is not None: data['creates_join_request'] = creates_join_request result = self._post('createChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] @log def edit_chat_invite_link( self, chat_id: Union[str, int], invite_link: str, expire_date: Union[int, datetime] = None, member_limit: int = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, name: str = None, creates_join_request: bool = 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`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). invite_link (:obj:`str`): The invite link to edit. 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. 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; 1-99999. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. name (:obj:`str`, optional): Invite link name; 0-32 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`, ``member_limit`` can't be specified. .. versionadded:: 13.8 Returns: :class:`telegram.ChatInviteLink` Raises: :class:`telegram.error.TelegramError` """ if creates_join_request and member_limit: raise ValueError( "If `creates_join_request` is `True`, `member_limit` can't be specified." ) data: JSONDict = {'chat_id': chat_id, 'invite_link': invite_link} if expire_date is not None: if isinstance(expire_date, datetime): expire_date = to_timestamp( expire_date, tzinfo=self.defaults.tzinfo if self.defaults else None ) data['expire_date'] = expire_date if member_limit is not None: data['member_limit'] = member_limit if name is not None: data['name'] = name if creates_join_request is not None: data['creates_join_request'] = creates_join_request result = self._post('editChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] @log def revoke_chat_invite_link( self, chat_id: Union[str, int], invite_link: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). invite_link (:obj:`str`): The invite link to edit. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.ChatInviteLink` Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'invite_link': invite_link} result = self._post('revokeChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] @log def approve_chat_join_request( self, chat_id: Union[str, int], user_id: int, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). user_id (:obj:`int`): Unique identifier of the target user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} result = self._post('approveChatJoinRequest', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def decline_chat_join_request( self, chat_id: Union[str, int], user_id: int, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). user_id (:obj:`int`): Unique identifier of the target user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} result = self._post('declineChatJoinRequest', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def set_chat_photo( self, chat_id: Union[str, int], photo: FileInput, timeout: DVInput[float] = DEFAULT_20, api_kwargs: 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`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). photo (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`): New chat photo. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'photo': parse_file_input(photo)} result = self._post('setChatPhoto', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def delete_chat_photo( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id} result = self._post('deleteChatPhoto', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def set_chat_title( self, chat_id: Union[str, int], title: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). title (:obj:`str`): New chat title, 1-255 characters. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'title': title} result = self._post('setChatTitle', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def set_chat_description( self, chat_id: Union[str, int], description: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). description (:obj:`str`): New chat description, 0-255 characters. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'description': description} result = self._post('setChatDescription', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def pin_chat_message( self, chat_id: Union[str, int], message_id: int, disable_notification: ODVInput[bool] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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 :attr:`telegram.ChatMember.can_pin_messages` admin right in a supergroup or :attr:`telegram.ChatMember.can_edit_messages` admin right in a channel. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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 self._post( # type: ignore[return-value] 'pinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs ) @log def unpin_chat_message( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, message_id: int = 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 :attr:`telegram.ChatMember.can_pin_messages` admin right in a supergroup or :attr:`telegram.ChatMember.can_edit_messages` admin right in a channel. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id} if message_id is not None: data['message_id'] = message_id return self._post( # type: ignore[return-value] 'unpinChatMessage', data, timeout=timeout, api_kwargs=api_kwargs ) @log def unpin_all_chat_messages( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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 :attr:`telegram.ChatMember.can_pin_messages` admin right in a supergroup or :attr:`telegram.ChatMember.can_edit_messages` admin right in a channel. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id} return self._post( # type: ignore[return-value] 'unpinAllChatMessages', data, timeout=timeout, api_kwargs=api_kwargs ) @log def get_sticker_set( self, name: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> StickerSet: """Use this method to get a sticker set. Args: name (:obj:`str`): Name of the sticker set. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.StickerSet` Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'name': name} result = self._post('getStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) return StickerSet.de_json(result, self) # type: ignore[return-value, arg-type] @log def upload_sticker_file( self, user_id: Union[str, int], png_sticker: FileInput, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, ) -> File: """ Use this method to upload a ``.PNG`` file with a sticker for later use in :meth:`create_new_sticker_set` and :meth:`add_sticker_to_set` methods (can be used multiple times). Note: The png_sticker argument can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` Args: user_id (:obj:`int`): User identifier of sticker file owner. png_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`): **PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.File`: On success, the uploaded File is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'user_id': user_id, 'png_sticker': parse_file_input(png_sticker)} result = self._post('uploadStickerFile', data, timeout=timeout, api_kwargs=api_kwargs) return File.de_json(result, self) # type: ignore[return-value, arg-type] @log def create_new_sticker_set( self, user_id: Union[str, int], name: str, title: str, emojis: str, png_sticker: FileInput = None, contains_masks: bool = None, mask_position: MaskPosition = None, timeout: DVInput[float] = DEFAULT_20, tgs_sticker: FileInput = None, api_kwargs: JSONDict = None, webm_sticker: FileInput = 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. You must use exactly one of the fields ``png_sticker``, ``tgs_sticker``, or ``webm_sticker``. Warning: As of API 4.7 ``png_sticker`` 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: The png_sticker and tgs_sticker argument can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` 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. 1-64 characters. title (:obj:`str`): Sticker set title, 1-64 characters. png_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \ optional): **PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. tgs_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \ optional): **TGS** animation with the sticker, uploaded using multipart/form-data. See https://core.telegram.org/stickers#animated-sticker-requirements for technical requirements. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\ optional): **WEBM** video with the sticker, uploaded using multipart/form-data. See https://core.telegram.org/stickers#video-sticker-requirements for technical requirements. .. versionadded:: 13.11 emojis (:obj:`str`): One or more emoji corresponding to the sticker. contains_masks (:obj:`bool`, optional): Pass :obj:`True`, if a set of mask stickers should be created. mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask should be placed on faces. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis} if png_sticker is not None: data['png_sticker'] = parse_file_input(png_sticker) if tgs_sticker is not None: data['tgs_sticker'] = parse_file_input(tgs_sticker) if webm_sticker is not None: data['webm_sticker'] = parse_file_input(webm_sticker) if contains_masks is not None: data['contains_masks'] = contains_masks if mask_position is not None: # We need to_json() instead of to_dict() here, because we're sending a media # message here, which isn't json dumped by utils.request data['mask_position'] = mask_position.to_json() result = self._post('createNewStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def add_sticker_to_set( self, user_id: Union[str, int], name: str, emojis: str, png_sticker: FileInput = None, mask_position: MaskPosition = None, timeout: DVInput[float] = DEFAULT_20, tgs_sticker: FileInput = None, api_kwargs: JSONDict = None, webm_sticker: FileInput = None, ) -> bool: """ Use this method to add a new sticker to a set created by the bot. You **must** use exactly one of the fields ``png_sticker``, ``tgs_sticker`` or ``webm_sticker``. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Warning: As of API 4.7 ``png_sticker`` 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: The png_sticker and tgs_sticker argument can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` Args: user_id (:obj:`int`): User identifier of created sticker set owner. name (:obj:`str`): Sticker set name. png_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \ optional): **PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. tgs_sticker (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \ optional): **TGS** animation with the sticker, uploaded using multipart/form-data. See https://core.telegram.org/stickers#animated-sticker-requirements for technical requirements. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\ optional): **WEBM** video with the sticker, uploaded using multipart/form-data. See https://core.telegram.org/stickers#video-sticker-requirements for technical requirements. .. versionadded:: 13.11 emojis (:obj:`str`): One or more emoji corresponding to the sticker. mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask should be placed on faces. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'user_id': user_id, 'name': name, 'emojis': emojis} if png_sticker is not None: data['png_sticker'] = parse_file_input(png_sticker) if tgs_sticker is not None: data['tgs_sticker'] = parse_file_input(tgs_sticker) if webm_sticker is not None: data['webm_sticker'] = parse_file_input(webm_sticker) if mask_position is not None: # We need to_json() instead of to_dict() here, because we're sending a media # message here, which isn't json dumped by utils.request data['mask_position'] = mask_position.to_json() result = self._post('addStickerToSet', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def set_sticker_position_in_set( self, sticker: str, position: int, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'sticker': sticker, 'position': position} result = self._post( 'setStickerPositionInSet', data, timeout=timeout, api_kwargs=api_kwargs ) return result # type: ignore[return-value] @log def delete_sticker_from_set( self, sticker: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'sticker': sticker} result = self._post('deleteStickerFromSet', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def set_sticker_set_thumb( self, name: str, user_id: Union[str, int], thumb: FileInput = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Video thumbnails can be set only for video sticker sets only. Note: The thumb can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` Args: name (:obj:`str`): Sticker set name user_id (:obj:`int`): User identifier of created sticker set owner. thumb (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path`, \ optional): A **PNG** image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a **TGS** animation with the thumbnail up to 32 kilobytes in size; see https://core.telegram.org/stickers#animated-sticker-requirements for animated sticker technical requirements, or a **WEBM** video with the thumbnail up to 32 kilobytes in size; see https://core.telegram.org/stickers#video-sticker-requirements for video sticker technical requirements. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. Animated sticker set thumbnails can't be uploaded via HTTP URL. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'name': name, 'user_id': user_id} if thumb is not None: data['thumb'] = parse_file_input(thumb) result = self._post('setStickerSetThumb', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def set_passport_data_errors( self, user_id: Union[str, int], errors: List[PassportElementError], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: 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 (List[:class:`PassportElementError`]): A JSON-serialized array describing the errors. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'user_id': user_id, 'errors': [error.to_dict() for error in errors]} result = self._post('setPassportDataErrors', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @log def send_poll( self, chat_id: Union[int, str], question: str, options: List[str], is_anonymous: bool = True, type: str = Poll.REGULAR, # pylint: disable=W0622 allows_multiple_answers: bool = False, correct_option_id: int = None, is_closed: bool = None, disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, explanation: str = None, explanation_parse_mode: ODVInput[str] = DEFAULT_NONE, open_period: int = None, close_date: Union[int, datetime] = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, ) -> Message: """ Use this method to send a native poll. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). question (:obj:`str`): Poll question, 1-300 characters. options (List[:obj:`str`]): List of answer options, 2-10 strings 1-100 characters each. is_anonymous (:obj:`bool`, optional): :obj:`True`, if the poll needs to be anonymous, defaults to :obj:`True`. type (:obj:`str`, optional): Poll type, :attr:`telegram.Poll.QUIZ` or :attr:`telegram.Poll.REGULAR`, defaults to :attr:`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-200 characters with at most 2 line feeds after entities parsing. explanation_parse_mode (:obj:`str`, optional): Mode for parsing entities in the explanation. See the constants in :class:`telegram.ParseMode` for the available modes. explanation_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in message text, which can be specified instead of :attr:`parse_mode`. open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active after creation, 5-600. Can't be used together with :attr:`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 5 and no more than 600 seconds in the future. Can't be used together with :attr:`open_period`. For timezone naive :obj:`datetime.datetime` objects, the default timezone of the bot will be 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): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. 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, } if not is_anonymous: data['is_anonymous'] = is_anonymous if type: data['type'] = type if allows_multiple_answers: data['allows_multiple_answers'] = allows_multiple_answers if correct_option_id is not None: data['correct_option_id'] = correct_option_id if is_closed: data['is_closed'] = is_closed if explanation: data['explanation'] = explanation if explanation_entities: data['explanation_entities'] = [me.to_dict() for me in explanation_entities] if open_period: data['open_period'] = open_period if close_date: if isinstance(close_date, datetime): close_date = to_timestamp( close_date, tzinfo=self.defaults.tzinfo if self.defaults else None ) data['close_date'] = close_date return self._message( # type: ignore[return-value] 'sendPoll', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def stop_poll( self, chat_id: Union[int, str], message_id: int, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Poll: """ Use this method to stop a poll which was sent by the bot. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). message_id (:obj:`int`): Identifier of the original message with the poll. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): A JSON-serialized object for a new message inline keyboard. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Poll`: On success, the stopped Poll with the final results is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id, 'message_id': message_id} if reply_markup: if isinstance(reply_markup, ReplyMarkup): # We need to_json() instead of to_dict() here, because reply_markups may be # attached to media messages, which aren't json dumped by utils.request data['reply_markup'] = reply_markup.to_json() else: data['reply_markup'] = reply_markup result = self._post('stopPoll', data, timeout=timeout, api_kwargs=api_kwargs) return Poll.de_json(result, self) # type: ignore[return-value, arg-type] @log def send_dice( self, chat_id: Union[int, str], disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, emoji: str = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> Message: """ Use this method to send an animated emoji that will display a random value. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). emoji (:obj:`str`, optional): Emoji on which the dice throw animation is based. Currently, must be one of “🎲”, “🎯”, “🏀”, “⚽”, "🎳", or “🎰”. Dice can have values 1-6 for “🎲”, “🎯” and "🎳", values 1-5 for “🏀” and “⚽”, and values 1-64 for “🎰”. Defaults to “🎲”. .. versionchanged:: 13.4 Added the "🎳" emoji. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {'chat_id': chat_id} if emoji: data['emoji'] = emoji return self._message( # type: ignore[return-value] 'sendDice', data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, ) @log def get_my_commands( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, scope: BotCommandScope = None, language_code: str = None, ) -> List[BotCommand]: """ Use this method to get the current list of the bot's commands for the given scope and user language. Args: timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized 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: List[:class:`telegram.BotCommand`]: On success, the commands set for the bot. An empty list is returned if commands are not set. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {} if scope: data['scope'] = scope.to_dict() if language_code: data['language_code'] = language_code result = self._post('getMyCommands', data, timeout=timeout, api_kwargs=api_kwargs) if (scope is None or scope.type == scope.DEFAULT) and language_code is None: self._commands = BotCommand.de_list(result, self) # type: ignore[assignment,arg-type] return self._commands # type: ignore[return-value] return BotCommand.de_list(result, self) # type: ignore[return-value,arg-type] @log def set_my_commands( self, commands: List[Union[BotCommand, Tuple[str, str]]], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, scope: BotCommandScope = None, language_code: str = None, ) -> bool: """ Use this method to change the list of the bot's commands. See the `Telegram docs `_ for more details about bot commands. Args: commands (List[:class:`BotCommand` | (:obj:`str`, :obj:`str`)]): A JSON-serialized list of bot commands to be set as the list of the bot's commands. At most 100 commands can be specified. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized 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': [c.to_dict() for c in cmds]} if scope: data['scope'] = scope.to_dict() if language_code: data['language_code'] = language_code result = self._post('setMyCommands', data, timeout=timeout, api_kwargs=api_kwargs) # Set commands only for default scope. No need to check for outcome. # If request failed, we won't come this far if (scope is None or scope.type == scope.DEFAULT) and language_code is None: self._commands = cmds return result # type: ignore[return-value] @log def delete_my_commands( self, scope: BotCommandScope = None, language_code: str = None, api_kwargs: JSONDict = None, timeout: ODVInput[float] = DEFAULT_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 Args: scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = {} if scope: data['scope'] = scope.to_dict() if language_code: data['language_code'] = language_code result = self._post('deleteMyCommands', data, timeout=timeout, api_kwargs=api_kwargs) if (scope is None or scope.type == scope.DEFAULT) and language_code is None: self._commands = [] return result # type: ignore[return-value] @log def log_out(self, timeout: ODVInput[float] = DEFAULT_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. Args: timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). Returns: :obj:`True`: On success Raises: :class:`telegram.error.TelegramError` """ return self._post('logOut', timeout=timeout) # type: ignore[return-value] @log def close(self, timeout: ODVInput[float] = DEFAULT_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. Args: timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). Returns: :obj:`True`: On success Raises: :class:`telegram.error.TelegramError` """ return self._post('close', timeout=timeout) # type: ignore[return-value] @log def copy_message( self, chat_id: Union[int, str], from_chat_id: Union[str, int], message_id: int, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = 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`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). 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-1024 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.ParseMode` for the available modes. caption_entities (:class:`telegram.utils.types.SLT[MessageEntity]`): List of special entities that appear in the new caption, which can be specified instead of parse_mode disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. protect_content (:obj:`bool`, optional): Protects the contents of the sent message from forwarding and saving. .. versionadded:: 13.10 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. Returns: :class:`telegram.MessageId`: On success Raises: :class:`telegram.error.TelegramError` """ data: JSONDict = { 'chat_id': chat_id, 'from_chat_id': from_chat_id, 'message_id': message_id, 'parse_mode': parse_mode, 'disable_notification': disable_notification, 'allow_sending_without_reply': allow_sending_without_reply, } if caption is not None: data['caption'] = caption if caption_entities: data['caption_entities'] = caption_entities if reply_to_message_id: data['reply_to_message_id'] = reply_to_message_id if protect_content: data['protect_content'] = protect_content if reply_markup: if isinstance(reply_markup, ReplyMarkup): # We need to_json() instead of to_dict() here, because reply_markups may be # attached to media messages, which aren't json dumped by utils.request data['reply_markup'] = reply_markup.to_json() else: data['reply_markup'] = reply_markup result = self._post('copyMessage', data, timeout=timeout, api_kwargs=api_kwargs) return MessageId.de_json(result, self) # type: ignore[return-value, arg-type] def to_dict(self) -> 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 def __eq__(self, other: object) -> bool: return self.bot == other def __hash__(self) -> int: return hash(self.bot) # 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`""" forwardMessage = forward_message """Alias for :meth:`forward_message`""" 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`""" kickChatMember = kick_chat_member """Alias for :meth:`kick_chat_member`""" 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`""" getChatMembersCount = get_chat_members_count """Alias for :meth:`get_chat_members_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`""" 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`""" 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`""" setStickerSetThumb = set_sticker_set_thumb """Alias for :meth:`set_sticker_set_thumb`""" 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`""" python-telegram-bot-13.11/telegram/botcommand.py000066400000000000000000000035151417656324400217350ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject 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, 1-32 characters. Can contain only lowercase English letters, digits and underscores. description (:obj:`str`): Description of the command, 1-256 characters. Attributes: command (:obj:`str`): Text of the command. description (:obj:`str`): Description of the command. """ __slots__ = ('description', '_id_attrs', 'command') def __init__(self, command: str, description: str, **_kwargs: Any): self.command = command self.description = description self._id_attrs = (self.command, self.description) python-telegram-bot-13.11/telegram/botcommandscope.py000066400000000000000000000233611417656324400227700ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=W0622 """This module contains objects representing Telegram bot command scopes.""" from typing import Any, Union, Optional, TYPE_CHECKING, Dict, Type from telegram import TelegramObject, constants 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', '_id_attrs') DEFAULT = constants.BOT_COMMAND_SCOPE_DEFAULT """:const:`telegram.constants.BOT_COMMAND_SCOPE_DEFAULT`""" ALL_PRIVATE_CHATS = constants.BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS`""" ALL_GROUP_CHATS = constants.BOT_COMMAND_SCOPE_ALL_GROUP_CHATS """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_GROUP_CHATS`""" ALL_CHAT_ADMINISTRATORS = constants.BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS`""" CHAT = constants.BOT_COMMAND_SCOPE_CHAT """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT`""" CHAT_ADMINISTRATORS = constants.BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS`""" CHAT_MEMBER = constants.BOT_COMMAND_SCOPE_CHAT_MEMBER """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT_MEMBER`""" def __init__(self, type: str, **_kwargs: Any): self.type = type self._id_attrs = (self.type,) @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: return _class_mapping.get(data['type'], cls)(**data, bot=bot) return cls(**data) 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 :attr:`telegram.BotCommandScope.DEFAULT`. """ __slots__ = () def __init__(self, **_kwargs: Any): super().__init__(type=BotCommandScope.DEFAULT) class BotCommandScopeAllPrivateChats(BotCommandScope): """Represents the scope of bot commands, covering all private chats. .. versionadded:: 13.7 Attributes: type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_PRIVATE_CHATS`. """ __slots__ = () def __init__(self, **_kwargs: Any): super().__init__(type=BotCommandScope.ALL_PRIVATE_CHATS) class BotCommandScopeAllGroupChats(BotCommandScope): """Represents the scope of bot commands, covering all group and supergroup chats. .. versionadded:: 13.7 Attributes: type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_GROUP_CHATS`. """ __slots__ = () def __init__(self, **_kwargs: Any): super().__init__(type=BotCommandScope.ALL_GROUP_CHATS) 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 :attr:`telegram.BotCommandScope.ALL_CHAT_ADMINISTRATORS`. """ __slots__ = () def __init__(self, **_kwargs: Any): super().__init__(type=BotCommandScope.ALL_CHAT_ADMINISTRATORS) 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`): Unique identifier for the target chat or username of the target supergroup (in the format ``@supergroupusername``) Attributes: type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT`. chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the target supergroup (in the format ``@supergroupusername``) """ __slots__ = ('chat_id',) def __init__(self, chat_id: Union[str, int], **_kwargs: Any): super().__init__(type=BotCommandScope.CHAT) self.chat_id = ( 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`): Unique identifier for the target chat or username of the target supergroup (in the format ``@supergroupusername``) Attributes: type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT_ADMINISTRATORS`. chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the target supergroup (in the format ``@supergroupusername``) """ __slots__ = ('chat_id',) def __init__(self, chat_id: Union[str, int], **_kwargs: Any): super().__init__(type=BotCommandScope.CHAT_ADMINISTRATORS) self.chat_id = ( 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`): Unique identifier for the target chat or username of the target supergroup (in the format ``@supergroupusername``) user_id (:obj:`int`): Unique identifier of the target user. Attributes: type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT_MEMBER`. chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the target supergroup (in the format ``@supergroupusername``) 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, **_kwargs: Any): super().__init__(type=BotCommandScope.CHAT_MEMBER) self.chat_id = ( 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-13.11/telegram/callbackquery.py000066400000000000000000000553561417656324400224460ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=W0622 """This module contains an object that represents a Telegram CallbackQuery""" from typing import TYPE_CHECKING, Any, List, Optional, Union, Tuple, ClassVar from telegram import Message, TelegramObject, User, Location, ReplyMarkup, constants from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput, DVInput if TYPE_CHECKING: from telegram import ( Bot, GameHighScore, InlineKeyboardMarkup, MessageId, InputMedia, MessageEntity, ) 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 ``from`` is a reserved word, use ``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:`Bot.arbitrary_callback_data`, :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.Message`, optional): Message with the callback button that originated the query. Note that message content and message date will not be available if the message is too old. data (:obj:`str`, optional): Data associated with the callback button. Be aware that a bad client can send arbitrary data in this field. 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 bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. 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. message (:class:`telegram.Message`): Optional. Message with the callback button that originated the query. data (:obj:`str` | :obj:`object`): Optional. Data associated with the callback button. 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. """ __slots__ = ( 'bot', 'game_short_name', 'message', 'chat_instance', 'id', 'from_user', 'inline_message_id', 'data', '_id_attrs', ) def __init__( self, id: str, # pylint: disable=W0622 from_user: User, chat_instance: str, message: Message = None, data: str = None, inline_message_id: str = None, game_short_name: str = None, bot: 'Bot' = None, **_kwargs: Any, ): # Required self.id = id # pylint: disable=C0103 self.from_user = from_user self.chat_instance = chat_instance # Optionals self.message = message self.data = data self.inline_message_id = inline_message_id self.game_short_name = game_short_name self.bot = bot self._id_attrs = (self.id,) @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.get('from'), bot) data['message'] = Message.de_json(data.get('message'), bot) return cls(bot=bot, **data) def answer( self, text: str = None, show_alert: bool = False, url: str = None, cache_time: int = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.answer_callback_query( callback_query_id=self.id, text=text, show_alert=show_alert, url=url, cache_time=cache_time, timeout=timeout, api_kwargs=api_kwargs, ) def edit_message_text( self, text: str, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, reply_markup: 'InlineKeyboardMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, ) -> Union[Message, bool]: """Shortcut for either:: update.callback_query.message.edit_text(text, *args, **kwargs) or:: bot.edit_message_text(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`. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise :obj:`True` is returned. """ if self.inline_message_id: return self.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, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, entities=entities, chat_id=None, message_id=None, ) return self.message.edit_text( text=text, parse_mode=parse_mode, disable_web_page_preview=disable_web_page_preview, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, entities=entities, ) def edit_message_caption( self, caption: str = None, reply_markup: 'InlineKeyboardMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, parse_mode: ODVInput[str] = DEFAULT_NONE, api_kwargs: JSONDict = None, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, ) -> Union[Message, bool]: """Shortcut for either:: update.callback_query.message.edit_caption(caption, *args, **kwargs) or:: bot.edit_message_caption(caption=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`. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise :obj:`True` is returned. """ if self.inline_message_id: return self.bot.edit_message_caption( caption=caption, inline_message_id=self.inline_message_id, reply_markup=reply_markup, timeout=timeout, parse_mode=parse_mode, api_kwargs=api_kwargs, caption_entities=caption_entities, chat_id=None, message_id=None, ) return self.message.edit_caption( caption=caption, reply_markup=reply_markup, timeout=timeout, parse_mode=parse_mode, api_kwargs=api_kwargs, caption_entities=caption_entities, ) def edit_message_reply_markup( self, reply_markup: Optional['InlineKeyboardMarkup'] = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Union[Message, bool]: """Shortcut for either:: update.callback_query.message.edit_reply_markup( reply_markup=reply_markup, *args, **kwargs ) or:: bot.edit_message_reply_markup inline_message_id=update.callback_query.inline_message_id, reply_markup=reply_markup, *args, **kwargs ) For the documentation of the arguments, please see :meth:`telegram.Bot.edit_message_reply_markup` and :meth:`telegram.Message.edit_reply_markup`. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise :obj:`True` is returned. """ if self.inline_message_id: return self.bot.edit_message_reply_markup( reply_markup=reply_markup, inline_message_id=self.inline_message_id, timeout=timeout, api_kwargs=api_kwargs, chat_id=None, message_id=None, ) return self.message.edit_reply_markup( reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, ) def edit_message_media( self, media: 'InputMedia' = None, reply_markup: 'InlineKeyboardMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Union[Message, bool]: """Shortcut for either:: update.callback_query.message.edit_media(*args, **kwargs) or:: 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`. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise :obj:`True` is returned. """ if self.inline_message_id: return self.bot.edit_message_media( inline_message_id=self.inline_message_id, media=media, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, chat_id=None, message_id=None, ) return self.message.edit_media( media=media, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, ) def edit_message_live_location( self, latitude: float = None, longitude: float = None, location: Location = None, reply_markup: 'InlineKeyboardMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, horizontal_accuracy: float = None, heading: int = None, proximity_alert_radius: int = None, ) -> Union[Message, bool]: """Shortcut for either:: update.callback_query.message.edit_live_location(*args, **kwargs) or:: 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`. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise :obj:`True` is returned. """ if self.inline_message_id: return self.bot.edit_message_live_location( inline_message_id=self.inline_message_id, latitude=latitude, longitude=longitude, location=location, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, horizontal_accuracy=horizontal_accuracy, heading=heading, proximity_alert_radius=proximity_alert_radius, chat_id=None, message_id=None, ) return self.message.edit_live_location( latitude=latitude, longitude=longitude, location=location, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, horizontal_accuracy=horizontal_accuracy, heading=heading, proximity_alert_radius=proximity_alert_radius, ) def stop_message_live_location( self, reply_markup: 'InlineKeyboardMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Union[Message, bool]: """Shortcut for either:: update.callback_query.message.stop_live_location(*args, **kwargs) or:: 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`. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise :obj:`True` is returned. """ if self.inline_message_id: return self.bot.stop_message_live_location( inline_message_id=self.inline_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, chat_id=None, message_id=None, ) return self.message.stop_live_location( reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, ) def set_game_score( self, user_id: Union[int, str], score: int, force: bool = None, disable_edit_message: bool = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Union[Message, bool]: """Shortcut for either:: update.callback_query.message.set_game_score(*args, **kwargs) or:: 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`. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise :obj:`True` is returned. """ if self.inline_message_id: return self.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, timeout=timeout, api_kwargs=api_kwargs, chat_id=None, message_id=None, ) return self.message.set_game_score( user_id=user_id, score=score, force=force, disable_edit_message=disable_edit_message, timeout=timeout, api_kwargs=api_kwargs, ) def get_game_high_scores( self, user_id: Union[int, str], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> List['GameHighScore']: """Shortcut for either:: update.callback_query.message.get_game_high_score(*args, **kwargs) or:: 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_score`. Returns: List[:class:`telegram.GameHighScore`] """ if self.inline_message_id: return self.bot.get_game_high_scores( inline_message_id=self.inline_message_id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs, chat_id=None, message_id=None, ) return self.message.get_game_high_scores( user_id=user_id, timeout=timeout, api_kwargs=api_kwargs, ) def delete_message( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: update.callback_query.message.delete(*args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Message.delete`. Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.message.delete( timeout=timeout, api_kwargs=api_kwargs, ) def pin_message( self, disable_notification: ODVInput[bool] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: update.callback_query.message.pin(*args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Message.pin`. Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.message.pin( disable_notification=disable_notification, timeout=timeout, api_kwargs=api_kwargs, ) def unpin_message( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: update.callback_query.message.unpin(*args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Message.unpin`. Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.message.unpin( timeout=timeout, api_kwargs=api_kwargs, ) def copy_message( self, chat_id: Union[int, str], caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, ) -> 'MessageId': """Shortcut for:: update.callback_query.message.copy( chat_id, 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`. Returns: :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. """ return self.message.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, timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) MAX_ANSWER_TEXT_LENGTH: ClassVar[int] = constants.MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH """ :const:`telegram.constants.MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH` .. versionadded:: 13.2 """ python-telegram-bot-13.11/telegram/chat.py000066400000000000000000001756261417656324400205460ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=W0622 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" import warnings from datetime import datetime from typing import TYPE_CHECKING, List, Optional, ClassVar, Union, Tuple, Any from telegram import ChatPhoto, TelegramObject, constants from telegram.utils.types import JSONDict, FileInput, ODVInput, DVInput from telegram.utils.deprecate import TelegramDeprecationWarning from .chatpermissions import ChatPermissions from .chatlocation import ChatLocation from .utils.helpers import DEFAULT_NONE, DEFAULT_20 if TYPE_CHECKING: from telegram import ( Bot, ChatMember, ChatInviteLink, Message, MessageId, ReplyMarkup, Contact, InlineKeyboardMarkup, Location, Venue, MessageEntity, InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo, PhotoSize, Audio, Document, Animation, LabeledPrice, Sticker, 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. 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 'private', 'group', 'supergroup' or '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 bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. 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`. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: id (:obj:`int`): Unique identifier for this chat. type (:obj:`str`): Type of chat. title (:obj:`str`): Optional. Title, for supergroups, channels and group chats. username (:obj:`str`): Optional. Username. 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. 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. .. versionadded:: 13.9 description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats. 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. .. versionadded:: 13.9 sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set. can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the sticker set. 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`. """ __slots__ = ( 'bio', 'id', 'type', 'last_name', 'bot', 'sticker_set_name', 'slow_mode_delay', 'location', 'first_name', 'permissions', 'invite_link', 'pinned_message', 'description', 'can_set_sticker_set', 'username', 'title', 'photo', 'linked_chat_id', 'all_members_are_administrators', 'message_auto_delete_time', 'has_protected_content', 'has_private_forwards', '_id_attrs', ) SENDER: ClassVar[str] = constants.CHAT_SENDER """:const:`telegram.constants.CHAT_SENDER` .. versionadded:: 13.5 """ PRIVATE: ClassVar[str] = constants.CHAT_PRIVATE """:const:`telegram.constants.CHAT_PRIVATE`""" GROUP: ClassVar[str] = constants.CHAT_GROUP """:const:`telegram.constants.CHAT_GROUP`""" SUPERGROUP: ClassVar[str] = constants.CHAT_SUPERGROUP """:const:`telegram.constants.CHAT_SUPERGROUP`""" CHANNEL: ClassVar[str] = constants.CHAT_CHANNEL """:const:`telegram.constants.CHAT_CHANNEL`""" def __init__( self, id: int, type: str, title: str = None, username: str = None, first_name: str = None, last_name: str = None, bot: 'Bot' = None, photo: ChatPhoto = None, description: str = None, invite_link: str = None, pinned_message: 'Message' = None, permissions: ChatPermissions = None, sticker_set_name: str = None, can_set_sticker_set: bool = None, slow_mode_delay: int = None, bio: str = None, linked_chat_id: int = None, location: ChatLocation = None, message_auto_delete_time: int = None, has_private_forwards: bool = None, has_protected_content: bool = None, **_kwargs: Any, ): # Required self.id = int(id) # pylint: disable=C0103 self.type = type # Optionals self.title = title self.username = username self.first_name = first_name self.last_name = last_name # TODO: Remove (also from tests), when Telegram drops this completely self.all_members_are_administrators = _kwargs.get('all_members_are_administrators') self.photo = photo self.bio = bio self.has_private_forwards = has_private_forwards self.description = description self.invite_link = invite_link self.pinned_message = pinned_message self.permissions = permissions self.slow_mode_delay = slow_mode_delay self.message_auto_delete_time = ( int(message_auto_delete_time) if message_auto_delete_time is not None else None ) self.has_protected_content = has_protected_content self.sticker_set_name = sticker_set_name self.can_set_sticker_set = can_set_sticker_set self.linked_chat_id = linked_chat_id self.location = location self.bot = bot self._id_attrs = (self.id,) @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 data['photo'] = ChatPhoto.de_json(data.get('photo'), bot) from telegram import Message # pylint: disable=C0415 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) return cls(bot=bot, **data) def leave(self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None) -> bool: """Shortcut for:: 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 self.bot.leave_chat( chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs, ) def get_administrators( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> List['ChatMember']: """Shortcut for:: 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: List[:class:`telegram.ChatMember`]: A list 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 self.bot.get_chat_administrators( chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs, ) def get_members_count( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> int: """ Deprecated, use :func:`~telegram.Chat.get_member_count` instead. .. deprecated:: 13.7 """ warnings.warn( '`Chat.get_members_count` is deprecated. Use `Chat.get_member_count` instead.', TelegramDeprecationWarning, stacklevel=2, ) return self.get_member_count( timeout=timeout, api_kwargs=api_kwargs, ) def get_member_count( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> int: """Shortcut for:: 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 self.bot.get_chat_member_count( chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs, ) def get_member( self, user_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> 'ChatMember': """Shortcut for:: 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 self.bot.get_chat_member( chat_id=self.id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs, ) def kick_member( self, user_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, until_date: Union[int, datetime] = None, api_kwargs: JSONDict = None, revoke_messages: bool = None, ) -> bool: """ Deprecated, use :func:`~telegram.Chat.ban_member` instead. .. deprecated:: 13.7 """ warnings.warn( '`Chat.kick_member` is deprecated. Use `Chat.ban_member` instead.', TelegramDeprecationWarning, stacklevel=2, ) return self.ban_member( user_id=user_id, timeout=timeout, until_date=until_date, api_kwargs=api_kwargs, revoke_messages=revoke_messages, ) def ban_member( self, user_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, until_date: Union[int, datetime] = None, api_kwargs: JSONDict = None, revoke_messages: bool = None, ) -> bool: """Shortcut for:: 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 self.bot.ban_chat_member( chat_id=self.id, user_id=user_id, timeout=timeout, until_date=until_date, api_kwargs=api_kwargs, revoke_messages=revoke_messages, ) def ban_sender_chat( self, sender_chat_id: int, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.ban_chat_sender_chat( chat_id=self.id, sender_chat_id=sender_chat_id, timeout=timeout, api_kwargs=api_kwargs ) def ban_chat( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.ban_chat_sender_chat( chat_id=chat_id, sender_chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs ) def unban_sender_chat( self, sender_chat_id: int, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.unban_chat_sender_chat( chat_id=self.id, sender_chat_id=sender_chat_id, timeout=timeout, api_kwargs=api_kwargs ) def unban_chat( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.unban_chat_sender_chat( chat_id=chat_id, sender_chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs ) def unban_member( self, user_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, only_if_banned: bool = None, ) -> bool: """Shortcut for:: 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 self.bot.unban_chat_member( chat_id=self.id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs, only_if_banned=only_if_banned, ) def promote_member( self, user_id: Union[str, int], can_change_info: bool = None, can_post_messages: bool = None, can_edit_messages: bool = None, can_delete_messages: bool = None, can_invite_users: bool = None, can_restrict_members: bool = None, can_pin_messages: bool = None, can_promote_members: bool = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, is_anonymous: bool = None, can_manage_chat: bool = None, can_manage_voice_chats: bool = None, ) -> bool: """Shortcut for:: 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 Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.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, timeout=timeout, api_kwargs=api_kwargs, is_anonymous=is_anonymous, can_manage_chat=can_manage_chat, can_manage_voice_chats=can_manage_voice_chats, ) def restrict_member( self, user_id: Union[str, int], permissions: ChatPermissions, until_date: Union[int, datetime] = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.bot.restrict_chat_member( chat_id=self.id, user_id=user_id, permissions=permissions, until_date=until_date, timeout=timeout, api_kwargs=api_kwargs, ) def set_permissions( self, permissions: ChatPermissions, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: bot.set_chat_permissions(update.effective_chat.id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.set_chat_permissions`. Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.bot.set_chat_permissions( chat_id=self.id, permissions=permissions, timeout=timeout, api_kwargs=api_kwargs, ) def set_administrator_custom_title( self, user_id: Union[int, str], custom_title: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.set_chat_administrator_custom_title( chat_id=self.id, user_id=user_id, custom_title=custom_title, timeout=timeout, api_kwargs=api_kwargs, ) def pin_message( self, message_id: int, disable_notification: ODVInput[bool] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.pin_chat_message( chat_id=self.id, message_id=message_id, disable_notification=disable_notification, timeout=timeout, api_kwargs=api_kwargs, ) def unpin_message( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, message_id: int = None, ) -> bool: """Shortcut for:: 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 self.bot.unpin_chat_message( chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs, message_id=message_id, ) def unpin_all_messages( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.unpin_all_chat_messages( chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs, ) def send_message( self, text: str, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.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, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, ) def send_media_group( self, media: List[ Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo'] ], disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> List['Message']: """Shortcut for:: 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: List[:class:`telegram.Message`]: On success, instance representing the message posted. """ return self.bot.send_media_group( chat_id=self.id, media=media, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def send_chat_action( self, action: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.send_chat_action( chat_id=self.id, action=action, timeout=timeout, api_kwargs=api_kwargs, ) send_action = send_chat_action """Alias for :attr:`send_chat_action`""" def send_photo( self, photo: Union[FileInput, 'PhotoSize'], caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.bot.send_photo( chat_id=self.id, photo=photo, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=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, ) def send_contact( self, phone_number: str = None, first_name: str = None, last_name: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, contact: 'Contact' = None, vcard: str = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.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_markup=reply_markup, timeout=timeout, contact=contact, vcard=vcard, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def send_audio( self, audio: Union[FileInput, 'Audio'], duration: int = None, performer: str = None, title: str = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.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_markup=reply_markup, timeout=timeout, parse_mode=parse_mode, thumb=thumb, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, filename=filename, protect_content=protect_content, ) def send_document( self, document: Union[FileInput, 'Document'], filename: str = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb: FileInput = None, api_kwargs: JSONDict = None, disable_content_type_detection: bool = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.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_markup=reply_markup, timeout=timeout, parse_mode=parse_mode, thumb=thumb, 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, ) def send_dice( self, disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, emoji: str = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.bot.send_dice( chat_id=self.id, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, emoji=emoji, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def send_game( self, game_short_name: str, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'InlineKeyboardMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.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_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def send_invoice( self, title: str, description: str, payload: str, provider_token: str, currency: str, prices: List['LabeledPrice'], start_parameter: str = None, photo_url: str = None, photo_size: int = None, photo_width: int = None, photo_height: int = None, need_name: bool = None, need_phone_number: bool = None, need_email: bool = None, need_shipping_address: bool = None, is_flexible: bool = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'InlineKeyboardMarkup' = None, provider_data: Union[str, object] = None, send_phone_number_to_provider: bool = None, send_email_to_provider: bool = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 :attr:`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 :attr:`start_parameter` is optional. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.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, timeout=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, ) def send_location( self, latitude: float = None, longitude: float = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, location: 'Location' = None, live_period: int = None, api_kwargs: JSONDict = None, horizontal_accuracy: float = None, heading: int = None, proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.bot.send_location( chat_id=self.id, latitude=latitude, longitude=longitude, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=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, ) def send_animation( self, animation: Union[FileInput, 'Animation'], duration: int = None, width: int = None, height: int = None, thumb: FileInput = None, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.bot.send_animation( chat_id=self.id, animation=animation, duration=duration, width=width, height=height, thumb=thumb, caption=caption, parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, filename=filename, protect_content=protect_content, ) def send_sticker( self, sticker: Union[FileInput, 'Sticker'], disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.bot.send_sticker( chat_id=self.id, sticker=sticker, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def send_venue( self, latitude: float = None, longitude: float = None, title: str = None, address: str = None, foursquare_id: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, venue: 'Venue' = None, foursquare_type: str = None, api_kwargs: JSONDict = None, google_place_id: str = None, google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.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_markup=reply_markup, timeout=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, ) def send_video( self, video: Union[FileInput, 'Video'], duration: int = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, width: int = None, height: int = None, parse_mode: ODVInput[str] = DEFAULT_NONE, supports_streaming: bool = None, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.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_markup=reply_markup, timeout=timeout, width=width, height=height, parse_mode=parse_mode, supports_streaming=supports_streaming, thumb=thumb, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, filename=filename, protect_content=protect_content, ) def send_video_note( self, video_note: Union[FileInput, 'VideoNote'], duration: int = None, length: int = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.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_markup=reply_markup, timeout=timeout, thumb=thumb, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, ) def send_voice( self, voice: Union[FileInput, 'Voice'], duration: int = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.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_markup=reply_markup, timeout=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, ) def send_poll( self, question: str, options: List[str], is_anonymous: bool = True, # We use constant.POLL_REGULAR instead of Poll.REGULAR here to avoid circular imports type: str = constants.POLL_REGULAR, # pylint: disable=W0622 allows_multiple_answers: bool = False, correct_option_id: int = None, is_closed: bool = None, disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, explanation: str = None, explanation_parse_mode: ODVInput[str] = DEFAULT_NONE, open_period: int = None, close_date: Union[int, datetime] = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 self.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_markup=reply_markup, timeout=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, ) def send_copy( self, from_chat_id: Union[str, int], message_id: int, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, ) -> 'MessageId': """Shortcut for:: bot.copy_message(chat_id=update.effective_chat.id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.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, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) def copy_message( self, chat_id: Union[int, str], message_id: int, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, ) -> 'MessageId': """Shortcut for:: 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`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.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, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) def export_invite_link( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> str: """Shortcut for:: 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 self.bot.export_chat_invite_link( chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs ) def create_invite_link( self, expire_date: Union[int, datetime] = None, member_limit: int = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, name: str = None, creates_join_request: bool = None, ) -> 'ChatInviteLink': """Shortcut for:: 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 self.bot.create_chat_invite_link( chat_id=self.id, expire_date=expire_date, member_limit=member_limit, timeout=timeout, api_kwargs=api_kwargs, name=name, creates_join_request=creates_join_request, ) def edit_invite_link( self, invite_link: str, expire_date: Union[int, datetime] = None, member_limit: int = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, name: str = None, creates_join_request: bool = None, ) -> 'ChatInviteLink': """Shortcut for:: 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 self.bot.edit_chat_invite_link( chat_id=self.id, invite_link=invite_link, expire_date=expire_date, member_limit=member_limit, timeout=timeout, api_kwargs=api_kwargs, name=name, creates_join_request=creates_join_request, ) def revoke_invite_link( self, invite_link: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> 'ChatInviteLink': """Shortcut for:: 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 self.bot.revoke_chat_invite_link( chat_id=self.id, invite_link=invite_link, timeout=timeout, api_kwargs=api_kwargs ) def approve_join_request( self, user_id: int, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.approve_chat_join_request( chat_id=self.id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs ) def decline_join_request( self, user_id: int, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.decline_chat_join_request( chat_id=self.id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs ) python-telegram-bot-13.11/telegram/chataction.py000066400000000000000000000064331417656324400217310ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 ChatAction.""" from typing import ClassVar from telegram import constants from telegram.utils.deprecate import set_new_attribute_deprecated class ChatAction: """Helper class to provide constants for different chat actions.""" __slots__ = ('__dict__',) # Adding __dict__ here since it doesn't subclass TGObject FIND_LOCATION: ClassVar[str] = constants.CHATACTION_FIND_LOCATION """:const:`telegram.constants.CHATACTION_FIND_LOCATION`""" RECORD_AUDIO: ClassVar[str] = constants.CHATACTION_RECORD_AUDIO """:const:`telegram.constants.CHATACTION_RECORD_AUDIO` .. deprecated:: 13.5 Deprecated by Telegram. Use :attr:`RECORD_VOICE` instead. """ RECORD_VOICE: ClassVar[str] = constants.CHATACTION_RECORD_VOICE """:const:`telegram.constants.CHATACTION_RECORD_VOICE` .. versionadded:: 13.5 """ RECORD_VIDEO: ClassVar[str] = constants.CHATACTION_RECORD_VIDEO """:const:`telegram.constants.CHATACTION_RECORD_VIDEO`""" RECORD_VIDEO_NOTE: ClassVar[str] = constants.CHATACTION_RECORD_VIDEO_NOTE """:const:`telegram.constants.CHATACTION_RECORD_VIDEO_NOTE`""" TYPING: ClassVar[str] = constants.CHATACTION_TYPING """:const:`telegram.constants.CHATACTION_TYPING`""" UPLOAD_AUDIO: ClassVar[str] = constants.CHATACTION_UPLOAD_AUDIO """:const:`telegram.constants.CHATACTION_UPLOAD_AUDIO` .. deprecated:: 13.5 Deprecated by Telegram. Use :attr:`UPLOAD_VOICE` instead. """ UPLOAD_VOICE: ClassVar[str] = constants.CHATACTION_UPLOAD_VOICE """:const:`telegram.constants.CHATACTION_UPLOAD_VOICE` .. versionadded:: 13.5 """ UPLOAD_DOCUMENT: ClassVar[str] = constants.CHATACTION_UPLOAD_DOCUMENT """:const:`telegram.constants.CHATACTION_UPLOAD_DOCUMENT`""" CHOOSE_STICKER: ClassVar[str] = constants.CHATACTION_CHOOSE_STICKER """:const:`telegram.constants.CHOOSE_STICKER` .. versionadded:: 13.8""" UPLOAD_PHOTO: ClassVar[str] = constants.CHATACTION_UPLOAD_PHOTO """:const:`telegram.constants.CHATACTION_UPLOAD_PHOTO`""" UPLOAD_VIDEO: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO """:const:`telegram.constants.CHATACTION_UPLOAD_VIDEO`""" UPLOAD_VIDEO_NOTE: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO_NOTE """:const:`telegram.constants.CHATACTION_UPLOAD_VIDEO_NOTE`""" def __setattr__(self, key: str, value: object) -> None: set_new_attribute_deprecated(self, key, value) python-telegram-bot-13.11/telegram/chatinvitelink.py000066400000000000000000000127211417656324400226250ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional from telegram import TelegramObject, User from telegram.utils.helpers import from_timestamp, to_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:`is_primary` and :attr:`is_revoked` are equal. .. versionadded:: 13.4 Args: invite_link (:obj:`str`): The invite link. creator (:class:`telegram.User`): Creator of the link. 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. 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; 1-99999. name (:obj:`str`, optional): Invite link name. .. 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. .. 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. 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. 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; 1-99999. name (:obj:`str`): Optional. Invite link name. .. 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. .. versionadded:: 13.8 pending_join_request_count (:obj:`int`): Optional. Number of pending join requests created using this link. .. versionadded:: 13.8 """ __slots__ = ( 'invite_link', 'creator', 'is_primary', 'is_revoked', 'expire_date', 'member_limit', 'name', 'creates_join_request', 'pending_join_request_count', '_id_attrs', ) def __init__( self, invite_link: str, creator: User, is_primary: bool, is_revoked: bool, expire_date: datetime.datetime = None, member_limit: int = None, name: str = None, creates_join_request: bool = None, pending_join_request_count: int = None, **_kwargs: Any, ): # Required self.invite_link = invite_link self.creator = creator self.is_primary = is_primary self.is_revoked = is_revoked # Optionals self.expire_date = expire_date self.member_limit = int(member_limit) if member_limit is not None else None self.name = name self.creates_join_request = creates_join_request self.pending_join_request_count = ( int(pending_join_request_count) if pending_join_request_count is not None else None ) self._id_attrs = (self.invite_link, self.creator, self.is_primary, self.is_revoked) @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 data['creator'] = User.de_json(data.get('creator'), bot) data['expire_date'] = from_timestamp(data.get('expire_date', None)) return cls(**data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['expire_date'] = to_timestamp(self.expire_date) return data python-telegram-bot-13.11/telegram/chatjoinrequest.py000066400000000000000000000126511417656324400230230ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional from telegram import TelegramObject, User, Chat, ChatInviteLink from telegram.utils.helpers import from_timestamp, to_timestamp, 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. .. versionadded:: 13.8 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. 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. 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. 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. """ __slots__ = ( 'chat', 'from_user', 'date', 'bio', 'invite_link', 'bot', '_id_attrs', ) def __init__( self, chat: Chat, from_user: User, date: datetime.datetime, bio: str = None, invite_link: ChatInviteLink = None, bot: 'Bot' = None, **_kwargs: Any, ): # Required self.chat = chat self.from_user = from_user self.date = date # Optionals self.bio = bio self.invite_link = invite_link self.bot = bot self._id_attrs = (self.chat, self.from_user, self.date) @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 data['chat'] = Chat.de_json(data.get('chat'), bot) data['from_user'] = User.de_json(data.get('from'), bot) data['date'] = from_timestamp(data.get('date', None)) data['invite_link'] = ChatInviteLink.de_json(data.get('invite_link'), bot) return cls(bot=bot, **data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['date'] = to_timestamp(self.date) return data def approve( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.approve_chat_join_request( chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs ) def decline( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.decline_chat_join_request( chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs ) python-telegram-bot-13.11/telegram/chatlocation.py000066400000000000000000000047211417656324400222620ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional from telegram import TelegramObject from telegram.utils.types import JSONDict from .files.location import Location 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; 1-64 characters, as defined by the chat owner **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: location (:class:`telegram.Location`): The location to which the supergroup is connected. address (:obj:`str`): Location address, as defined by the chat owner """ __slots__ = ('location', '_id_attrs', 'address') def __init__( self, location: Location, address: str, **_kwargs: Any, ): self.location = location self.address = address self._id_attrs = (self.location,) @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 cls(bot=bot, **data) python-telegram-bot-13.11/telegram/chatmember.py000066400000000000000000000675731417656324400217370ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional, ClassVar, Dict, Type from telegram import TelegramObject, User, constants from telegram.utils.helpers import from_timestamp, to_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. Note: 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 deprecated and you should no longer use :class:`ChatMember` directly. 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.CREATOR`, :attr:`~telegram.ChatMember.KICKED`, :attr:`~telegram.ChatMember.LEFT`, :attr:`~telegram.ChatMember.MEMBER` or :attr:`~telegram.ChatMember.RESTRICTED`. custom_title (:obj:`str`, optional): Owner and administrators only. Custom title for this user. .. deprecated:: 13.7 is_anonymous (:obj:`bool`, optional): Owner and administrators only. :obj:`True`, if the user's presence in the chat is hidden. .. deprecated:: 13.7 until_date (:class:`datetime.datetime`, optional): Restricted and kicked only. Date when restrictions will be lifted for this user. .. deprecated:: 13.7 can_be_edited (:obj:`bool`, optional): Administrators only. :obj:`True`, if the bot is allowed to edit administrator privileges of that user. .. deprecated:: 13.7 can_manage_chat (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege. .. versionadded:: 13.4 .. deprecated:: 13.7 can_manage_voice_chats (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can manage voice chats. .. versionadded:: 13.4 .. deprecated:: 13.7 can_change_info (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`, if the user can change the chat title, photo and other settings. .. deprecated:: 13.7 can_post_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can post in the channel, channels only. .. deprecated:: 13.7 can_edit_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can edit messages of other users and can pin messages; channels only. .. deprecated:: 13.7 can_delete_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can delete messages of other users. .. deprecated:: 13.7 can_invite_users (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`, if the user can invite new users to the chat. .. deprecated:: 13.7 can_restrict_members (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can restrict, ban or unban chat members. .. deprecated:: 13.7 can_pin_messages (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`, if the user can pin messages, groups and supergroups only. .. deprecated:: 13.7 can_promote_members (:obj:`bool`, optional): Administrators only. :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). .. deprecated:: 13.7 is_member (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is a member of the chat at the moment of the request. .. deprecated:: 13.7 can_send_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user can send text messages, contacts, locations and venues. .. deprecated:: 13.7 can_send_media_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user can send audios, documents, photos, videos, video notes and voice notes. .. deprecated:: 13.7 can_send_polls (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is allowed to send polls. .. deprecated:: 13.7 can_send_other_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user can send animations, games, stickers and use inline bots. .. deprecated:: 13.7 can_add_web_page_previews (:obj:`bool`, optional): Restricted only. :obj:`True`, if user may add web page previews to his messages. .. deprecated:: 13.7 Attributes: user (:class:`telegram.User`): Information about the user. status (:obj:`str`): The member's status in the chat. custom_title (:obj:`str`): Optional. Custom title for owner and administrators. .. deprecated:: 13.7 is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's presence in the chat is hidden. .. deprecated:: 13.7 until_date (:class:`datetime.datetime`): Optional. Date when restrictions will be lifted for this user. .. deprecated:: 13.7 can_be_edited (:obj:`bool`): Optional. If the bot is allowed to edit administrator privileges of that user. .. deprecated:: 13.7 can_manage_chat (:obj:`bool`): Optional. If the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. .. versionadded:: 13.4 .. deprecated:: 13.7 can_manage_voice_chats (:obj:`bool`): Optional. if the administrator can manage voice chats. .. versionadded:: 13.4 .. deprecated:: 13.7 can_change_info (:obj:`bool`): Optional. If the user can change the chat title, photo and other settings. .. deprecated:: 13.7 can_post_messages (:obj:`bool`): Optional. If the administrator can post in the channel. .. deprecated:: 13.7 can_edit_messages (:obj:`bool`): Optional. If the administrator can edit messages of other users. .. deprecated:: 13.7 can_delete_messages (:obj:`bool`): Optional. If the administrator can delete messages of other users. .. deprecated:: 13.7 can_invite_users (:obj:`bool`): Optional. If the user can invite new users to the chat. .. deprecated:: 13.7 can_restrict_members (:obj:`bool`): Optional. If the administrator can restrict, ban or unban chat members. .. deprecated:: 13.7 can_pin_messages (:obj:`bool`): Optional. If the user can pin messages. .. deprecated:: 13.7 can_promote_members (:obj:`bool`): Optional. If the administrator can add new administrators. .. deprecated:: 13.7 is_member (:obj:`bool`): Optional. Restricted only. :obj:`True`, if the user is a member of the chat at the moment of the request. .. deprecated:: 13.7 can_send_messages (:obj:`bool`): Optional. If the user can send text messages, contacts, locations and venues. .. deprecated:: 13.7 can_send_media_messages (:obj:`bool`): Optional. If the user can send media messages, implies can_send_messages. .. deprecated:: 13.7 can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send polls. .. deprecated:: 13.7 can_send_other_messages (:obj:`bool`): Optional. If the user can send animations, games, stickers and use inline bots, implies can_send_media_messages. .. deprecated:: 13.7 can_add_web_page_previews (:obj:`bool`): Optional. If user may add web page previews to his messages, implies can_send_media_messages .. deprecated:: 13.7 """ __slots__ = ( 'is_member', 'can_restrict_members', 'can_delete_messages', 'custom_title', 'can_be_edited', 'can_post_messages', 'can_send_messages', 'can_edit_messages', 'can_send_media_messages', 'is_anonymous', 'can_add_web_page_previews', 'can_send_other_messages', 'can_invite_users', 'can_send_polls', 'user', 'can_promote_members', 'status', 'can_change_info', 'can_pin_messages', 'can_manage_chat', 'can_manage_voice_chats', 'until_date', '_id_attrs', ) ADMINISTRATOR: ClassVar[str] = constants.CHATMEMBER_ADMINISTRATOR """:const:`telegram.constants.CHATMEMBER_ADMINISTRATOR`""" CREATOR: ClassVar[str] = constants.CHATMEMBER_CREATOR """:const:`telegram.constants.CHATMEMBER_CREATOR`""" KICKED: ClassVar[str] = constants.CHATMEMBER_KICKED """:const:`telegram.constants.CHATMEMBER_KICKED`""" LEFT: ClassVar[str] = constants.CHATMEMBER_LEFT """:const:`telegram.constants.CHATMEMBER_LEFT`""" MEMBER: ClassVar[str] = constants.CHATMEMBER_MEMBER """:const:`telegram.constants.CHATMEMBER_MEMBER`""" RESTRICTED: ClassVar[str] = constants.CHATMEMBER_RESTRICTED """:const:`telegram.constants.CHATMEMBER_RESTRICTED`""" def __init__( self, user: User, status: str, until_date: datetime.datetime = None, can_be_edited: bool = None, can_change_info: bool = None, can_post_messages: bool = None, can_edit_messages: bool = None, can_delete_messages: bool = None, can_invite_users: bool = None, can_restrict_members: bool = None, can_pin_messages: bool = None, can_promote_members: bool = None, can_send_messages: bool = None, can_send_media_messages: bool = None, can_send_polls: bool = None, can_send_other_messages: bool = None, can_add_web_page_previews: bool = None, is_member: bool = None, custom_title: str = None, is_anonymous: bool = None, can_manage_chat: bool = None, can_manage_voice_chats: bool = None, **_kwargs: Any, ): # Required self.user = user self.status = status # Optionals self.custom_title = custom_title self.is_anonymous = is_anonymous self.until_date = until_date self.can_be_edited = can_be_edited self.can_change_info = can_change_info self.can_post_messages = can_post_messages self.can_edit_messages = can_edit_messages self.can_delete_messages = can_delete_messages self.can_invite_users = can_invite_users self.can_restrict_members = can_restrict_members self.can_pin_messages = can_pin_messages self.can_promote_members = can_promote_members self.can_send_messages = can_send_messages self.can_send_media_messages = can_send_media_messages self.can_send_polls = can_send_polls self.can_send_other_messages = can_send_other_messages self.can_add_web_page_previews = can_add_web_page_previews self.is_member = is_member self.can_manage_chat = can_manage_chat self.can_manage_voice_chats = can_manage_voice_chats self._id_attrs = (self.user, self.status) @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 data['user'] = User.de_json(data.get('user'), bot) data['until_date'] = from_timestamp(data.get('until_date', None)) _class_mapping: Dict[str, Type['ChatMember']] = { cls.CREATOR: ChatMemberOwner, cls.ADMINISTRATOR: ChatMemberAdministrator, cls.MEMBER: ChatMemberMember, cls.RESTRICTED: ChatMemberRestricted, cls.LEFT: ChatMemberLeft, cls.KICKED: ChatMemberBanned, } if cls is ChatMember: return _class_mapping.get(data['status'], cls)(**data, bot=bot) return cls(**data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['until_date'] = to_timestamp(self.until_date) return data 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. custom_title (:obj:`str`, optional): Custom title for this user. is_anonymous (:obj:`bool`, optional): :obj:`True`, if the user's presence in the chat is hidden. Attributes: status (:obj:`str`): The member's status in the chat, always :attr:`telegram.ChatMember.CREATOR`. user (:class:`telegram.User`): Information about the user. custom_title (:obj:`str`): Optional. Custom title for this user. is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's presence in the chat is hidden. """ __slots__ = () def __init__( self, user: User, custom_title: str = None, is_anonymous: bool = None, **_kwargs: Any, ): super().__init__( status=ChatMember.CREATOR, user=user, custom_title=custom_title, is_anonymous=is_anonymous, ) class ChatMemberAdministrator(ChatMember): """ Represents a chat member that has some additional privileges. .. versionadded:: 13.7 Args: user (:class:`telegram.User`): Information about the user. can_be_edited (:obj:`bool`, optional): :obj:`True`, if the bot is allowed to edit administrator privileges of that user. custom_title (:obj:`str`, optional): Custom title for this user. is_anonymous (:obj:`bool`, optional): :obj:`True`, if the user's presence in the chat is hidden. can_manage_chat (:obj:`bool`, optional): :obj:`True`, if the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege. can_post_messages (:obj:`bool`, optional): :obj:`True`, if the administrator can post in the channel, channels only. can_edit_messages (:obj:`bool`, optional): :obj:`True`, if the administrator can edit messages of other users and can pin messages; channels only. can_delete_messages (:obj:`bool`, optional): :obj:`True`, if the administrator can delete messages of other users. can_manage_voice_chats (:obj:`bool`, optional): :obj:`True`, if the administrator can manage voice chats. can_restrict_members (:obj:`bool`, optional): :obj:`True`, if the administrator can restrict, ban or unban chat members. can_promote_members (:obj:`bool`, optional): :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`, optional): :obj:`True`, if the user can change the chat title, photo and other settings. can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user can invite new users to the chat. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. Attributes: status (:obj:`str`): The member's status in the chat, always :attr:`telegram.ChatMember.ADMINISTRATOR`. user (:class:`telegram.User`): Information about the user. can_be_edited (:obj:`bool`): Optional. :obj:`True`, if the bot is allowed to edit administrator privileges of that user. custom_title (:obj:`str`): Optional. Custom title for this user. is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's presence in the chat is hidden. can_manage_chat (:obj:`bool`): Optional. :obj:`True`, if the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege. can_post_messages (:obj:`bool`): Optional. :obj:`True`, if the administrator can post in the channel, channels only. can_edit_messages (:obj:`bool`): Optional. :obj:`True`, if the administrator can edit messages of other users and can pin messages; channels only. can_delete_messages (:obj:`bool`): Optional. :obj:`True`, if the administrator can delete messages of other users. can_manage_voice_chats (:obj:`bool`): Optional. :obj:`True`, if the administrator can manage voice chats. can_restrict_members (:obj:`bool`): Optional. :obj:`True`, if the administrator can restrict, ban or unban chat members. can_promote_members (:obj:`bool`): Optional. :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`): Optional. :obj:`True`, if the user can change the chat title, photo and other settings. can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user can invite new users to the chat. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. """ __slots__ = () def __init__( self, user: User, can_be_edited: bool = None, custom_title: str = None, is_anonymous: bool = None, can_manage_chat: bool = None, can_post_messages: bool = None, can_edit_messages: bool = None, can_delete_messages: bool = None, can_manage_voice_chats: bool = None, can_restrict_members: bool = None, can_promote_members: bool = None, can_change_info: bool = None, can_invite_users: bool = None, can_pin_messages: bool = None, **_kwargs: Any, ): super().__init__( status=ChatMember.ADMINISTRATOR, user=user, can_be_edited=can_be_edited, custom_title=custom_title, is_anonymous=is_anonymous, can_manage_chat=can_manage_chat, can_post_messages=can_post_messages, can_edit_messages=can_edit_messages, can_delete_messages=can_delete_messages, can_manage_voice_chats=can_manage_voice_chats, can_restrict_members=can_restrict_members, can_promote_members=can_promote_members, can_change_info=can_change_info, can_invite_users=can_invite_users, can_pin_messages=can_pin_messages, ) 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 :attr:`telegram.ChatMember.MEMBER`. user (:class:`telegram.User`): Information about the user. """ __slots__ = () def __init__(self, user: User, **_kwargs: Any): super().__init__(status=ChatMember.MEMBER, user=user) class ChatMemberRestricted(ChatMember): """ Represents a chat member that is under certain restrictions in the chat. Supergroups only. .. versionadded:: 13.7 Args: user (:class:`telegram.User`): Information about the user. is_member (:obj:`bool`, optional): :obj:`True`, if the user is a member of the chat at the moment of the request. can_change_info (:obj:`bool`, optional): :obj:`True`, if the user can change the chat title, photo and other settings. can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user can invite new users to the chat. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send text messages, contacts, locations and venues. can_send_media_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send audios, documents, photos, videos, video notes and voice notes. 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. until_date (:class:`datetime.datetime`, optional): Date when restrictions will be lifted for this user. Attributes: status (:obj:`str`): The member's status in the chat, always :attr:`telegram.ChatMember.RESTRICTED`. user (:class:`telegram.User`): Information about the user. is_member (:obj:`bool`): Optional. :obj:`True`, if the user is a member of the chat at the moment of the request. can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user can change the chat title, photo and other settings. can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user can invite new users to the chat. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text messages, contacts, locations and venues. can_send_media_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send audios, documents, photos, videos, video notes and voice notes. 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. until_date (:class:`datetime.datetime`): Optional. Date when restrictions will be lifted for this user. """ __slots__ = () def __init__( self, user: User, is_member: bool = None, can_change_info: bool = None, can_invite_users: bool = None, can_pin_messages: bool = None, can_send_messages: bool = None, can_send_media_messages: bool = None, can_send_polls: bool = None, can_send_other_messages: bool = None, can_add_web_page_previews: bool = None, until_date: datetime.datetime = None, **_kwargs: Any, ): super().__init__( status=ChatMember.RESTRICTED, user=user, is_member=is_member, can_change_info=can_change_info, can_invite_users=can_invite_users, can_pin_messages=can_pin_messages, can_send_messages=can_send_messages, can_send_media_messages=can_send_media_messages, can_send_polls=can_send_polls, can_send_other_messages=can_send_other_messages, can_add_web_page_previews=can_add_web_page_previews, until_date=until_date, ) 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 :attr:`telegram.ChatMember.LEFT`. user (:class:`telegram.User`): Information about the user. """ __slots__ = () def __init__(self, user: User, **_kwargs: Any): super().__init__(status=ChatMember.LEFT, user=user) 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`, optional): Date when restrictions will be lifted for this user. Attributes: status (:obj:`str`): The member's status in the chat, always :attr:`telegram.ChatMember.KICKED`. user (:class:`telegram.User`): Information about the user. until_date (:class:`datetime.datetime`): Optional. Date when restrictions will be lifted for this user. """ __slots__ = () def __init__( self, user: User, until_date: datetime.datetime = None, **_kwargs: Any, ): super().__init__( status=ChatMember.KICKED, user=user, until_date=until_date, ) python-telegram-bot-13.11/telegram/chatmemberupdated.py000066400000000000000000000147121417656324400232710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional, Dict, Tuple, Union from telegram import TelegramObject, User, Chat, ChatMember, ChatInviteLink from telegram.utils.helpers import from_timestamp, to_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 ``from`` is a reserved word, use ``from_user`` instead. 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`. 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. 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`. 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. """ __slots__ = ( 'chat', 'from_user', 'date', 'old_chat_member', 'new_chat_member', 'invite_link', '_id_attrs', ) def __init__( self, chat: Chat, from_user: User, date: datetime.datetime, old_chat_member: ChatMember, new_chat_member: ChatMember, invite_link: ChatInviteLink = None, **_kwargs: Any, ): # Required self.chat = chat self.from_user = from_user self.date = date self.old_chat_member = old_chat_member self.new_chat_member = new_chat_member # Optionals self.invite_link = invite_link self._id_attrs = ( self.chat, self.from_user, self.date, self.old_chat_member, self.new_chat_member, ) @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 data['chat'] = Chat.de_json(data.get('chat'), bot) data['from_user'] = User.de_json(data.get('from'), bot) data['date'] = from_timestamp(data.get('date')) 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 cls(**data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() # Required data['date'] = to_timestamp(self.date) return data 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:: python >>> 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[:obj:`obj`, :obj:`obj`]]: 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.old_chat_member[attribute], self.new_chat_member[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-13.11/telegram/chatpermissions.py000066400000000000000000000135671417656324400230350ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject 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_media_messages`, :attr:`can_send_polls`, :attr:`can_send_other_messages`, :attr:`can_add_web_page_previews`, :attr:`can_change_info`, :attr:`can_invite_users` and :attr:`can_pin_messages` are equal. 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 behaviour 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_media_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send audios, documents, photos, videos, video notes and voice notes, implies :attr:`can_send_messages`. 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, implies :attr:`can_send_media_messages`. can_add_web_page_previews (:obj:`bool`, optional): :obj:`True`, if the user is allowed to add web page previews to their messages, implies :attr:`can_send_media_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. Attributes: can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text messages, contacts, locations and venues. can_send_media_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send audios, documents, photos, videos, video notes and voice notes, implies :attr:`can_send_messages`. 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, implies :attr:`can_send_media_messages`. can_add_web_page_previews (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to add web page previews to their messages, implies :attr:`can_send_media_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. """ __slots__ = ( 'can_send_other_messages', 'can_invite_users', 'can_send_polls', '_id_attrs', 'can_send_messages', 'can_send_media_messages', 'can_change_info', 'can_pin_messages', 'can_add_web_page_previews', ) def __init__( self, can_send_messages: bool = None, can_send_media_messages: bool = None, can_send_polls: bool = None, can_send_other_messages: bool = None, can_add_web_page_previews: bool = None, can_change_info: bool = None, can_invite_users: bool = None, can_pin_messages: bool = None, **_kwargs: Any, ): # Required self.can_send_messages = can_send_messages self.can_send_media_messages = can_send_media_messages self.can_send_polls = can_send_polls self.can_send_other_messages = can_send_other_messages self.can_add_web_page_previews = can_add_web_page_previews self.can_change_info = can_change_info self.can_invite_users = can_invite_users self.can_pin_messages = can_pin_messages self._id_attrs = ( self.can_send_messages, self.can_send_media_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, ) python-telegram-bot-13.11/telegram/choseninlineresult.py000066400000000000000000000073501417656324400235300ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0902,R0913 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional from telegram import Location, TelegramObject, 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 ``from`` is a reserved word, use ``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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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. inline_message_id (:obj:`str`): Optional. Identifier of the sent inline message. query (:obj:`str`): The query that was used to obtain the result. """ __slots__ = ('location', 'result_id', 'from_user', 'inline_message_id', '_id_attrs', 'query') def __init__( self, result_id: str, from_user: User, query: str, location: Location = None, inline_message_id: str = None, **_kwargs: Any, ): # Required self.result_id = result_id self.from_user = from_user self.query = query # Optionals self.location = location self.inline_message_id = inline_message_id self._id_attrs = (self.result_id,) @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'), bot) # Optionals data['location'] = Location.de_json(data.get('location'), bot) return cls(**data) python-telegram-bot-13.11/telegram/constants.py000066400000000000000000000326171417656324400216330ustar00rootroot00000000000000# python-telegram-bot - a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # 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/]. """Constants in the Telegram network. The following constants were extracted from the `Telegram Bots FAQ `_ and `Telegram Bots API `_. Attributes: BOT_API_VERSION (:obj:`str`): `5.7`. Telegram Bot API version supported by this version of `python-telegram-bot`. Also available as ``telegram.bot_api_version``. .. versionadded:: 13.4 MAX_MESSAGE_LENGTH (:obj:`int`): 4096 MAX_CAPTION_LENGTH (:obj:`int`): 1024 SUPPORTED_WEBHOOK_PORTS (List[:obj:`int`]): [443, 80, 88, 8443] MAX_FILESIZE_DOWNLOAD (:obj:`int`): In bytes (20MB) MAX_FILESIZE_UPLOAD (:obj:`int`): In bytes (50MB) MAX_PHOTOSIZE_UPLOAD (:obj:`int`): In bytes (10MB) MAX_MESSAGES_PER_SECOND_PER_CHAT (:obj:`int`): `1`. Telegram may allow short bursts that go over this limit, but eventually you'll begin receiving 429 errors. MAX_MESSAGES_PER_SECOND (:obj:`int`): 30 MAX_MESSAGES_PER_MINUTE_PER_GROUP (:obj:`int`): 20 MAX_INLINE_QUERY_RESULTS (:obj:`int`): 50 MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH (:obj:`int`): 200 .. versionadded:: 13.2 The following constant have been found by experimentation: Attributes: MAX_MESSAGE_ENTITIES (:obj:`int`): 100 (Beyond this cap telegram will simply ignore further formatting styles) ANONYMOUS_ADMIN_ID (:obj:`int`): ``1087968824`` (User id in groups for anonymous admin) SERVICE_CHAT_ID (:obj:`int`): ``777000`` (Telegram service chat, that also acts as sender of channel posts forwarded to discussion groups) FAKE_CHANNEL_ID (:obj:`int`): ``136817688`` (User id in groups when message is sent on behalf of a channel). .. versionadded:: 13.9 The following constants are related to specific classes and are also available as attributes of those classes: :class:`telegram.Chat`: Attributes: CHAT_PRIVATE (:obj:`str`): ``'private'`` CHAT_GROUP (:obj:`str`): ``'group'`` CHAT_SUPERGROUP (:obj:`str`): ``'supergroup'`` CHAT_CHANNEL (:obj:`str`): ``'channel'`` CHAT_SENDER (:obj:`str`): ``'sender'``. Only relevant for :attr:`telegram.InlineQuery.chat_type`. .. versionadded:: 13.5 :class:`telegram.ChatAction`: Attributes: CHATACTION_FIND_LOCATION (:obj:`str`): ``'find_location'`` CHATACTION_RECORD_AUDIO (:obj:`str`): ``'record_audio'`` .. deprecated:: 13.5 Deprecated by Telegram. Use :const:`CHATACTION_RECORD_VOICE` instead. CHATACTION_RECORD_VOICE (:obj:`str`): ``'record_voice'`` .. versionadded:: 13.5 CHATACTION_RECORD_VIDEO (:obj:`str`): ``'record_video'`` CHATACTION_RECORD_VIDEO_NOTE (:obj:`str`): ``'record_video_note'`` CHATACTION_TYPING (:obj:`str`): ``'typing'`` CHATACTION_UPLOAD_AUDIO (:obj:`str`): ``'upload_audio'`` .. deprecated:: 13.5 Deprecated by Telegram. Use :const:`CHATACTION_UPLOAD_VOICE` instead. CHATACTION_UPLOAD_VOICE (:obj:`str`): ``'upload_voice'`` .. versionadded:: 13.5 CHATACTION_UPLOAD_DOCUMENT (:obj:`str`): ``'upload_document'`` CHATACTION_CHOOSE_STICKER (:obj:`str`): ``'choose_sticker'`` .. versionadded:: 13.8 CHATACTION_UPLOAD_PHOTO (:obj:`str`): ``'upload_photo'`` CHATACTION_UPLOAD_VIDEO (:obj:`str`): ``'upload_video'`` CHATACTION_UPLOAD_VIDEO_NOTE (:obj:`str`): ``'upload_video_note'`` :class:`telegram.ChatMember`: Attributes: CHATMEMBER_ADMINISTRATOR (:obj:`str`): ``'administrator'`` CHATMEMBER_CREATOR (:obj:`str`): ``'creator'`` CHATMEMBER_KICKED (:obj:`str`): ``'kicked'`` CHATMEMBER_LEFT (:obj:`str`): ``'left'`` CHATMEMBER_MEMBER (:obj:`str`): ``'member'`` CHATMEMBER_RESTRICTED (:obj:`str`): ``'restricted'`` :class:`telegram.Dice`: Attributes: DICE_DICE (:obj:`str`): ``'🎲'`` DICE_DARTS (:obj:`str`): ``'🎯'`` DICE_BASKETBALL (:obj:`str`): ``'🏀'`` DICE_FOOTBALL (:obj:`str`): ``'⚽'`` DICE_SLOT_MACHINE (:obj:`str`): ``'🎰'`` DICE_BOWLING (:obj:`str`): ``'🎳'`` .. versionadded:: 13.4 DICE_ALL_EMOJI (List[:obj:`str`]): List of all supported base emoji. .. versionchanged:: 13.4 Added :attr:`DICE_BOWLING` :class:`telegram.MessageEntity`: Attributes: MESSAGEENTITY_MENTION (:obj:`str`): ``'mention'`` MESSAGEENTITY_HASHTAG (:obj:`str`): ``'hashtag'`` MESSAGEENTITY_CASHTAG (:obj:`str`): ``'cashtag'`` MESSAGEENTITY_PHONE_NUMBER (:obj:`str`): ``'phone_number'`` MESSAGEENTITY_BOT_COMMAND (:obj:`str`): ``'bot_command'`` MESSAGEENTITY_URL (:obj:`str`): ``'url'`` MESSAGEENTITY_EMAIL (:obj:`str`): ``'email'`` MESSAGEENTITY_BOLD (:obj:`str`): ``'bold'`` MESSAGEENTITY_ITALIC (:obj:`str`): ``'italic'`` MESSAGEENTITY_CODE (:obj:`str`): ``'code'`` MESSAGEENTITY_PRE (:obj:`str`): ``'pre'`` MESSAGEENTITY_TEXT_LINK (:obj:`str`): ``'text_link'`` MESSAGEENTITY_TEXT_MENTION (:obj:`str`): ``'text_mention'`` MESSAGEENTITY_UNDERLINE (:obj:`str`): ``'underline'`` MESSAGEENTITY_STRIKETHROUGH (:obj:`str`): ``'strikethrough'`` MESSAGEENTITY_SPOILER (:obj:`str`): ``'spoiler'`` .. versionadded:: 13.10 MESSAGEENTITY_ALL_TYPES (List[:obj:`str`]): List of all the types of message entity. :class:`telegram.ParseMode`: Attributes: PARSEMODE_MARKDOWN (:obj:`str`): ``'Markdown'`` PARSEMODE_MARKDOWN_V2 (:obj:`str`): ``'MarkdownV2'`` PARSEMODE_HTML (:obj:`str`): ``'HTML'`` :class:`telegram.Poll`: Attributes: POLL_REGULAR (:obj:`str`): ``'regular'`` POLL_QUIZ (:obj:`str`): ``'quiz'`` MAX_POLL_QUESTION_LENGTH (:obj:`int`): 300 MAX_POLL_OPTION_LENGTH (:obj:`int`): 100 :class:`telegram.MaskPosition`: Attributes: STICKER_FOREHEAD (:obj:`str`): ``'forehead'`` STICKER_EYES (:obj:`str`): ``'eyes'`` STICKER_MOUTH (:obj:`str`): ``'mouth'`` STICKER_CHIN (:obj:`str`): ``'chin'`` :class:`telegram.Update`: Attributes: UPDATE_MESSAGE (:obj:`str`): ``'message'`` .. versionadded:: 13.5 UPDATE_EDITED_MESSAGE (:obj:`str`): ``'edited_message'`` .. versionadded:: 13.5 UPDATE_CHANNEL_POST (:obj:`str`): ``'channel_post'`` .. versionadded:: 13.5 UPDATE_EDITED_CHANNEL_POST (:obj:`str`): ``'edited_channel_post'`` .. versionadded:: 13.5 UPDATE_INLINE_QUERY (:obj:`str`): ``'inline_query'`` .. versionadded:: 13.5 UPDATE_CHOSEN_INLINE_RESULT (:obj:`str`): ``'chosen_inline_result'`` .. versionadded:: 13.5 UPDATE_CALLBACK_QUERY (:obj:`str`): ``'callback_query'`` .. versionadded:: 13.5 UPDATE_SHIPPING_QUERY (:obj:`str`): ``'shipping_query'`` .. versionadded:: 13.5 UPDATE_PRE_CHECKOUT_QUERY (:obj:`str`): ``'pre_checkout_query'`` .. versionadded:: 13.5 UPDATE_POLL (:obj:`str`): ``'poll'`` .. versionadded:: 13.5 UPDATE_POLL_ANSWER (:obj:`str`): ``'poll_answer'`` .. versionadded:: 13.5 UPDATE_MY_CHAT_MEMBER (:obj:`str`): ``'my_chat_member'`` .. versionadded:: 13.5 UPDATE_CHAT_MEMBER (:obj:`str`): ``'chat_member'`` .. versionadded:: 13.5 UPDATE_CHAT_JOIN_REQUEST (:obj:`str`): ``'chat_join_request'`` .. versionadded:: 13.8 UPDATE_ALL_TYPES (List[:obj:`str`]): List of all update types. .. versionadded:: 13.5 .. versionchanged:: 13.8 :class:`telegram.BotCommandScope`: Attributes: BOT_COMMAND_SCOPE_DEFAULT (:obj:`str`): ``'default'`` ..versionadded:: 13.7 BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS (:obj:`str`): ``'all_private_chats'`` ..versionadded:: 13.7 BOT_COMMAND_SCOPE_ALL_GROUP_CHATS (:obj:`str`): ``'all_group_chats'`` ..versionadded:: 13.7 BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS (:obj:`str`): ``'all_chat_administrators'`` ..versionadded:: 13.7 BOT_COMMAND_SCOPE_CHAT (:obj:`str`): ``'chat'`` ..versionadded:: 13.7 BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS (:obj:`str`): ``'chat_administrators'`` ..versionadded:: 13.7 BOT_COMMAND_SCOPE_CHAT_MEMBER (:obj:`str`): ``'chat_member'`` ..versionadded:: 13.7 """ from typing import List BOT_API_VERSION: str = '5.7' MAX_MESSAGE_LENGTH: int = 4096 MAX_CAPTION_LENGTH: int = 1024 ANONYMOUS_ADMIN_ID: int = 1087968824 SERVICE_CHAT_ID: int = 777000 FAKE_CHANNEL_ID: int = 136817688 # constants above this line are tested SUPPORTED_WEBHOOK_PORTS: List[int] = [443, 80, 88, 8443] MAX_FILESIZE_DOWNLOAD: int = int(20e6) # (20MB) MAX_FILESIZE_UPLOAD: int = int(50e6) # (50MB) MAX_PHOTOSIZE_UPLOAD: int = int(10e6) # (10MB) MAX_MESSAGES_PER_SECOND_PER_CHAT: int = 1 MAX_MESSAGES_PER_SECOND: int = 30 MAX_MESSAGES_PER_MINUTE_PER_GROUP: int = 20 MAX_MESSAGE_ENTITIES: int = 100 MAX_INLINE_QUERY_RESULTS: int = 50 MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH: int = 200 CHAT_SENDER: str = 'sender' CHAT_PRIVATE: str = 'private' CHAT_GROUP: str = 'group' CHAT_SUPERGROUP: str = 'supergroup' CHAT_CHANNEL: str = 'channel' CHATACTION_FIND_LOCATION: str = 'find_location' CHATACTION_RECORD_AUDIO: str = 'record_audio' CHATACTION_RECORD_VOICE: str = 'record_voice' CHATACTION_RECORD_VIDEO: str = 'record_video' CHATACTION_RECORD_VIDEO_NOTE: str = 'record_video_note' CHATACTION_TYPING: str = 'typing' CHATACTION_UPLOAD_AUDIO: str = 'upload_audio' CHATACTION_UPLOAD_VOICE: str = 'upload_voice' CHATACTION_UPLOAD_DOCUMENT: str = 'upload_document' CHATACTION_CHOOSE_STICKER: str = 'choose_sticker' CHATACTION_UPLOAD_PHOTO: str = 'upload_photo' CHATACTION_UPLOAD_VIDEO: str = 'upload_video' CHATACTION_UPLOAD_VIDEO_NOTE: str = 'upload_video_note' CHATMEMBER_ADMINISTRATOR: str = 'administrator' CHATMEMBER_CREATOR: str = 'creator' CHATMEMBER_KICKED: str = 'kicked' CHATMEMBER_LEFT: str = 'left' CHATMEMBER_MEMBER: str = 'member' CHATMEMBER_RESTRICTED: str = 'restricted' DICE_DICE: str = '🎲' DICE_DARTS: str = '🎯' DICE_BASKETBALL: str = '🏀' DICE_FOOTBALL: str = '⚽' DICE_SLOT_MACHINE: str = '🎰' DICE_BOWLING: str = '🎳' DICE_ALL_EMOJI: List[str] = [ DICE_DICE, DICE_DARTS, DICE_BASKETBALL, DICE_FOOTBALL, DICE_SLOT_MACHINE, DICE_BOWLING, ] MESSAGEENTITY_MENTION: str = 'mention' MESSAGEENTITY_HASHTAG: str = 'hashtag' MESSAGEENTITY_CASHTAG: str = 'cashtag' MESSAGEENTITY_PHONE_NUMBER: str = 'phone_number' MESSAGEENTITY_BOT_COMMAND: str = 'bot_command' MESSAGEENTITY_URL: str = 'url' MESSAGEENTITY_EMAIL: str = 'email' MESSAGEENTITY_BOLD: str = 'bold' MESSAGEENTITY_ITALIC: str = 'italic' MESSAGEENTITY_CODE: str = 'code' MESSAGEENTITY_PRE: str = 'pre' MESSAGEENTITY_TEXT_LINK: str = 'text_link' MESSAGEENTITY_TEXT_MENTION: str = 'text_mention' MESSAGEENTITY_UNDERLINE: str = 'underline' MESSAGEENTITY_STRIKETHROUGH: str = 'strikethrough' MESSAGEENTITY_SPOILER: str = 'spoiler' MESSAGEENTITY_ALL_TYPES: List[str] = [ MESSAGEENTITY_MENTION, MESSAGEENTITY_HASHTAG, MESSAGEENTITY_CASHTAG, MESSAGEENTITY_PHONE_NUMBER, MESSAGEENTITY_BOT_COMMAND, MESSAGEENTITY_URL, MESSAGEENTITY_EMAIL, MESSAGEENTITY_BOLD, MESSAGEENTITY_ITALIC, MESSAGEENTITY_CODE, MESSAGEENTITY_PRE, MESSAGEENTITY_TEXT_LINK, MESSAGEENTITY_TEXT_MENTION, MESSAGEENTITY_UNDERLINE, MESSAGEENTITY_STRIKETHROUGH, MESSAGEENTITY_SPOILER, ] PARSEMODE_MARKDOWN: str = 'Markdown' PARSEMODE_MARKDOWN_V2: str = 'MarkdownV2' PARSEMODE_HTML: str = 'HTML' POLL_REGULAR: str = 'regular' POLL_QUIZ: str = 'quiz' MAX_POLL_QUESTION_LENGTH: int = 300 MAX_POLL_OPTION_LENGTH: int = 100 STICKER_FOREHEAD: str = 'forehead' STICKER_EYES: str = 'eyes' STICKER_MOUTH: str = 'mouth' STICKER_CHIN: str = 'chin' UPDATE_MESSAGE = 'message' UPDATE_EDITED_MESSAGE = 'edited_message' UPDATE_CHANNEL_POST = 'channel_post' UPDATE_EDITED_CHANNEL_POST = 'edited_channel_post' UPDATE_INLINE_QUERY = 'inline_query' UPDATE_CHOSEN_INLINE_RESULT = 'chosen_inline_result' UPDATE_CALLBACK_QUERY = 'callback_query' UPDATE_SHIPPING_QUERY = 'shipping_query' UPDATE_PRE_CHECKOUT_QUERY = 'pre_checkout_query' UPDATE_POLL = 'poll' UPDATE_POLL_ANSWER = 'poll_answer' UPDATE_MY_CHAT_MEMBER = 'my_chat_member' UPDATE_CHAT_MEMBER = 'chat_member' UPDATE_CHAT_JOIN_REQUEST = 'chat_join_request' UPDATE_ALL_TYPES = [ UPDATE_MESSAGE, UPDATE_EDITED_MESSAGE, UPDATE_CHANNEL_POST, UPDATE_EDITED_CHANNEL_POST, UPDATE_INLINE_QUERY, UPDATE_CHOSEN_INLINE_RESULT, UPDATE_CALLBACK_QUERY, UPDATE_SHIPPING_QUERY, UPDATE_PRE_CHECKOUT_QUERY, UPDATE_POLL, UPDATE_POLL_ANSWER, UPDATE_MY_CHAT_MEMBER, UPDATE_CHAT_MEMBER, UPDATE_CHAT_JOIN_REQUEST, ] BOT_COMMAND_SCOPE_DEFAULT = 'default' BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS = 'all_private_chats' BOT_COMMAND_SCOPE_ALL_GROUP_CHATS = 'all_group_chats' BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS = 'all_chat_administrators' BOT_COMMAND_SCOPE_CHAT = 'chat' BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS = 'chat_administrators' BOT_COMMAND_SCOPE_CHAT_MEMBER = 'chat_member' python-telegram-bot-13.11/telegram/dice.py000066400000000000000000000076421417656324400205230ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any, List, ClassVar from telegram import TelegramObject, constants 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 "🎯", 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 "🏀", 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 "⚽", 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 "🎳", 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 "🎰", each value corresponds to a unique combination of symbols, which can be found at our `wiki `_. However, this behaviour is undocumented and might be changed by Telegram. Args: value (:obj:`int`): Value of the dice. 1-6 for dice, darts and bowling balls, 1-5 for basketball and football/soccer ball, 1-64 for slot machine. emoji (:obj:`str`): Emoji on which the dice throw animation is based. Attributes: value (:obj:`int`): Value of the dice. emoji (:obj:`str`): Emoji on which the dice throw animation is based. """ __slots__ = ('emoji', 'value', '_id_attrs') def __init__(self, value: int, emoji: str, **_kwargs: Any): self.value = value self.emoji = emoji self._id_attrs = (self.value, self.emoji) DICE: ClassVar[str] = constants.DICE_DICE # skipcq: PTC-W0052 """:const:`telegram.constants.DICE_DICE`""" DARTS: ClassVar[str] = constants.DICE_DARTS """:const:`telegram.constants.DICE_DARTS`""" BASKETBALL: ClassVar[str] = constants.DICE_BASKETBALL """:const:`telegram.constants.DICE_BASKETBALL`""" FOOTBALL: ClassVar[str] = constants.DICE_FOOTBALL """:const:`telegram.constants.DICE_FOOTBALL`""" SLOT_MACHINE: ClassVar[str] = constants.DICE_SLOT_MACHINE """:const:`telegram.constants.DICE_SLOT_MACHINE`""" BOWLING: ClassVar[str] = constants.DICE_BOWLING """ :const:`telegram.constants.DICE_BOWLING` .. versionadded:: 13.4 """ ALL_EMOJI: ClassVar[List[str]] = constants.DICE_ALL_EMOJI """:const:`telegram.constants.DICE_ALL_EMOJI`""" python-telegram-bot-13.11/telegram/error.py000066400000000000000000000102371417656324400207420ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=C0115 """This module contains an object that represents Telegram errors.""" from typing import Tuple 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. """ if in_s.startswith(lstr): res = in_s[len(lstr) :] else: res = in_s return res class TelegramError(Exception): """Base class for Telegram errors.""" # Apparently the base class Exception already has __dict__ in it, so its not included here __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 = msg def __str__(self) -> str: return '%s' % self.message def __reduce__(self) -> Tuple[type, Tuple[str]]: return self.__class__, (self.message,) class Unauthorized(TelegramError): """Raised when the bot has not enough rights to perform the requested action.""" __slots__ = () class InvalidToken(TelegramError): """Raised when the token is invalid.""" __slots__ = () def __init__(self) -> None: super().__init__('Invalid token') def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override] return self.__class__, () class NetworkError(TelegramError): """Base class for exceptions due to networking 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.""" __slots__ = () def __init__(self) -> None: super().__init__('Timed out') def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override] return self.__class__, () class ChatMigrated(TelegramError): """ Raised when the requested group chat migrated to supergroup and has a new chat id. Args: 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 = 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. Args: 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 {float(retry_after)} seconds') self.retry_after = float(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,) python-telegram-bot-13.11/telegram/ext/000077500000000000000000000000001417656324400200345ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/ext/__init__.py000066400000000000000000000070641417656324400221540ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=C0413 """Extensions over the Telegram Bot API to facilitate bot making""" from .extbot import ExtBot from .basepersistence import BasePersistence from .picklepersistence import PicklePersistence from .dictpersistence import DictPersistence from .handler import Handler from .callbackcontext import CallbackContext from .contexttypes import ContextTypes from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async # https://bugs.python.org/issue41451, fixed on 3.7+, doesn't actually remove slots # try-except is just here in case the __init__ is called twice (like in the tests) # this block is also the reason for the pylint-ignore at the top of the file try: del Dispatcher.__slots__ except AttributeError as exc: if str(exc) == '__slots__': pass else: raise exc from .jobqueue import JobQueue, Job from .updater import Updater from .callbackqueryhandler import CallbackQueryHandler from .choseninlineresulthandler import ChosenInlineResultHandler from .inlinequeryhandler import InlineQueryHandler from .filters import BaseFilter, MessageFilter, UpdateFilter, Filters from .messagehandler import MessageHandler from .commandhandler import CommandHandler, PrefixHandler from .regexhandler import RegexHandler from .stringcommandhandler import StringCommandHandler from .stringregexhandler import StringRegexHandler from .typehandler import TypeHandler from .conversationhandler import ConversationHandler from .precheckoutqueryhandler import PreCheckoutQueryHandler from .shippingqueryhandler import ShippingQueryHandler from .messagequeue import MessageQueue from .messagequeue import DelayQueue from .pollanswerhandler import PollAnswerHandler from .pollhandler import PollHandler from .chatmemberhandler import ChatMemberHandler from .chatjoinrequesthandler import ChatJoinRequestHandler from .defaults import Defaults from .callbackdatacache import CallbackDataCache, InvalidCallbackData __all__ = ( 'BaseFilter', 'BasePersistence', 'CallbackContext', 'CallbackDataCache', 'CallbackQueryHandler', 'ChatJoinRequestHandler', 'ChatMemberHandler', 'ChosenInlineResultHandler', 'CommandHandler', 'ContextTypes', 'ConversationHandler', 'Defaults', 'DelayQueue', 'DictPersistence', 'Dispatcher', 'DispatcherHandlerStop', 'ExtBot', 'Filters', 'Handler', 'InlineQueryHandler', 'InvalidCallbackData', 'Job', 'JobQueue', 'MessageFilter', 'MessageHandler', 'MessageQueue', 'PicklePersistence', 'PollAnswerHandler', 'PollHandler', 'PreCheckoutQueryHandler', 'PrefixHandler', 'RegexHandler', 'ShippingQueryHandler', 'StringCommandHandler', 'StringRegexHandler', 'TypeHandler', 'UpdateFilter', 'Updater', 'run_async', ) python-telegram-bot-13.11/telegram/ext/basepersistence.py000066400000000000000000000567101417656324400235760ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" import warnings from sys import version_info as py_ver from abc import ABC, abstractmethod from copy import copy from typing import Dict, Optional, Tuple, cast, ClassVar, Generic, DefaultDict from telegram.utils.deprecate import set_new_attribute_deprecated from telegram import Bot import telegram.ext.extbot from telegram.ext.utils.types import UD, CD, BD, ConversationDict, CDCData 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. 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:`get_user_data` * :meth:`update_user_data` * :meth:`refresh_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 ``pass`` is enough. For example, if ``store_bot_data=False``, you don't need :meth:`get_bot_data`, :meth:`update_bot_data` or :meth:`refresh_bot_data`. Warning: Persistence will try to replace :class:`telegram.Bot` instances by :attr:`REPLACED_BOT` and insert the bot set with :meth:`set_bot` upon loading of the data. This is to ensure that changes to the bot apply to the saved objects, too. If you change the bots token, this may lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see :meth:`replace_bot` and :meth:`insert_bot`. Note: :meth:`replace_bot` and :meth:`insert_bot` are used *independently* of the implementation of the :meth:`update/get_*` methods, i.e. you don't need to worry about it while implementing a custom persistence subclass. Args: store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this persistence class. Default is :obj:`True`. store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this persistence class. Default is :obj:`True` . store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this persistence class. Default is :obj:`True`. store_callback_data (:obj:`bool`, optional): Whether callback_data should be saved by this persistence class. Default is :obj:`False`. .. versionadded:: 13.6 Attributes: store_user_data (:obj:`bool`): Optional, Whether user_data should be saved by this persistence class. store_chat_data (:obj:`bool`): Optional. Whether chat_data should be saved by this persistence class. store_bot_data (:obj:`bool`): Optional. Whether bot_data should be saved by this persistence class. store_callback_data (:obj:`bool`): Optional. Whether callback_data should be saved by this persistence class. .. versionadded:: 13.6 """ # Apparently Py 3.7 and below have '__dict__' in ABC if py_ver < (3, 7): __slots__ = ( 'store_user_data', 'store_chat_data', 'store_bot_data', 'store_callback_data', 'bot', ) else: __slots__ = ( 'store_user_data', # type: ignore[assignment] 'store_chat_data', 'store_bot_data', 'store_callback_data', 'bot', '__dict__', ) def __new__( cls, *args: object, **kwargs: object # pylint: disable=W0613 ) -> 'BasePersistence': """This overrides the get_* and update_* methods to use insert/replace_bot. That has the side effect that we always pass deepcopied data to those methods, so in Pickle/DictPersistence we don't have to worry about copying the data again. Note: This doesn't hold for second tuple-entry of callback_data. That's a Dict[str, str], so no bots to replace anyway. """ instance = super().__new__(cls) get_user_data = instance.get_user_data get_chat_data = instance.get_chat_data get_bot_data = instance.get_bot_data get_callback_data = instance.get_callback_data update_user_data = instance.update_user_data update_chat_data = instance.update_chat_data update_bot_data = instance.update_bot_data update_callback_data = instance.update_callback_data def get_user_data_insert_bot() -> DefaultDict[int, UD]: return instance.insert_bot(get_user_data()) def get_chat_data_insert_bot() -> DefaultDict[int, CD]: return instance.insert_bot(get_chat_data()) def get_bot_data_insert_bot() -> BD: return instance.insert_bot(get_bot_data()) def get_callback_data_insert_bot() -> Optional[CDCData]: cdc_data = get_callback_data() if cdc_data is None: return None return instance.insert_bot(cdc_data[0]), cdc_data[1] def update_user_data_replace_bot(user_id: int, data: UD) -> None: return update_user_data(user_id, instance.replace_bot(data)) def update_chat_data_replace_bot(chat_id: int, data: CD) -> None: return update_chat_data(chat_id, instance.replace_bot(data)) def update_bot_data_replace_bot(data: BD) -> None: return update_bot_data(instance.replace_bot(data)) def update_callback_data_replace_bot(data: CDCData) -> None: obj_data, queue = data return update_callback_data((instance.replace_bot(obj_data), queue)) # We want to ignore TGDeprecation warnings so we use obj.__setattr__. Adds to __dict__ object.__setattr__(instance, 'get_user_data', get_user_data_insert_bot) object.__setattr__(instance, 'get_chat_data', get_chat_data_insert_bot) object.__setattr__(instance, 'get_bot_data', get_bot_data_insert_bot) object.__setattr__(instance, 'get_callback_data', get_callback_data_insert_bot) object.__setattr__(instance, 'update_user_data', update_user_data_replace_bot) object.__setattr__(instance, 'update_chat_data', update_chat_data_replace_bot) object.__setattr__(instance, 'update_bot_data', update_bot_data_replace_bot) object.__setattr__(instance, 'update_callback_data', update_callback_data_replace_bot) return instance def __init__( self, store_user_data: bool = True, store_chat_data: bool = True, store_bot_data: bool = True, store_callback_data: bool = False, ): self.store_user_data = store_user_data self.store_chat_data = store_chat_data self.store_bot_data = store_bot_data self.store_callback_data = store_callback_data self.bot: Bot = None # type: ignore[assignment] def __setattr__(self, key: str, value: object) -> None: # Allow user defined subclasses to have custom attributes. if issubclass(self.__class__, BasePersistence) and self.__class__.__name__ not in { 'DictPersistence', 'PicklePersistence', }: object.__setattr__(self, key, value) return set_new_attribute_deprecated(self, key, value) def set_bot(self, bot: Bot) -> None: """Set the Bot to be used by this persistence instance. Args: bot (:class:`telegram.Bot`): The bot. """ if self.store_callback_data and not isinstance(bot, telegram.ext.extbot.ExtBot): raise TypeError('store_callback_data can only be used with telegram.ext.ExtBot.') self.bot = bot @classmethod def replace_bot(cls, obj: object) -> object: """ Replaces all instances of :class:`telegram.Bot` that occur within the passed object with :attr:`REPLACED_BOT`. Currently, this handles objects of type ``list``, ``tuple``, ``set``, ``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or ``__slots__`` attribute, excluding classes and objects that can't be copied with ``copy.copy``. If the parsing of an object fails, the object will be returned unchanged and the error will be logged. Args: obj (:obj:`object`): The object Returns: :obj:`obj`: Copy of the object with Bot instances replaced. """ return cls._replace_bot(obj, {}) @classmethod def _replace_bot(cls, obj: object, memo: Dict[int, object]) -> object: # pylint: disable=R0911 obj_id = id(obj) if obj_id in memo: return memo[obj_id] if isinstance(obj, Bot): memo[obj_id] = cls.REPLACED_BOT return cls.REPLACED_BOT if isinstance(obj, (list, set)): # We copy the iterable here for thread safety, i.e. make sure the object we iterate # over doesn't change its length during the iteration temp_iterable = obj.copy() new_iterable = obj.__class__(cls._replace_bot(item, memo) for item in temp_iterable) memo[obj_id] = new_iterable return new_iterable if isinstance(obj, (tuple, frozenset)): # tuples and frozensets are immutable so we don't need to worry about thread safety new_immutable = obj.__class__(cls._replace_bot(item, memo) for item in obj) memo[obj_id] = new_immutable return new_immutable if isinstance(obj, type): # classes usually do have a __dict__, but it's not writable warnings.warn( 'BasePersistence.replace_bot does not handle classes. See ' 'the docs of BasePersistence.replace_bot for more information.', RuntimeWarning, ) return obj try: new_obj = copy(obj) memo[obj_id] = new_obj except Exception: warnings.warn( 'BasePersistence.replace_bot does not handle objects that can not be copied. See ' 'the docs of BasePersistence.replace_bot for more information.', RuntimeWarning, ) memo[obj_id] = obj return obj if isinstance(obj, dict): # We handle dicts via copy(obj) so we don't have to make a # difference between dict and defaultdict new_obj = cast(dict, new_obj) # We can't iterate over obj.items() due to thread safety, i.e. the dicts length may # change during the iteration temp_dict = new_obj.copy() new_obj.clear() for k, val in temp_dict.items(): new_obj[cls._replace_bot(k, memo)] = cls._replace_bot(val, memo) memo[obj_id] = new_obj return new_obj try: if hasattr(obj, '__slots__'): for attr_name in new_obj.__slots__: setattr( new_obj, attr_name, cls._replace_bot( cls._replace_bot(getattr(new_obj, attr_name), memo), memo ), ) if '__dict__' in obj.__slots__: # In this case, we have already covered the case that obj has __dict__ # Note that obj may have a __dict__ even if it's not in __slots__! memo[obj_id] = new_obj return new_obj if hasattr(obj, '__dict__'): for attr_name, attr in new_obj.__dict__.items(): setattr(new_obj, attr_name, cls._replace_bot(attr, memo)) memo[obj_id] = new_obj return new_obj except Exception as exception: warnings.warn( f'Parsing of an object failed with the following exception: {exception}. ' f'See the docs of BasePersistence.replace_bot for more information.', RuntimeWarning, ) memo[obj_id] = obj return obj def insert_bot(self, obj: object) -> object: """ Replaces all instances of :attr:`REPLACED_BOT` that occur within the passed object with :attr:`bot`. Currently, this handles objects of type ``list``, ``tuple``, ``set``, ``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or ``__slots__`` attribute, excluding classes and objects that can't be copied with ``copy.copy``. If the parsing of an object fails, the object will be returned unchanged and the error will be logged. Args: obj (:obj:`object`): The object Returns: :obj:`obj`: Copy of the object with Bot instances inserted. """ return self._insert_bot(obj, {}) def _insert_bot(self, obj: object, memo: Dict[int, object]) -> object: # pylint: disable=R0911 obj_id = id(obj) if obj_id in memo: return memo[obj_id] if isinstance(obj, Bot): memo[obj_id] = self.bot return self.bot if isinstance(obj, str) and obj == self.REPLACED_BOT: memo[obj_id] = self.bot return self.bot if isinstance(obj, (list, set)): # We copy the iterable here for thread safety, i.e. make sure the object we iterate # over doesn't change its length during the iteration temp_iterable = obj.copy() new_iterable = obj.__class__(self._insert_bot(item, memo) for item in temp_iterable) memo[obj_id] = new_iterable return new_iterable if isinstance(obj, (tuple, frozenset)): # tuples and frozensets are immutable so we don't need to worry about thread safety new_immutable = obj.__class__(self._insert_bot(item, memo) for item in obj) memo[obj_id] = new_immutable return new_immutable if isinstance(obj, type): # classes usually do have a __dict__, but it's not writable warnings.warn( 'BasePersistence.insert_bot does not handle classes. See ' 'the docs of BasePersistence.insert_bot for more information.', RuntimeWarning, ) return obj try: new_obj = copy(obj) except Exception: warnings.warn( 'BasePersistence.insert_bot does not handle objects that can not be copied. See ' 'the docs of BasePersistence.insert_bot for more information.', RuntimeWarning, ) memo[obj_id] = obj return obj if isinstance(obj, dict): # We handle dicts via copy(obj) so we don't have to make a # difference between dict and defaultdict new_obj = cast(dict, new_obj) # We can't iterate over obj.items() due to thread safety, i.e. the dicts length may # change during the iteration temp_dict = new_obj.copy() new_obj.clear() for k, val in temp_dict.items(): new_obj[self._insert_bot(k, memo)] = self._insert_bot(val, memo) memo[obj_id] = new_obj return new_obj try: if hasattr(obj, '__slots__'): for attr_name in obj.__slots__: setattr( new_obj, attr_name, self._insert_bot( self._insert_bot(getattr(new_obj, attr_name), memo), memo ), ) if '__dict__' in obj.__slots__: # In this case, we have already covered the case that obj has __dict__ # Note that obj may have a __dict__ even if it's not in __slots__! memo[obj_id] = new_obj return new_obj if hasattr(obj, '__dict__'): for attr_name, attr in new_obj.__dict__.items(): setattr(new_obj, attr_name, self._insert_bot(attr, memo)) memo[obj_id] = new_obj return new_obj except Exception as exception: warnings.warn( f'Parsing of an object failed with the following exception: {exception}. ' f'See the docs of BasePersistence.insert_bot for more information.', RuntimeWarning, ) memo[obj_id] = obj return obj @abstractmethod def get_user_data(self) -> DefaultDict[int, UD]: """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. It should return the ``user_data`` if stored, or an empty :obj:`defaultdict(telegram.ext.utils.types.UD)` with integer keys. Returns: DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.UD`]: The restored user data. """ @abstractmethod def get_chat_data(self) -> DefaultDict[int, CD]: """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. It should return the ``chat_data`` if stored, or an empty :obj:`defaultdict(telegram.ext.utils.types.CD)` with integer keys. Returns: DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.CD`]: The restored chat data. """ @abstractmethod def get_bot_data(self) -> BD: """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. It should return the ``bot_data`` if stored, or an empty :class:`telegram.ext.utils.types.BD`. Returns: :class:`telegram.ext.utils.types.BD`: The restored bot data. """ def get_callback_data(self) -> Optional[CDCData]: """Will be called by :class:`telegram.ext.Dispatcher` upon creation with a persistence object. If callback data was stored, it should be returned. .. versionadded:: 13.6 Returns: Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or :obj:`None`, if no data was stored. """ raise NotImplementedError @abstractmethod def get_conversations(self, name: str) -> ConversationDict: """Will be called by :class:`telegram.ext.Dispatcher` 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 `name` or an empty :obj:`dict` Args: name (:obj:`str`): The handlers name. Returns: :obj:`dict`: The restored conversations for the handler. """ @abstractmethod def update_conversation( self, name: str, key: Tuple[int, ...], 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 (:obj:`tuple` | :obj:`any`): The new state for the given key. """ @abstractmethod def update_user_data(self, user_id: int, data: UD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has handled an update. Args: user_id (:obj:`int`): The user the data might have been changed for. data (:class:`telegram.ext.utils.types.UD`): The :attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``. """ @abstractmethod def update_chat_data(self, chat_id: int, data: CD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has handled an update. Args: chat_id (:obj:`int`): The chat the data might have been changed for. data (:class:`telegram.ext.utils.types.CD`): The :attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``. """ @abstractmethod def update_bot_data(self, data: BD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has handled an update. Args: data (:class:`telegram.ext.utils.types.BD`): The :attr:`telegram.ext.Dispatcher.bot_data`. """ def refresh_user_data(self, user_id: int, user_data: UD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` before passing the :attr:`user_data` to a callback. Can be used to update data stored in :attr:`user_data` from an external source. .. versionadded:: 13.6 Args: user_id (:obj:`int`): The user ID this :attr:`user_data` is associated with. user_data (:class:`telegram.ext.utils.types.UD`): The ``user_data`` of a single user. """ def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` before passing the :attr:`chat_data` to a callback. Can be used to update data stored in :attr:`chat_data` from an external source. .. versionadded:: 13.6 Args: chat_id (:obj:`int`): The chat ID this :attr:`chat_data` is associated with. chat_data (:class:`telegram.ext.utils.types.CD`): The ``chat_data`` of a single chat. """ def refresh_bot_data(self, bot_data: BD) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` before passing the :attr:`bot_data` to a callback. Can be used to update data stored in :attr:`bot_data` from an external source. .. versionadded:: 13.6 Args: bot_data (:class:`telegram.ext.utils.types.BD`): The ``bot_data``. """ def update_callback_data(self, data: CDCData) -> None: """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has handled an update. .. versionadded:: 13.6 Args: data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore :class:`telegram.ext.CallbackDataCache`. """ raise NotImplementedError def flush(self) -> None: """Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the persistence a chance to finish up saving or close a database connection gracefully. """ REPLACED_BOT: ClassVar[str] = 'bot_instance_replaced_by_ptb_persistence' """:obj:`str`: Placeholder for :class:`telegram.Bot` instances replaced in saved data.""" python-telegram-bot-13.11/telegram/ext/callbackcontext.py000066400000000000000000000342171417656324400235560ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=R0201 """This module contains the CallbackContext class.""" from queue import Queue from typing import ( TYPE_CHECKING, Dict, List, Match, NoReturn, Optional, Tuple, Union, Generic, Type, TypeVar, ) from telegram import Update, CallbackQuery from telegram.ext import ExtBot from telegram.ext.utils.types import UD, CD, BD if TYPE_CHECKING: from telegram import Bot from telegram.ext import Dispatcher, Job, JobQueue CC = TypeVar('CC', bound='CallbackContext') class CallbackContext(Generic[UD, CD, BD]): """ This is a context object passed to the callback called by :class:`telegram.ext.Handler` or by the :class:`telegram.ext.Dispatcher` in an error handler added by :attr:`telegram.ext.Dispatcher.add_error_handler` or to the callback of a :class:`telegram.ext.Job`. Note: :class:`telegram.ext.Dispatcher` 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 get passed the same `CallbackContext` object (of course with proper attributes like `.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 `CallbackContext` might change in the future, so make sure to use a fairly unique name for the attributes. Warning: Do not combine custom attributes and ``@run_async``/ :meth:`telegram.ext.Disptacher.run_async`. Due to how ``run_async`` works, 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. Args: dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this context. Attributes: matches (List[:obj:`re match object`]): Optional. If the associated update originated from a regex-supported handler or had 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 (:obj:`Exception`): Optional. The error that was raised. Only present when passed to a error handler registered with :attr:`telegram.ext.Dispatcher.add_error_handler`. async_args (List[:obj:`object`]): Optional. Positional arguments of the function that raised the error. Only present when the raising function was run asynchronously using :meth:`telegram.ext.Dispatcher.run_async`. async_kwargs (Dict[:obj:`str`, :obj:`object`]): Optional. Keyword arguments of the function that raised the error. Only present when the raising function was run asynchronously using :meth:`telegram.ext.Dispatcher.run_async`. job (:class:`telegram.ext.Job`): Optional. The job which originated this callback. Only present when passed to the callback of :class:`telegram.ext.Job`. """ __slots__ = ( '_dispatcher', '_chat_id_and_data', '_user_id_and_data', 'args', 'matches', 'error', 'job', 'async_args', 'async_kwargs', '__dict__', ) def __init__(self, dispatcher: 'Dispatcher'): """ Args: dispatcher (:class:`telegram.ext.Dispatcher`): """ if not dispatcher.use_context: raise ValueError( 'CallbackContext should not be used with a non context aware ' 'dispatcher!' ) self._dispatcher = dispatcher self._chat_id_and_data: Optional[Tuple[int, CD]] = None self._user_id_and_data: Optional[Tuple[int, UD]] = None self.args: Optional[List[str]] = None self.matches: Optional[List[Match]] = None self.error: Optional[Exception] = None self.job: Optional['Job'] = None self.async_args: Optional[Union[List, Tuple]] = None self.async_kwargs: Optional[Dict[str, object]] = None @property def dispatcher(self) -> 'Dispatcher': """:class:`telegram.ext.Dispatcher`: The dispatcher associated with this context.""" return self._dispatcher @property def bot_data(self) -> BD: """:obj:`dict`: Optional. A dict that can be used to keep any data in. For each update it will be the same ``dict``. """ return self.dispatcher.bot_data @bot_data.setter def bot_data(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to bot_data, see https://git.io/Jt6ic" ) @property def chat_data(self) -> Optional[CD]: """:obj:`dict`: Optional. A dict that can be used to keep any data in. For each update from the same chat id it will be the same ``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 page `_. """ if self._chat_id_and_data: return self._chat_id_and_data[1] return None @chat_data.setter def chat_data(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to chat_data, see https://git.io/Jt6ic" ) @property def user_data(self) -> Optional[UD]: """:obj:`dict`: Optional. A dict that can be used to keep any data in. For each update from the same user it will be the same ``dict``. """ if self._user_id_and_data: return self._user_id_and_data[1] return None @user_data.setter def user_data(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to user_data, see https://git.io/Jt6ic" ) def refresh_data(self) -> None: """If :attr:`dispatcher` 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. .. versionadded:: 13.6 """ if self.dispatcher.persistence: if self.dispatcher.persistence.store_bot_data: self.dispatcher.persistence.refresh_bot_data(self.bot_data) if self.dispatcher.persistence.store_chat_data and self._chat_id_and_data is not None: self.dispatcher.persistence.refresh_chat_data(*self._chat_id_and_data) if self.dispatcher.persistence.store_user_data and self._user_id_and_data is not None: self.dispatcher.persistence.refresh_user_data(*self._user_id_and_data) 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 :class:`KeyError` in case the callback query can not be found in the cache. Args: callback_query (:class:`telegram.CallbackQuery`): The callback query. Raises: KeyError | RuntimeError: :class:`KeyError`, if the callback query can not be found in the cache and :class:`RuntimeError`, if the bot doesn't allow for arbitrary callback data. """ if isinstance(self.bot, ExtBot): if not self.bot.arbitrary_callback_data: 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[CC], update: object, error: Exception, dispatcher: 'Dispatcher', async_args: Union[List, Tuple] = None, async_kwargs: Dict[str, object] = None, ) -> CC: """ Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the error handlers. .. seealso:: :meth:`telegram.ext.Dispatcher.add_error_handler` 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. dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this context. async_args (List[:obj:`object`]): Optional. Positional arguments of the function that raised the error. Pass only when the raising function was run asynchronously using :meth:`telegram.ext.Dispatcher.run_async`. async_kwargs (Dict[:obj:`str`, :obj:`object`]): Optional. Keyword arguments of the function that raised the error. Pass only when the raising function was run asynchronously using :meth:`telegram.ext.Dispatcher.run_async`. Returns: :class:`telegram.ext.CallbackContext` """ self = cls.from_update(update, dispatcher) self.error = error self.async_args = async_args self.async_kwargs = async_kwargs return self @classmethod def from_update(cls: Type[CC], update: object, dispatcher: 'Dispatcher') -> CC: """ Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the handlers. .. seealso:: :meth:`telegram.ext.Dispatcher.add_handler` Args: update (:obj:`object` | :class:`telegram.Update`): The update. dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this context. Returns: :class:`telegram.ext.CallbackContext` """ self = cls(dispatcher) if update is not None and isinstance(update, Update): chat = update.effective_chat user = update.effective_user if chat: self._chat_id_and_data = ( chat.id, dispatcher.chat_data[chat.id], # pylint: disable=W0212 ) if user: self._user_id_and_data = ( user.id, dispatcher.user_data[user.id], # pylint: disable=W0212 ) return self @classmethod def from_job(cls: Type[CC], job: 'Job', dispatcher: 'Dispatcher') -> CC: """ 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. dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this context. Returns: :class:`telegram.ext.CallbackContext` """ self = cls(dispatcher) 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) -> 'Bot': """:class:`telegram.Bot`: The bot associated with this context.""" return self._dispatcher.bot @property def job_queue(self) -> Optional['JobQueue']: """ :class:`telegram.ext.JobQueue`: The ``JobQueue`` used by the :class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater` associated with this context. """ return self._dispatcher.job_queue @property def update_queue(self) -> Queue: """ :class:`queue.Queue`: The ``Queue`` instance used by the :class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater` associated with this context. """ return self._dispatcher.update_queue @property def match(self) -> Optional[Match[str]]: """ `Regex match type`: The first match from :attr:`matches`. Useful if you are only filtering using a single regex filter. Returns `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-13.11/telegram/ext/callbackdatacache.py000066400000000000000000000400031417656324400237550ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 logging import time from datetime import datetime from threading import Lock from typing import Dict, Tuple, Union, Optional, MutableMapping, TYPE_CHECKING, cast from uuid import uuid4 from cachetools import LRUCache # pylint: disable=E0401 from telegram import ( InlineKeyboardMarkup, InlineKeyboardButton, TelegramError, CallbackQuery, Message, User, ) from telegram.utils.helpers import to_float_timestamp 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 tempered with or deleted from cache. .. 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: str = None) -> None: super().__init__( 'The object belonging to this callback_data was deleted or the callback_data was ' 'manipulated.' ) self.callback_data = callback_data def __reduce__(self) -> Tuple[type, Tuple[Optional[str]]]: # type: ignore[override] return self.__class__, (self.callback_data,) class _KeyboardData: __slots__ = ('keyboard_uuid', 'button_data', 'access_time') def __init__( self, keyboard_uuid: str, access_time: float = None, button_data: 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. .. versionadded:: 13.6 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 (:obj:`telegram.ext.utils.types.CDCData`, 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. maxsize (:obj:`int`): maximum size of the cache. """ __slots__ = ('bot', 'maxsize', '_keyboard_data', '_callback_queries', '__lock', 'logger') def __init__( self, bot: 'ExtBot', maxsize: int = 1024, persistent_data: CDCData = None, ): self.logger = logging.getLogger(__name__) self.bot = bot self.maxsize = maxsize self._keyboard_data: MutableMapping[str, _KeyboardData] = LRUCache(maxsize=maxsize) self._callback_queries: MutableMapping[str, str] = LRUCache(maxsize=maxsize) self.__lock = Lock() if persistent_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 persistence_data(self) -> CDCData: """:obj:`telegram.ext.utils.types.CDCData`: 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 with self.__lock: 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:`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. """ with self.__lock: return self.__process_keyboard(reply_markup) def __process_keyboard(self, reply_markup: InlineKeyboardMarkup) -> InlineKeyboardMarkup: 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 ``callback_data``. Args: callback_data (:obj:`str`): The ``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 caches 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 these method separately. * *In place*, i.e. the passed :class:`telegram.Message` will be changed! Args: message (:class:`telegram.Message`): The message. """ with self.__lock: 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:`callback_query.data` or :attr:`callback_query.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. """ with self.__lock: 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) 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 callback_query.message: self.__process_message(callback_query.message) for message in ( callback_query.message.pinned_message, callback_query.message.reply_to_message, ): if message: self.__process_message(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 :class:`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 """ with self.__lock: 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: 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. """ with self.__lock: self.__clear(self._keyboard_data, time_cutoff=time_cutoff) def clear_callback_queries(self) -> None: """Clears the stored callback query IDs.""" with self.__lock: self.__clear(self._callback_queries) def __clear(self, mapping: MutableMapping, time_cutoff: 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-13.11/telegram/ext/callbackqueryhandler.py000066400000000000000000000251321417656324400245710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 re from typing import ( TYPE_CHECKING, Callable, Dict, Match, Optional, Pattern, TypeVar, Union, cast, ) from telegram import Update from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler from .utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher RT = TypeVar('RT') class CallbackQueryHandler(Handler[Update, CCT]): """Handler class to handle Telegram callback queries. Optionally based on a regex. Read the documentation of the ``re`` module for more information. Note: * :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. * If your bot allows arbitrary objects as ``callback_data``, it may happen that the original ``callback_data`` for the incoming :class:`telegram.CallbackQuery`` can not be found. This is the case when either a malicious client tempered with the ``callback_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 ``callback_data``. .. versionadded:: 13.6 Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pattern (:obj:`str` | `Pattern` | :obj:`callable` | :obj:`type`, optional): Pattern to test :attr:`telegram.CallbackQuery.data` against. If a string or a regex pattern is passed, :meth:`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 ``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. .. versionchanged:: 13.6 Added support for arbitrary callback data. pass_groups (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groups()`` as a keyword argument called ``groups``. Default is :obj:`False` DEPRECATED: Please switch to context based callbacks. pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``. Default is :obj:`False` DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: callback (:obj:`callable`): The callback function for this handler. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pattern (`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. pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the callback function. pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = ('pattern', 'pass_groups', 'pass_groupdict') def __init__( self, callback: Callable[[Update, CCT], RT], pass_update_queue: bool = False, pass_job_queue: bool = False, pattern: Union[str, Pattern, type, Callable[[object], Optional[bool]]] = None, pass_groups: bool = False, pass_groupdict: bool = False, pass_user_data: bool = False, pass_chat_data: bool = False, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, pass_chat_data=pass_chat_data, run_async=run_async, ) if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern = pattern self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict def check_update(self, update: object) -> Optional[Union[bool, object]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ 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) match = re.match(self.pattern, callback_data) if match: return match else: return True return None def collect_optional_args( self, dispatcher: 'Dispatcher', update: Update = None, check_result: Union[bool, Match] = None, ) -> Dict[str, object]: """Pass the results of ``re.match(pattern, data).{groups(), groupdict()}`` to the callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if needed. """ optional_args = super().collect_optional_args(dispatcher, update, check_result) if self.pattern and not callable(self.pattern): check_result = cast(Match, check_result) if self.pass_groups: optional_args['groups'] = check_result.groups() if self.pass_groupdict: optional_args['groupdict'] = check_result.groupdict() return optional_args def collect_additional_context( self, context: CCT, update: Update, dispatcher: 'Dispatcher', check_result: Union[bool, Match], ) -> 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-13.11/telegram/ext/chatjoinrequesthandler.py000066400000000000000000000114221417656324400251540ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import Update from .handler import Handler from .utils.types import CCT class ChatJoinRequestHandler(Handler[Update, CCT]): """Handler class to handle Telegram updates that contain a chat join request. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. .. versionadded:: 13.8 Args: callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: callback (:obj:`callable`): The callback function for this handler. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = () def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ return isinstance(update, Update) and bool(update.chat_join_request) python-telegram-bot-13.11/telegram/ext/chatmemberhandler.py000066400000000000000000000155721417656324400240650ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 classes.""" from typing import ClassVar, TypeVar, Union, Callable from telegram import Update from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler from .utils.types import CCT RT = TypeVar('RT') class ChatMemberHandler(Handler[Update, CCT]): """Handler class to handle Telegram updates that contain a chat member update. .. versionadded:: 13.4 Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: callback (:obj:`callable`): 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. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = ('chat_member_types',) MY_CHAT_MEMBER: ClassVar[int] = -1 """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.my_chat_member`.""" CHAT_MEMBER: ClassVar[int] = 0 """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.chat_member`.""" ANY_CHAT_MEMBER: ClassVar[int] = 1 """:obj:`int`: Used as a constant to handle bot :attr:`telegram.Update.my_chat_member` and :attr:`telegram.Update.chat_member`.""" def __init__( self, callback: Callable[[Update, CCT], RT], chat_member_types: int = MY_CHAT_MEMBER, pass_update_queue: bool = False, pass_job_queue: bool = False, pass_user_data: bool = False, pass_chat_data: bool = False, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, pass_chat_data=pass_chat_data, run_async=run_async, ) self.chat_member_types = chat_member_types def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :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-13.11/telegram/ext/choseninlineresulthandler.py000066400000000000000000000157321417656324400256710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Optional, TypeVar, Union, Callable, TYPE_CHECKING, Pattern, Match, cast from telegram import Update from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler from .utils.types import CCT RT = TypeVar('RT') if TYPE_CHECKING: from telegram.ext import CallbackContext, Dispatcher class ChosenInlineResultHandler(Handler[Update, CCT]): """Handler class to handle Telegram updates that contain a chosen inline result. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. pattern (:obj:`str` | `Pattern`, optional): Regex pattern. If not :obj:`None`, ``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 (:obj:`callable`): The callback function for this handler. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. pattern (`Pattern`): Optional. Regex pattern to test :attr:`telegram.ChosenInlineResult.result_id` against. .. versionadded:: 13.6 """ __slots__ = ('pattern',) def __init__( self, callback: Callable[[Update, 'CallbackContext'], RT], pass_update_queue: bool = False, pass_job_queue: bool = False, pass_user_data: bool = False, pass_chat_data: bool = False, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, pattern: Union[str, Pattern] = None, ): super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, pass_chat_data=pass_chat_data, run_async=run_async, ) if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern = pattern def check_update(self, update: object) -> Optional[Union[bool, object]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if isinstance(update, Update) and update.chosen_inline_result: if self.pattern: match = re.match(self.pattern, update.chosen_inline_result.result_id) if match: return match else: return True return None def collect_additional_context( self, context: 'CallbackContext', update: Update, dispatcher: 'Dispatcher', check_result: Union[bool, Match], ) -> 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-13.11/telegram/ext/commandhandler.py000066400000000000000000000503251417656324400233670ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 and PrefixHandler classes.""" import re import warnings from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, TypeVar, Union from telegram import MessageEntity, Update from telegram.ext import BaseFilter, Filters from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.types import SLT from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .utils.types import CCT from .handler import Handler if TYPE_CHECKING: from telegram.ext import Dispatcher RT = TypeVar('RT') class CommandHandler(Handler[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 ``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 ``~Filters.update.edited_message`` in the filter argument. Note: * :class:`CommandHandler` does *not* handle (edited) channel posts. * :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a :obj:`dict` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same :obj:`dict`. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: command (:class:`telegram.utils.types.SLT[str]`): The command or list of commands this handler should listen for. Limitations are the same as described here https://core.telegram.org/bots#commands callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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.BaseFilter`, optional): A filter inheriting from :class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in :class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise operators (& for and, | for or, ~ for not). allow_edited (:obj:`bool`, optional): Determines whether the handler should also accept edited messages. Default is :obj:`False`. DEPRECATED: Edited is allowed by default. To change this behavior use ``~Filters.update.edited_message``. pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the arguments passed to the command as a keyword argument called ``args``. It will contain a list of strings, which is the text following the command split on single or consecutive whitespace characters. Default is :obj:`False` DEPRECATED: Please switch to context based callbacks. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Raises: ValueError: when command is too long or has illegal chars. Attributes: command (:class:`telegram.utils.types.SLT[str]`): The command or list of commands this handler should listen for. Limitations are the same as described here https://core.telegram.org/bots#commands callback (:obj:`callable`): The callback function for this handler. filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these Filters. allow_edited (:obj:`bool`): Determines whether the handler should also accept edited messages. pass_args (:obj:`bool`): Determines whether the handler should be passed ``args``. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = ('command', 'filters', 'pass_args') def __init__( self, command: SLT[str], callback: Callable[[Update, CCT], RT], filters: BaseFilter = None, allow_edited: bool = None, pass_args: bool = False, pass_update_queue: bool = False, pass_job_queue: bool = False, pass_user_data: bool = False, pass_chat_data: bool = False, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, pass_chat_data=pass_chat_data, run_async=run_async, ) if isinstance(command, str): self.command = [command.lower()] else: self.command = [x.lower() for x in command] for comm in self.command: if not re.match(r'^[\da-z_]{1,32}$', comm): raise ValueError('Command is not a valid bot command') if filters: self.filters = Filters.update.messages & filters else: self.filters = Filters.update.messages if allow_edited is not None: warnings.warn( 'allow_edited is deprecated. See https://git.io/fxJuV for more info', TelegramDeprecationWarning, stacklevel=2, ) if not allow_edited: self.filters &= ~Filters.update.edited_message self.pass_args = pass_args def check_update( self, update: object ) -> Optional[Union[bool, Tuple[List[str], Optional[Union[bool, Dict]]]]]: """Determines whether an update should be passed to this handlers :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.bot ): command = message.text[1 : message.entities[0].length] args = message.text.split()[1:] command_parts = command.split('@') command_parts.append(message.bot.username) if not ( command_parts[0].lower() in self.command and command_parts[1].lower() == message.bot.username.lower() ): return None filter_result = self.filters(update) if filter_result: return args, filter_result return False return None def collect_optional_args( self, dispatcher: 'Dispatcher', update: Update = None, check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]] = None, ) -> Dict[str, object]: """Provide text after the command to the callback the ``args`` argument as list, split on single whitespaces. """ optional_args = super().collect_optional_args(dispatcher, update) if self.pass_args and isinstance(check_result, tuple): optional_args['args'] = check_result[0] return optional_args def collect_additional_context( self, context: CCT, update: Update, dispatcher: 'Dispatcher', 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]) class PrefixHandler(CommandHandler): """Handler class to handle custom prefix commands. This is a intermediate handler between :class:`MessageHandler` and :class:`CommandHandler`. It supports configurable commands with the same options as CommandHandler. It will respond to every combination of :attr:`prefix` and :attr:`command`. It will add a ``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. 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 ``~Filters.update.edited_message``. Note: * :class:`PrefixHandler` does *not* handle (edited) channel posts. * :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a :obj:`dict` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same :obj:`dict`. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: prefix (:class:`telegram.utils.types.SLT[str]`): The prefix(es) that will precede :attr:`command`. command (:class:`telegram.utils.types.SLT[str]`): The command or list of commands this handler should listen for. callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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.BaseFilter`, optional): A filter inheriting from :class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in :class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise operators (& for and, | for or, ~ for not). pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the arguments passed to the command as a keyword argument called ``args``. It will contain a list of strings, which is the text following the command split on single or consecutive whitespace characters. Default is :obj:`False` DEPRECATED: Please switch to context based callbacks. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: callback (:obj:`callable`): The callback function for this handler. filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these Filters. pass_args (:obj:`bool`): Determines whether the handler should be passed ``args``. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ # 'prefix' is a class property, & 'command' is included in the superclass, so they're left out. __slots__ = ('_prefix', '_command', '_commands') def __init__( self, prefix: SLT[str], command: SLT[str], callback: Callable[[Update, CCT], RT], filters: BaseFilter = None, pass_args: bool = False, pass_update_queue: bool = False, pass_job_queue: bool = False, pass_user_data: bool = False, pass_chat_data: bool = False, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): self._prefix: List[str] = [] self._command: List[str] = [] self._commands: List[str] = [] super().__init__( 'nocommand', callback, filters=filters, allow_edited=None, pass_args=pass_args, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, pass_chat_data=pass_chat_data, run_async=run_async, ) self.prefix = prefix # type: ignore[assignment] self.command = command # type: ignore[assignment] self._build_commands() @property def prefix(self) -> List[str]: """ The prefixes that will precede :attr:`command`. Returns: List[:obj:`str`] """ return self._prefix @prefix.setter def prefix(self, prefix: Union[str, List[str]]) -> None: if isinstance(prefix, str): self._prefix = [prefix.lower()] else: self._prefix = prefix self._build_commands() @property # type: ignore[override] def command(self) -> List[str]: # type: ignore[override] """ The list of commands this handler should listen for. Returns: List[:obj:`str`] """ return self._command @command.setter def command(self, command: Union[str, List[str]]) -> None: if isinstance(command, str): self._command = [command.lower()] else: self._command = command self._build_commands() def _build_commands(self) -> None: self._commands = [x.lower() + y.lower() for x in self.prefix for y in self.command] def check_update( self, update: object ) -> Optional[Union[bool, Tuple[List[str], Optional[Union[bool, Dict]]]]]: """Determines whether an update should be passed to this handlers :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(update) if filter_result: return text_list[1:], filter_result return False return None python-telegram-bot-13.11/telegram/ext/contexttypes.py000066400000000000000000000136171417656324400231670ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=R0201 """This module contains the auxiliary class ContextTypes.""" from typing import Type, Generic, overload, Dict # pylint: disable=W0611 from telegram.ext.callbackcontext import CallbackContext from telegram.ext.utils.types import CCT, UD, CD, BD class ContextTypes(Generic[CCT, UD, CD, BD]): """ Convenience class to gather customizable types of the :class:`telegram.ext.CallbackContext` interface. .. 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 ``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 ``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 ``context.user_data`` of all (error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support instantiating without arguments. """ __slots__ = ('_context', '_bot_data', '_chat_data', '_user_data') # overload signatures generated with https://git.io/JtJPj @overload def __init__( self: 'ContextTypes[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]', ): ... @overload def __init__(self: 'ContextTypes[CCT, Dict, Dict, Dict]', context: Type[CCT]): ... @overload def __init__( self: 'ContextTypes[CallbackContext[UD, Dict, Dict], UD, Dict, Dict]', user_data: Type[UD] ): ... @overload def __init__( self: 'ContextTypes[CallbackContext[Dict, CD, Dict], Dict, CD, Dict]', chat_data: Type[CD] ): ... @overload def __init__( self: 'ContextTypes[CallbackContext[Dict, Dict, BD], Dict, Dict, BD]', bot_data: Type[BD] ): ... @overload def __init__( self: 'ContextTypes[CCT, UD, Dict, Dict]', context: Type[CCT], user_data: Type[UD] ): ... @overload def __init__( self: 'ContextTypes[CCT, Dict, CD, Dict]', context: Type[CCT], chat_data: Type[CD] ): ... @overload def __init__( self: 'ContextTypes[CCT, Dict, Dict, BD]', context: Type[CCT], bot_data: Type[BD] ): ... @overload def __init__( self: 'ContextTypes[CallbackContext[UD, CD, Dict], UD, CD, Dict]', user_data: Type[UD], chat_data: Type[CD], ): ... @overload def __init__( self: 'ContextTypes[CallbackContext[UD, Dict, BD], UD, Dict, BD]', user_data: Type[UD], bot_data: Type[BD], ): ... @overload def __init__( self: 'ContextTypes[CallbackContext[Dict, CD, BD], Dict, CD, BD]', chat_data: Type[CD], bot_data: Type[BD], ): ... @overload def __init__( self: 'ContextTypes[CCT, UD, CD, Dict]', context: Type[CCT], user_data: Type[UD], chat_data: Type[CD], ): ... @overload def __init__( self: 'ContextTypes[CCT, UD, Dict, BD]', context: Type[CCT], user_data: Type[UD], bot_data: Type[BD], ): ... @overload def __init__( self: 'ContextTypes[CCT, Dict, CD, BD]', context: Type[CCT], chat_data: Type[CD], bot_data: Type[BD], ): ... @overload def __init__( self: 'ContextTypes[CallbackContext[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[no-untyped-def] self, context=CallbackContext, bot_data=dict, chat_data=dict, user_data=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]: return self._context @property def bot_data(self) -> Type[BD]: return self._bot_data @property def chat_data(self) -> Type[CD]: return self._chat_data @property def user_data(self) -> Type[UD]: return self._user_data python-telegram-bot-13.11/telegram/ext/conversationhandler.py000066400000000000000000000752001417656324400244620ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=R0201 """This module contains the ConversationHandler.""" import logging import warnings import functools import datetime from threading import Lock from typing import TYPE_CHECKING, Dict, List, NoReturn, Optional, Union, Tuple, cast, ClassVar from telegram import Update from telegram.ext import ( BasePersistence, CallbackContext, CallbackQueryHandler, ChosenInlineResultHandler, DispatcherHandlerStop, Handler, InlineQueryHandler, ) from telegram.ext.utils.promise import Promise from telegram.ext.utils.types import ConversationDict from telegram.ext.utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher, Job CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]] class _ConversationTimeoutContext: # '__dict__' is not included since this a private class __slots__ = ('conversation_key', 'update', 'dispatcher', 'callback_context') def __init__( self, conversation_key: Tuple[int, ...], update: Update, dispatcher: 'Dispatcher', callback_context: Optional[CallbackContext], ): self.conversation_key = conversation_key self.update = update self.dispatcher = dispatcher self.callback_context = callback_context class ConversationHandler(Handler[Update, CCT]): """ A handler to hold a conversation with a single or multiple users through Telegram updates by managing four collections of other handlers. Note: ``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` ``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 ``per_message=True``, ``ConversationHandler`` uses ``update.callback_query.message.message_id`` when ``per_chat=True`` and ``update.callback_query.inline_message_id`` when ``per_chat=False``. For a more detailed explanation, please see our `FAQ`_. Finally, ``ConversationHandler``, does *not* handle (edited) channel posts. .. _`FAQ`: https://git.io/JtcyU The first collection, a ``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 ``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 ``@run_async`` decorated handler is not finished. The third collection, a ``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.DispatcherHandlerStop` can be used in conversations as described in the corresponding documentation. Note: In each of the described collections of handlers, a handler may in turn be a :class:`ConversationHandler`. In that case, the nested :class:`ConversationHandler` should have the attribute :attr:`map_to_parent` which allows to return to the parent conversation at specified states within the nested 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 this has ended or even map a state to :attr:`END` to end the *parent* conversation from within the nested one. For an example on nested :class:`ConversationHandler` s, see our `examples`_. .. _`examples`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples Args: entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can trigger the start of the conversation. The first handler which :attr:`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.Handler`]]): A :obj:`dict` that defines the different states of conversation a user can be in and one or more associated ``Handler`` objects that should be used in that state. The first handler which :attr:`check_update` method returns :obj:`True` will be used. fallbacks (List[:class:`telegram.ext.Handler`]): 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 :attr:`check_update`. The first handler which :attr:`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. per_chat (:obj:`bool`, optional): If the conversationkey should contain the Chat's ID. Default is :obj:`True`. per_user (:obj:`bool`, optional): If the conversationkey should contain the User's ID. Default is :obj:`True`. per_message (:obj:`bool`, optional): If the conversationkey 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 ``context`` will be handled by ALL the handler's who's :attr:`check_update` method returns :obj:`True` that are in the state :attr:`ConversationHandler.TIMEOUT`. Note: 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. name (:obj:`str`, optional): The name for this conversationhandler. Required for persistence. persistent (:obj:`bool`, optional): If the conversations dict for this handler should be saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater` map_to_parent (Dict[:obj:`object`, :obj:`object`], optional): A :obj:`dict` that can be used to instruct a nested conversationhandler to transition into a mapped state on its parent conversationhandler in place of a specified nested state. run_async (:obj:`bool`, optional): Pass :obj:`True` to *override* the :attr:`Handler.run_async` setting of all handlers (in :attr:`entry_points`, :attr:`states` and :attr:`fallbacks`). Note: If set to :obj:`True`, you should not pass a handler instance, that needs to be run synchronously in another context. .. versionadded:: 13.2 Raises: ValueError Attributes: persistent (:obj:`bool`): Optional. If the conversations dict for this handler should be saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater` run_async (:obj:`bool`): If :obj:`True`, will override the :attr:`Handler.run_async` setting of all internal handlers on initialization. .. versionadded:: 13.2 """ __slots__ = ( '_entry_points', '_states', '_fallbacks', '_allow_reentry', '_per_user', '_per_chat', '_per_message', '_conversation_timeout', '_name', 'persistent', '_persistence', '_map_to_parent', 'timeout_jobs', '_timeout_jobs_lock', '_conversations', '_conversations_lock', 'logger', ) END: ClassVar[int] = -1 """:obj:`int`: Used as a constant to return when a conversation is ended.""" TIMEOUT: ClassVar[int] = -2 """:obj:`int`: Used as a constant to handle state when a conversation is timed out.""" WAITING: ClassVar[int] = -3 """:obj:`int`: Used as a constant to handle state when a conversation is still waiting on the previous ``@run_sync`` decorated running handler to finish.""" # pylint: disable=W0231 def __init__( self, entry_points: List[Handler[Update, CCT]], states: Dict[object, List[Handler[Update, CCT]]], fallbacks: List[Handler[Update, CCT]], allow_reentry: bool = False, per_chat: bool = True, per_user: bool = True, per_message: bool = False, conversation_timeout: Union[float, datetime.timedelta] = None, name: str = None, persistent: bool = False, map_to_parent: Dict[object, object] = None, run_async: bool = False, ): self.run_async = run_async self._entry_points = entry_points self._states = states self._fallbacks = fallbacks self._allow_reentry = allow_reentry self._per_user = per_user self._per_chat = per_chat self._per_message = per_message self._conversation_timeout = conversation_timeout self._name = name if persistent and not self.name: raise ValueError("Conversations can't be persistent when handler is unnamed.") self.persistent: bool = persistent self._persistence: Optional[BasePersistence] = None """:obj:`telegram.ext.BasePersistence`: The persistence used to store conversations. Set by dispatcher""" self._map_to_parent = map_to_parent self.timeout_jobs: Dict[Tuple[int, ...], 'Job'] = {} self._timeout_jobs_lock = Lock() self._conversations: ConversationDict = {} self._conversations_lock = Lock() self.logger = logging.getLogger(__name__) 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: warnings.warn( "If 'per_message=True' is used, 'per_chat=True' should also be used, " "since message IDs are not globally unique." ) all_handlers: List[Handler] = [] all_handlers.extend(entry_points) all_handlers.extend(fallbacks) for state_handlers in states.values(): all_handlers.extend(state_handlers) if self.per_message: for handler in all_handlers: if not isinstance(handler, CallbackQueryHandler): warnings.warn( "If 'per_message=True', all entry points and state handlers" " must be 'CallbackQueryHandler', since no other handlers " "have a message context." ) break else: for handler in all_handlers: if isinstance(handler, CallbackQueryHandler): warnings.warn( "If 'per_message=False', 'CallbackQueryHandler' will not be " "tracked for every message." ) break if self.per_chat: for handler in all_handlers: if isinstance(handler, (InlineQueryHandler, ChosenInlineResultHandler)): warnings.warn( "If 'per_chat=True', 'InlineQueryHandler' can not be used, " "since inline queries have no chat context." ) break if self.conversation_timeout: for handler in all_handlers: if isinstance(handler, self.__class__): warnings.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." ) break if self.run_async: for handler in all_handlers: handler.run_async = True @property def entry_points(self) -> List[Handler]: """List[:class:`telegram.ext.Handler`]: A list of ``Handler`` objects that can trigger the start of the conversation. """ return self._entry_points @entry_points.setter def entry_points(self, value: object) -> NoReturn: raise ValueError('You can not assign a new value to entry_points after initialization.') @property def states(self) -> Dict[object, List[Handler]]: """Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]: A :obj:`dict` that defines the different states of conversation a user can be in and one or more associated ``Handler`` objects that should be used in that state. """ return self._states @states.setter def states(self, value: object) -> NoReturn: raise ValueError('You can not assign a new value to states after initialization.') @property def fallbacks(self) -> List[Handler]: """List[:class:`telegram.ext.Handler`]: 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 :attr:`check_update`. """ return self._fallbacks @fallbacks.setter def fallbacks(self, value: object) -> NoReturn: raise ValueError('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 ValueError('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 ValueError('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 ValueError('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 ValueError('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 ValueError( '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 ValueError('You can not assign a new value to name 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 ValueError('You can not assign a new value to map_to_parent after initialization.') @property def persistence(self) -> Optional[BasePersistence]: """The persistence class as provided by the :class:`Dispatcher`.""" return self._persistence @persistence.setter def persistence(self, persistence: BasePersistence) -> None: self._persistence = persistence # Set persistence for nested conversations for handlers in self.states.values(): for handler in handlers: if isinstance(handler, ConversationHandler): handler.persistence = self.persistence @property def conversations(self) -> ConversationDict: # skipcq: PY-D0003 return self._conversations @conversations.setter def conversations(self, value: ConversationDict) -> None: self._conversations = value # Set conversations for nested conversations for handlers in self.states.values(): for handler in handlers: if isinstance(handler, ConversationHandler) and self.persistence and handler.name: handler.conversations = self.persistence.get_conversations(handler.name) def _get_key(self, update: Update) -> Tuple[int, ...]: chat = update.effective_chat user = update.effective_user key = [] if self.per_chat: key.append(chat.id) # type: ignore[union-attr] if self.per_user and user is not None: key.append(user.id) if self.per_message: key.append( update.callback_query.inline_message_id # type: ignore[union-attr] or update.callback_query.message.message_id # type: ignore[union-attr] ) return tuple(key) def _resolve_promise(self, state: Tuple) -> object: old_state, new_state = state try: res = new_state.result(0) res = res if res is not None else old_state except Exception as exc: self.logger.exception("Promise function raised exception") self.logger.exception("%s", exc) res = old_state finally: if res is None and old_state is None: res = self.END return res def _schedule_job( self, new_state: object, dispatcher: 'Dispatcher', update: Update, context: Optional[CallbackContext], conversation_key: Tuple[int, ...], ) -> None: if new_state != self.END: try: # both job_queue & conversation_timeout are checked before calling _schedule_job j_queue = dispatcher.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] context=_ConversationTimeoutContext( conversation_key, update, dispatcher, context ), ) except Exception as exc: self.logger.exception( "Failed to schedule timeout job due to the following exception:" ) self.logger.exception("%s", exc) def check_update(self, update: object) -> CheckUpdateType: # pylint: disable=R0911 """ Determines whether an update should be handled by this conversationhandler, 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_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) with self._conversations_lock: state = self.conversations.get(key) # Resolve promises if isinstance(state, tuple) and len(state) == 2 and isinstance(state[1], Promise): self.logger.debug('waiting for promise...') # check if promise is finished or not if state[1].done.wait(0): res = self._resolve_promise(state) self._update_state(res, key) with self._conversations_lock: state = self.conversations.get(key) # if not then handle WAITING state instead else: hdlrs = self.states.get(self.WAITING, []) for hdlr in hdlrs: check = hdlr.check_update(update) if check is not None and check is not False: return key, hdlr, check return None self.logger.debug('selecting conversation %s with state %s', str(key), str(state)) handler = 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 not handler: handlers = self.states.get(state) for candidate in handlers or []: 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 key, handler, check # type: ignore[return-value] def handle_update( # type: ignore[override] self, update: Update, dispatcher: 'Dispatcher', check_result: CheckUpdateType, context: CallbackContext = None, ) -> Optional[object]: """Send the update to the callback for the current state and Handler Args: check_result: The result from check_update. For this handler it's a tuple of key, handler, and the handler's check result. update (:class:`telegram.Update`): Incoming telegram update. dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update. context (:class:`telegram.ext.CallbackContext`, optional): The context as provided by the dispatcher. """ update = cast(Update, update) # for mypy conversation_key, handler, check_result = check_result # type: ignore[assignment,misc] raise_dp_handler_stop = False 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() try: new_state = handler.handle_update(update, dispatcher, check_result, context) except DispatcherHandlerStop as exception: new_state = exception.state raise_dp_handler_stop = True with self._timeout_jobs_lock: if self.conversation_timeout: if dispatcher.job_queue is not None: # Add the new timeout job if isinstance(new_state, Promise): new_state.add_done_callback( functools.partial( self._schedule_job, dispatcher=dispatcher, update=update, context=context, conversation_key=conversation_key, ) ) elif new_state != self.END: self._schedule_job( new_state, dispatcher, update, context, conversation_key ) else: self.logger.warning( "Ignoring `conversation_timeout` because the Dispatcher has no JobQueue." ) if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent: self._update_state(self.END, conversation_key) if raise_dp_handler_stop: raise DispatcherHandlerStop(self.map_to_parent.get(new_state)) return self.map_to_parent.get(new_state) self._update_state(new_state, conversation_key) 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 DispatcherHandlerStop() return None def _update_state(self, new_state: object, key: Tuple[int, ...]) -> None: if new_state == self.END: with self._conversations_lock: if key in self.conversations: # If there is no key in conversations, nothing is done. del self.conversations[key] if self.persistent and self.persistence and self.name: self.persistence.update_conversation(self.name, key, None) elif isinstance(new_state, Promise): with self._conversations_lock: self.conversations[key] = (self.conversations.get(key), new_state) if self.persistent and self.persistence and self.name: self.persistence.update_conversation( self.name, key, (self.conversations.get(key), new_state) ) elif new_state is not None: if new_state not in self.states: warnings.warn( f"Handler returned state {new_state} which is unknown to the " f"ConversationHandler{' ' + self.name if self.name is not None else ''}." ) with self._conversations_lock: self.conversations[key] = new_state if self.persistent and self.persistence and self.name: self.persistence.update_conversation(self.name, key, new_state) def _trigger_timeout(self, context: CallbackContext, job: 'Job' = None) -> None: self.logger.debug('conversation timeout was triggered!') # Backward compatibility with bots that do not use CallbackContext if isinstance(context, CallbackContext): job = context.job ctxt = cast(_ConversationTimeoutContext, job.context) # type: ignore[union-attr] else: ctxt = cast(_ConversationTimeoutContext, job.context) callback_context = ctxt.callback_context with self._timeout_jobs_lock: found_job = self.timeout_jobs[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] 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: handler.handle_update(ctxt.update, ctxt.dispatcher, check, callback_context) except DispatcherHandlerStop: self.logger.warning( 'DispatcherHandlerStop in TIMEOUT state of ' 'ConversationHandler has no effect. Ignoring.' ) self._update_state(self.END, ctxt.conversation_key) python-telegram-bot-13.11/telegram/ext/defaults.py000066400000000000000000000242221417656324400222170ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=R0201 """This module contains the class Defaults, which allows to pass default values to Updater.""" from typing import NoReturn, Optional, Dict, Any import pytz from telegram.utils.deprecate import set_new_attribute_deprecated from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput class Defaults: """Convenience Class to gather all parameters with a (user defined) default value Parameters: parse_mode (: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. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). Note: Will *not* be used for :meth:`telegram.Bot.get_updates`! quote (:obj:`bool`, optional): If set to :obj:`True`, the reply is sent as an actual reply to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. tzinfo (:obj:`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 ``tzinfo``. Must be a timezone provided by the ``pytz`` module. Defaults to UTC. run_async (:obj:`bool`, optional): Default setting for the ``run_async`` parameter of handlers and error handlers registered through :meth:`Dispatcher.add_handler` and :meth:`Dispatcher.add_error_handler`. Defaults to :obj:`False`. """ __slots__ = ( '_timeout', '_tzinfo', '_disable_web_page_preview', '_run_async', '_quote', '_disable_notification', '_allow_sending_without_reply', '_parse_mode', '_api_defaults', '__dict__', ) def __init__( self, parse_mode: str = None, disable_notification: bool = None, disable_web_page_preview: bool = None, # Timeout needs special treatment, since the bot methods have two different # default values for timeout (None and 20s) timeout: ODVInput[float] = DEFAULT_NONE, quote: bool = None, tzinfo: pytz.BaseTzInfo = pytz.utc, run_async: bool = False, allow_sending_without_reply: bool = None, ): self._parse_mode = parse_mode self._disable_notification = disable_notification self._disable_web_page_preview = disable_web_page_preview self._allow_sending_without_reply = allow_sending_without_reply self._timeout = timeout self._quote = quote self._tzinfo = tzinfo self._run_async = run_async # Gather all defaults that actually have a default value self._api_defaults = {} for kwarg in ( 'parse_mode', 'explanation_parse_mode', 'disable_notification', 'disable_web_page_preview', 'allow_sending_without_reply', ): value = getattr(self, kwarg) if value not in [None, DEFAULT_NONE]: self._api_defaults[kwarg] = value # Special casing, as None is a valid default value if self._timeout != DEFAULT_NONE: self._api_defaults['timeout'] = self._timeout def __setattr__(self, key: str, value: object) -> None: set_new_attribute_deprecated(self, key, value) @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 defaults after because it would " "not have any effect." ) @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 defaults after because it would " "not have any effect." ) @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 defaults after because it would " "not have any effect." ) @property def disable_web_page_preview(self) -> Optional[bool]: """:obj:`bool`: Optional. Disables link previews for links in this message. """ return self._disable_web_page_preview @disable_web_page_preview.setter def disable_web_page_preview(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to defaults after because it would " "not have any effect." ) @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 defaults after because it would " "not have any effect." ) @property def timeout(self) -> ODVInput[float]: """:obj:`int` | :obj:`float`: Optional. If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). """ return self._timeout @timeout.setter def timeout(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to defaults after because it would " "not have any effect." ) @property def quote(self) -> Optional[bool]: """:obj:`bool`: Optional. If set to :obj:`True`, the reply is sent as an actual reply to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. """ return self._quote @quote.setter def quote(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to defaults after because it would " "not have any effect." ) @property def tzinfo(self) -> pytz.BaseTzInfo: """: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 defaults after because it would " "not have any effect." ) @property def run_async(self) -> bool: """:obj:`bool`: Optional. Default setting for the ``run_async`` parameter of handlers and error handlers registered through :meth:`Dispatcher.add_handler` and :meth:`Dispatcher.add_error_handler`. """ return self._run_async @run_async.setter def run_async(self, value: object) -> NoReturn: raise AttributeError( "You can not assign a new value to defaults after because it would " "not have any effect." ) def __hash__(self) -> int: return hash( ( self._parse_mode, self._disable_notification, self._disable_web_page_preview, self._allow_sending_without_reply, self._timeout, self._quote, self._tzinfo, self._run_async, ) ) def __eq__(self, other: object) -> bool: if isinstance(other, Defaults): return all(getattr(self, attr) == getattr(other, attr) for attr in self.__slots__) return False def __ne__(self, other: object) -> bool: return not self == other python-telegram-bot-13.11/telegram/ext/dictpersistence.py000066400000000000000000000371521417656324400236060ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" from typing import DefaultDict, Dict, Optional, Tuple, cast from collections import defaultdict from telegram.utils.helpers import ( decode_conversations_from_json, decode_user_chat_data_from_json, encode_conversations_to_json, ) from telegram.ext import BasePersistence from telegram.ext.utils.types import ConversationDict, CDCData try: import ujson as json except ImportError: import json # type: ignore[no-redef] class DictPersistence(BasePersistence): """Using Python's :obj:`dict` and ``json`` for making your bot persistent. Note: This class does *not* implement a :meth:`flush` method, meaning that data managed by ``DictPersistence`` is in-memory only and will be lost when the bot shuts down. This is, because ``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. Warning: :class:`DictPersistence` will try to replace :class:`telegram.Bot` instances by :attr:`REPLACED_BOT` and insert the bot set with :meth:`telegram.ext.BasePersistence.set_bot` upon loading of the data. This is to ensure that changes to the bot apply to the saved objects, too. If you change the bots token, this may lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see :meth:`telegram.ext.BasePersistence.replace_bot` and :meth:`telegram.ext.BasePersistence.insert_bot`. Args: store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this persistence class. Default is :obj:`True`. store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this persistence class. Default is :obj:`True`. store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this persistence class. Default is :obj:`True`. store_callback_data (:obj:`bool`, optional): Whether callback_data should be saved by this persistence class. Default is :obj:`False`. .. versionadded:: 13.6 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 ``""``. 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 conversations_json (:obj:`str`, optional): JSON string that will be used to reconstruct conversation on creating this persistence. Default is ``""``. Attributes: store_user_data (:obj:`bool`): Whether user_data should be saved by this persistence class. store_chat_data (:obj:`bool`): Whether chat_data should be saved by this persistence class. store_bot_data (:obj:`bool`): Whether bot_data should be saved by this persistence class. store_callback_data (:obj:`bool`): Whether callback_data be saved by this persistence class. .. versionadded:: 13.6 """ __slots__ = ( '_user_data', '_chat_data', '_bot_data', '_callback_data', '_conversations', '_user_data_json', '_chat_data_json', '_bot_data_json', '_callback_data_json', '_conversations_json', ) def __init__( self, store_user_data: bool = True, store_chat_data: bool = True, store_bot_data: bool = True, user_data_json: str = '', chat_data_json: str = '', bot_data_json: str = '', conversations_json: str = '', store_callback_data: bool = False, callback_data_json: str = '', ): super().__init__( store_user_data=store_user_data, store_chat_data=store_chat_data, store_bot_data=store_bot_data, store_callback_data=store_callback_data, ) self._user_data = None self._chat_data = None self._bot_data = None self._callback_data = None self._conversations = None self._user_data_json = None self._chat_data_json = None self._bot_data_json = None self._callback_data_json = None self._conversations_json = None if user_data_json: try: self._user_data = 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 = 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 = 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[DefaultDict[int, Dict]]: """: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[DefaultDict[int, Dict]]: """: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]: """: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]: """:class:`telegram.ext.utils.types.CDCData`: The meta data on the stored callback data. .. versionadded:: 13.6 """ return self._callback_data @property def callback_data_json(self) -> str: """:obj:`str`: The meta data 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 return encode_conversations_to_json(self.conversations) # type: ignore[arg-type] def get_user_data(self) -> DefaultDict[int, Dict[object, object]]: """Returns the user_data created from the ``user_data_json`` or an empty :obj:`defaultdict`. Returns: :obj:`defaultdict`: The restored user data. """ if self.user_data is None: self._user_data = defaultdict(dict) return self.user_data # type: ignore[return-value] def get_chat_data(self) -> DefaultDict[int, Dict[object, object]]: """Returns the chat_data created from the ``chat_data_json`` or an empty :obj:`defaultdict`. Returns: :obj:`defaultdict`: The restored chat data. """ if self.chat_data is None: self._chat_data = defaultdict(dict) return self.chat_data # type: ignore[return-value] 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 self.bot_data # type: ignore[return-value] def get_callback_data(self) -> Optional[CDCData]: """Returns the callback_data created from the ``callback_data_json`` or :obj:`None`. .. versionadded:: 13.6 Returns: Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or :obj:`None`, if no data was stored. """ if self.callback_data is None: self._callback_data = None return None return self.callback_data[0], self.callback_data[1].copy() 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] def update_conversation( self, name: str, key: Tuple[int, ...], 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` | :obj:`any`): 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 def update_user_data(self, user_id: int, data: Dict) -> 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.Dispatcher.user_data` ``[user_id]``. """ if self._user_data is None: self._user_data = defaultdict(dict) if self._user_data.get(user_id) == data: return self._user_data[user_id] = data self._user_data_json = None def update_chat_data(self, chat_id: int, data: Dict) -> 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.Dispatcher.chat_data` ``[chat_id]``. """ if self._chat_data is None: self._chat_data = defaultdict(dict) if self._chat_data.get(chat_id) == data: return self._chat_data[chat_id] = data self._chat_data_json = None def update_bot_data(self, data: Dict) -> None: """Will update the bot_data (if changed). Args: data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.bot_data`. """ if self._bot_data == data: return self._bot_data = data self._bot_data_json = None def update_callback_data(self, data: CDCData) -> None: """Will update the callback_data (if changed). .. versionadded:: 13.6 Args: data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore :class:`telegram.ext.CallbackDataCache`. """ if self._callback_data == data: return self._callback_data = (data[0], data[1].copy()) self._callback_data_json = None def refresh_user_data(self, user_id: int, user_data: Dict) -> None: """Does nothing. .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data` """ def refresh_chat_data(self, chat_id: int, chat_data: Dict) -> None: """Does nothing. .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data` """ def refresh_bot_data(self, bot_data: Dict) -> None: """Does nothing. .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data` """ python-telegram-bot-13.11/telegram/ext/dispatcher.py000066400000000000000000001022231417656324400225340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Dispatcher class.""" import logging import warnings import weakref from collections import defaultdict from functools import wraps from queue import Empty, Queue from threading import BoundedSemaphore, Event, Lock, Thread, current_thread from time import sleep from typing import ( TYPE_CHECKING, Callable, DefaultDict, Dict, List, Optional, Set, Union, Generic, TypeVar, overload, cast, ) from uuid import uuid4 from telegram import TelegramError, Update from telegram.ext import BasePersistence, ContextTypes from telegram.ext.callbackcontext import CallbackContext from telegram.ext.handler import Handler import telegram.ext.extbot from telegram.ext.callbackdatacache import CallbackDataCache from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated from telegram.ext.utils.promise import Promise from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from telegram.ext.utils.types import CCT, UD, CD, BD if TYPE_CHECKING: from telegram import Bot from telegram.ext import JobQueue DEFAULT_GROUP: int = 0 UT = TypeVar('UT') def run_async( func: Callable[[Update, CallbackContext], object] ) -> Callable[[Update, CallbackContext], object]: """ Function decorator that will run the function in a new thread. Will run :attr:`telegram.ext.Dispatcher.run_async`. Using this decorator is only possible when only a single Dispatcher exist in the system. Note: DEPRECATED. Use :attr:`telegram.ext.Dispatcher.run_async` directly instead or the :attr:`Handler.run_async` parameter. Warning: If you're using ``@run_async`` you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. """ @wraps(func) def async_func(*args: object, **kwargs: object) -> object: warnings.warn( 'The @run_async decorator is deprecated. Use the `run_async` parameter of ' 'your Handler or `Dispatcher.run_async` instead.', TelegramDeprecationWarning, stacklevel=2, ) return Dispatcher.get_instance()._run_async( # pylint: disable=W0212 func, *args, update=None, error_handling=False, **kwargs ) return async_func class DispatcherHandlerStop(Exception): """ Raise this in handler to prevent execution of any other handler (even in different group). In order to use this exception in a :class:`telegram.ext.ConversationHandler`, pass the optional ``state`` parameter instead of returning the next state: .. code-block:: python def callback(update, context): ... raise DispatcherHandlerStop(next_state) Attributes: state (:obj:`object`): Optional. The next state of the conversation. Args: state (:obj:`object`, optional): The next state of the conversation. """ __slots__ = ('state',) def __init__(self, state: object = None) -> None: super().__init__() self.state = state class Dispatcher(Generic[CCT, UD, CD, BD]): """This class dispatches all kinds of updates to its registered handlers. Args: bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers. update_queue (:obj:`Queue`): The synchronized queue that will contain the updates. job_queue (:class:`telegram.ext.JobQueue`, optional): The :class:`telegram.ext.JobQueue` instance to pass onto handler callbacks. workers (:obj:`int`, optional): Number of maximum concurrent worker threads for the ``@run_async`` decorator and :meth:`run_async`. Defaults to 4. persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to store data that should be persistent over restarts. use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`. **New users**: set this to :obj:`True`. 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 Attributes: bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers. update_queue (:obj:`Queue`): The synchronized queue that will contain the updates. job_queue (:class:`telegram.ext.JobQueue`): Optional. The :class:`telegram.ext.JobQueue` instance to pass onto handler callbacks. workers (:obj:`int`, optional): Number of maximum concurrent worker threads for the ``@run_async`` decorator and :meth:`run_async`. user_data (:obj:`defaultdict`): A dictionary handlers can use to store data for the user. chat_data (:obj:`defaultdict`): A dictionary handlers can use to store data for the chat. bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot. persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to store data that should be persistent over restarts. context_types (:class:`telegram.ext.ContextTypes`): Container for the types used in the ``context`` interface. .. versionadded:: 13.6 """ # Allowing '__weakref__' creation here since we need it for the singleton __slots__ = ( 'workers', 'persistence', 'use_context', 'update_queue', 'job_queue', 'user_data', 'chat_data', 'bot_data', '_update_persistence_lock', 'handlers', 'groups', 'error_handlers', 'running', '__stop_event', '__exception_event', '__async_queue', '__async_threads', 'bot', '__dict__', '__weakref__', 'context_types', ) __singleton_lock = Lock() __singleton_semaphore = BoundedSemaphore() __singleton = None logger = logging.getLogger(__name__) @overload def __init__( self: 'Dispatcher[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]', bot: 'Bot', update_queue: Queue, workers: int = 4, exception_event: Event = None, job_queue: 'JobQueue' = None, persistence: BasePersistence = None, use_context: bool = True, ): ... @overload def __init__( self: 'Dispatcher[CCT, UD, CD, BD]', bot: 'Bot', update_queue: Queue, workers: int = 4, exception_event: Event = None, job_queue: 'JobQueue' = None, persistence: BasePersistence = None, use_context: bool = True, context_types: ContextTypes[CCT, UD, CD, BD] = None, ): ... def __init__( self, bot: 'Bot', update_queue: Queue, workers: int = 4, exception_event: Event = None, job_queue: 'JobQueue' = None, persistence: BasePersistence = None, use_context: bool = True, context_types: ContextTypes[CCT, UD, CD, BD] = None, ): self.bot = bot self.update_queue = update_queue self.job_queue = job_queue self.workers = workers self.use_context = use_context self.context_types = cast(ContextTypes[CCT, UD, CD, BD], context_types or ContextTypes()) if not use_context: warnings.warn( 'Old Handler API is deprecated - see https://git.io/fxJuV for details', TelegramDeprecationWarning, stacklevel=3, ) if self.workers < 1: warnings.warn( 'Asynchronous callbacks can not be processed without at least one worker thread.' ) self.user_data: DefaultDict[int, UD] = defaultdict(self.context_types.user_data) self.chat_data: DefaultDict[int, CD] = defaultdict(self.context_types.chat_data) self.bot_data = self.context_types.bot_data() self.persistence: Optional[BasePersistence] = None self._update_persistence_lock = Lock() if persistence: if not isinstance(persistence, BasePersistence): raise TypeError("persistence must be based on telegram.ext.BasePersistence") self.persistence = persistence self.persistence.set_bot(self.bot) if self.persistence.store_user_data: self.user_data = self.persistence.get_user_data() if not isinstance(self.user_data, defaultdict): raise ValueError("user_data must be of type defaultdict") if self.persistence.store_chat_data: self.chat_data = self.persistence.get_chat_data() if not isinstance(self.chat_data, defaultdict): raise ValueError("chat_data must be of type defaultdict") if self.persistence.store_bot_data: self.bot_data = 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__}" ) if self.persistence.store_callback_data: self.bot = cast(telegram.ext.extbot.ExtBot, self.bot) persistent_data = self.persistence.get_callback_data() if persistent_data is not None: if not isinstance(persistent_data, tuple) and len(persistent_data) != 2: raise ValueError('callback_data must be a 2-tuple') self.bot.callback_data_cache = CallbackDataCache( self.bot, self.bot.callback_data_cache.maxsize, persistent_data=persistent_data, ) else: self.persistence = None self.handlers: Dict[int, List[Handler]] = {} """Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]: Holds the handlers per group.""" self.groups: List[int] = [] """List[:obj:`int`]: A list with all groups.""" self.error_handlers: Dict[Callable, Union[bool, DefaultValue]] = {} """Dict[:obj:`callable`, :obj:`bool`]: A dict, where the keys are error handlers and the values indicate whether they are to be run asynchronously.""" self.running = False """:obj:`bool`: Indicates if this dispatcher is running.""" self.__stop_event = Event() self.__exception_event = exception_event or Event() self.__async_queue: Queue = Queue() self.__async_threads: Set[Thread] = set() # For backward compatibility, we allow a "singleton" mode for the dispatcher. When there's # only one instance of Dispatcher, it will be possible to use the `run_async` decorator. with self.__singleton_lock: if self.__singleton_semaphore.acquire(blocking=False): # pylint: disable=R1732 self._set_singleton(self) else: self._set_singleton(None) def __setattr__(self, key: str, value: object) -> None: # Mangled names don't automatically apply in __setattr__ (see # https://docs.python.org/3/tutorial/classes.html#private-variables), so we have to make # it mangled so they don't raise TelegramDeprecationWarning unnecessarily if key.startswith('__'): key = f"_{self.__class__.__name__}{key}" if issubclass(self.__class__, Dispatcher) and self.__class__ is not Dispatcher: object.__setattr__(self, key, value) return set_new_attribute_deprecated(self, key, value) @property def exception_event(self) -> Event: # skipcq: PY-D0003 return self.__exception_event def _init_async_threads(self, base_name: str, workers: int) -> None: base_name = f'{base_name}_' if base_name else '' for i in range(workers): thread = Thread(target=self._pooled, name=f'Bot:{self.bot.id}:worker:{base_name}{i}') self.__async_threads.add(thread) thread.start() @classmethod def _set_singleton(cls, val: Optional['Dispatcher']) -> None: cls.logger.debug('Setting singleton dispatcher as %s', val) cls.__singleton = weakref.ref(val) if val else None @classmethod def get_instance(cls) -> 'Dispatcher': """Get the singleton instance of this class. Returns: :class:`telegram.ext.Dispatcher` Raises: RuntimeError """ if cls.__singleton is not None: return cls.__singleton() # type: ignore[return-value] # pylint: disable=not-callable raise RuntimeError(f'{cls.__name__} not initialized or multiple instances exist') def _pooled(self) -> None: thr_name = current_thread().name while 1: promise = self.__async_queue.get() # If unpacking fails, the thread pool is being closed from Updater._join_async_threads if not isinstance(promise, Promise): self.logger.debug( "Closing run_async thread %s/%d", thr_name, len(self.__async_threads) ) break promise.run() if not promise.exception: self.update_persistence(update=promise.update) continue if isinstance(promise.exception, DispatcherHandlerStop): self.logger.warning( 'DispatcherHandlerStop is not supported with async functions; func: %s', promise.pooled_function.__name__, ) continue # Avoid infinite recursion of error handlers. if promise.pooled_function in self.error_handlers: self.logger.error('An uncaught error was raised while handling the error.') continue # Don't perform error handling for a `Promise` with deactivated error handling. This # should happen only via the deprecated `@run_async` decorator or `Promises` created # within error handlers if not promise.error_handling: self.logger.error('A promise with deactivated error handling raised an error.') continue # If we arrive here, an exception happened in the promise and was neither # DispatcherHandlerStop nor raised by an error handler. So we can and must handle it try: self.dispatch_error(promise.update, promise.exception, promise=promise) except Exception: self.logger.exception('An uncaught error was raised while handling the error.') def run_async( self, func: Callable[..., object], *args: object, update: object = None, **kwargs: object ) -> Promise: """ Queue a function (with given args/kwargs) to be run asynchronously. Exceptions raised by the function will be handled by the error handlers registered with :meth:`add_error_handler`. Warning: * If you're using ``@run_async``/:meth:`run_async` you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. * Calling a function through :meth:`run_async` from within an error handler can lead to an infinite error handling loop. Args: func (:obj:`callable`): The function to run in the thread. *args (:obj:`tuple`, optional): Arguments to ``func``. update (:class:`telegram.Update` | :obj:`object`, optional): The update associated with the functions call. If passed, it will be available in the error handlers, in case an exception is raised by :attr:`func`. **kwargs (:obj:`dict`, optional): Keyword arguments to ``func``. Returns: Promise """ return self._run_async(func, *args, update=update, error_handling=True, **kwargs) def _run_async( self, func: Callable[..., object], *args: object, update: object = None, error_handling: bool = True, **kwargs: object, ) -> Promise: # TODO: Remove error_handling parameter once we drop the @run_async decorator promise = Promise(func, args, kwargs, update=update, error_handling=error_handling) self.__async_queue.put(promise) return promise def start(self, ready: Event = None) -> None: """Thread target of thread 'dispatcher'. Runs in background and processes the update queue. Args: ready (:obj:`threading.Event`, optional): If specified, the event will be set once the dispatcher is ready. """ if self.running: self.logger.warning('already running') if ready is not None: ready.set() return if self.__exception_event.is_set(): msg = 'reusing dispatcher after exception event is forbidden' self.logger.error(msg) raise TelegramError(msg) self._init_async_threads(str(uuid4()), self.workers) self.running = True self.logger.debug('Dispatcher started') if ready is not None: ready.set() while 1: try: # Pop update from update queue. update = self.update_queue.get(True, 1) except Empty: if self.__stop_event.is_set(): self.logger.debug('orderly stopping') break if self.__exception_event.is_set(): self.logger.critical('stopping due to exception in another thread') break continue self.logger.debug('Processing Update: %s', update) self.process_update(update) self.update_queue.task_done() self.running = False self.logger.debug('Dispatcher thread stopped') def stop(self) -> None: """Stops the thread.""" if self.running: self.__stop_event.set() while self.running: sleep(0.1) self.__stop_event.clear() # async threads must be join()ed only after the dispatcher thread was joined, # otherwise we can still have new async threads dispatched threads = list(self.__async_threads) total = len(threads) # Stop all threads in the thread pool by put()ting one non-tuple per thread for i in range(total): self.__async_queue.put(None) for i, thr in enumerate(threads): self.logger.debug('Waiting for async thread %s/%s to end', i + 1, total) thr.join() self.__async_threads.remove(thr) self.logger.debug('async thread %s/%s has ended', i + 1, total) @property def has_running_threads(self) -> bool: # skipcq: PY-D0003 return self.running or bool(self.__async_threads) def process_update(self, update: object) -> None: """Processes a single update and updates the persistence. Note: If the update is handled by least one synchronously running handlers (i.e. ``run_async=False``), :meth:`update_persistence` is called *once* after all handlers synchronous handlers are done. Each asynchronously running handler will trigger :meth:`update_persistence` on its own. Args: update (:class:`telegram.Update` | :obj:`object` | \ :class:`telegram.error.TelegramError`): The update to process. """ # An error happened while polling if isinstance(update, TelegramError): try: self.dispatch_error(None, update) except Exception: self.logger.exception('An uncaught error was raised while handling the error.') return context = None handled = False sync_modes = [] for group in self.groups: try: for handler in self.handlers[group]: check = handler.check_update(update) if check is not None and check is not False: if not context and self.use_context: context = self.context_types.context.from_update(update, self) context.refresh_data() handled = True sync_modes.append(handler.run_async) handler.handle_update(update, self, check, context) break # Stop processing with any other handler. except DispatcherHandlerStop: self.logger.debug('Stopping further handlers due to DispatcherHandlerStop') self.update_persistence(update=update) break # Dispatch any error. except Exception as exc: try: self.dispatch_error(update, exc) except DispatcherHandlerStop: self.logger.debug('Error handler stopped further handlers') break # Errors should not stop the thread. except Exception: self.logger.exception('An uncaught error was raised while handling the error.') # Update persistence, if handled handled_only_async = all(sync_modes) if handled: # Respect default settings if all(mode is DEFAULT_FALSE for mode in sync_modes) and self.bot.defaults: handled_only_async = self.bot.defaults.run_async # If update was only handled by async handlers, we don't need to update here if not handled_only_async: self.update_persistence(update=update) def add_handler(self, handler: Handler[UT, 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.DispatcherHandlerStop`. A handler must be an instance of a subclass of :class:`telegram.ext.Handler`. 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.DispatcherHandlerStop` 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 should handle an update (see :attr:`telegram.ext.Handler.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. Args: handler (:class:`telegram.ext.Handler`): A Handler instance. group (:obj:`int`, optional): The group identifier. Default is 0. """ # Unfortunately due to circular imports this has to be here from .conversationhandler import ConversationHandler # pylint: disable=C0415 if not isinstance(handler, Handler): raise TypeError(f'handler is not an instance of {Handler.__name__}') if not isinstance(group, int): raise TypeError('group is not int') # For some reason MyPy infers the type of handler is here, # so for now we just ignore all the errors if ( isinstance(handler, ConversationHandler) and handler.persistent # type: ignore[attr-defined] and handler.name # type: ignore[attr-defined] ): if not self.persistence: raise ValueError( f"ConversationHandler {handler.name} " # type: ignore[attr-defined] f"can not be persistent if dispatcher has no persistence" ) handler.persistence = self.persistence # type: ignore[attr-defined] handler.conversations = ( # type: ignore[attr-defined] self.persistence.get_conversations(handler.name) # type: ignore[attr-defined] ) if group not in self.handlers: self.handlers[group] = [] self.groups.append(group) self.groups = sorted(self.groups) self.handlers[group].append(handler) def remove_handler(self, handler: Handler, group: int = DEFAULT_GROUP) -> None: """Remove a handler from the specified group. Args: handler (:class:`telegram.ext.Handler`): A Handler 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] self.groups.remove(group) def update_persistence(self, update: object = None) -> None: """Update :attr:`user_data`, :attr:`chat_data` and :attr:`bot_data` in :attr:`persistence`. Args: update (:class:`telegram.Update`, optional): The update to process. If passed, only the corresponding ``user_data`` and ``chat_data`` will be updated. """ with self._update_persistence_lock: self.__update_persistence(update) def __update_persistence(self, update: object = None) -> None: if self.persistence: # We use list() here in order to decouple chat_ids from self.chat_data, as dict view # objects will change, when the dict does and we want to loop over chat_ids chat_ids = list(self.chat_data.keys()) user_ids = list(self.user_data.keys()) if isinstance(update, Update): if update.effective_chat: chat_ids = [update.effective_chat.id] else: chat_ids = [] if update.effective_user: user_ids = [update.effective_user.id] else: user_ids = [] if self.persistence.store_callback_data: self.bot = cast(telegram.ext.extbot.ExtBot, self.bot) try: self.persistence.update_callback_data( self.bot.callback_data_cache.persistence_data ) except Exception as exc: try: self.dispatch_error(update, exc) except Exception: message = ( 'Saving callback data raised an error and an ' 'uncaught error was raised while handling ' 'the error with an error_handler' ) self.logger.exception(message) if self.persistence.store_bot_data: try: self.persistence.update_bot_data(self.bot_data) except Exception as exc: try: self.dispatch_error(update, exc) except Exception: message = ( 'Saving bot data raised an error and an ' 'uncaught error was raised while handling ' 'the error with an error_handler' ) self.logger.exception(message) if self.persistence.store_chat_data: for chat_id in chat_ids: try: self.persistence.update_chat_data(chat_id, self.chat_data[chat_id]) except Exception as exc: try: self.dispatch_error(update, exc) except Exception: message = ( 'Saving chat data raised an error and an ' 'uncaught error was raised while handling ' 'the error with an error_handler' ) self.logger.exception(message) if self.persistence.store_user_data: for user_id in user_ids: try: self.persistence.update_user_data(user_id, self.user_data[user_id]) except Exception as exc: try: self.dispatch_error(update, exc) except Exception: message = ( 'Saving user data raised an error and an ' 'uncaught error was raised while handling ' 'the error with an error_handler' ) self.logger.exception(message) def add_error_handler( self, callback: Callable[[object, CCT], None], run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, # pylint: disable=W0621 ) -> None: """Registers an error handler in the Dispatcher. This handler will receive every error which happens in your bot. Note: Attempts to add the same callback multiple times will be ignored. Warning: The errors handled within these handlers won't show up in the logger, so you need to make sure that you reraise the error. Args: callback (:obj:`callable`): The callback function for this error handler. Will be called when an error is raised. Callback signature for context based API: ``def callback(update: object, context: CallbackContext)`` The error that happened will be present in context.error. run_async (:obj:`bool`, optional): Whether this handlers callback should be run asynchronously using :meth:`run_async`. Defaults to :obj:`False`. Note: See https://git.io/fxJuV for more info about switching to context based API. """ if callback in self.error_handlers: self.logger.debug('The callback is already registered as an error handler. Ignoring.') return if run_async is DEFAULT_FALSE and self.bot.defaults and self.bot.defaults.run_async: run_async = True self.error_handlers[callback] = run_async def remove_error_handler(self, callback: Callable[[object, CCT], None]) -> None: """Removes an error handler. Args: callback (:obj:`callable`): The error handler to remove. """ self.error_handlers.pop(callback, None) def dispatch_error( self, update: Optional[object], error: Exception, promise: Promise = None ) -> None: """Dispatches an error. Args: update (:obj:`object` | :class:`telegram.Update`): The update that caused the error. error (:obj:`Exception`): The error that was raised. promise (:class:`telegram.utils.Promise`, optional): The promise whose pooled function raised the error. """ async_args = None if not promise else promise.args async_kwargs = None if not promise else promise.kwargs if self.error_handlers: for callback, run_async in self.error_handlers.items(): # pylint: disable=W0621 if self.use_context: context = self.context_types.context.from_error( update, error, self, async_args=async_args, async_kwargs=async_kwargs ) if run_async: self.run_async(callback, update, context, update=update) else: callback(update, context) else: if run_async: self.run_async(callback, self.bot, update, error, update=update) else: callback(self.bot, update, error) else: self.logger.exception( 'No error handlers are registered, logging exception.', exc_info=error ) python-telegram-bot-13.11/telegram/ext/extbot.py000066400000000000000000000330551417656324400217210ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=E0611,E0213,E1102,C0103,E1101,R0913,R0904 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 typing import Union, cast, List, Callable, Optional, Tuple, TypeVar, TYPE_CHECKING, Sequence import telegram.bot from telegram import ( ReplyMarkup, Message, InlineKeyboardMarkup, Poll, MessageId, Update, Chat, CallbackQuery, ) from telegram.ext.callbackdatacache import CallbackDataCache from telegram.utils.types import JSONDict, ODVInput, DVInput from ..utils.helpers import DEFAULT_NONE if TYPE_CHECKING: from telegram import InlineQueryResult, MessageEntity from telegram.utils.request import Request from .defaults import Defaults HandledTypes = TypeVar('HandledTypes', bound=Union[Message, CallbackQuery, Chat]) class ExtBot(telegram.bot.Bot): """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`. .. versionadded:: 13.6 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. For more details, please see our `wiki `_. Defaults to :obj:`False`. Attributes: arbitrary_callback_data (:obj:`bool` | :obj:`int`): Whether this bot instance allows to use arbitrary objects as callback data for :class:`telegram.InlineKeyboardButton`. callback_data_cache (:class:`telegram.ext.CallbackDataCache`): The cache for objects passed as callback data for :class:`telegram.InlineKeyboardButton`. """ __slots__ = ('arbitrary_callback_data', 'callback_data_cache') # The ext_bot argument is a little hack to get warnings handled correctly. # It's not very clean, but the warnings will be dropped at some point anyway. def __setattr__(self, key: str, value: object, ext_bot: bool = True) -> None: if issubclass(self.__class__, ExtBot) and self.__class__ is not ExtBot: object.__setattr__(self, key, value) return super().__setattr__(key, value, ext_bot=ext_bot) # type: ignore[call-arg] def __init__( self, token: str, base_url: str = None, base_file_url: str = None, request: 'Request' = None, private_key: bytes = None, private_key_password: bytes = None, defaults: 'Defaults' = None, arbitrary_callback_data: Union[bool, int] = False, ): super().__init__( token=token, base_url=base_url, base_file_url=base_file_url, request=request, private_key=private_key, private_key_password=private_key_password, ) # We don't pass this to super().__init__ to avoid the deprecation warning self.defaults = defaults # set up callback_data if not isinstance(arbitrary_callback_data, bool): maxsize = cast(int, arbitrary_callback_data) self.arbitrary_callback_data = True else: maxsize = 1024 self.arbitrary_callback_data = arbitrary_callback_data self.callback_data_cache: CallbackDataCache = CallbackDataCache(bot=self, maxsize=maxsize) def _replace_keyboard(self, reply_markup: Optional[ReplyMarkup]) -> Optional[ReplyMarkup]: # 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.arbitrary_callback_data: return self.callback_data_cache.process_keyboard(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 check if the reply markup (if any) was actually sent by this caches 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: *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 not self.arbitrary_callback_data: 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 obj.reply_to_message.pinned_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 obj.pinned_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 def _message( self, endpoint: str, data: JSONDict, reply_to_message_id: int = None, disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, ) -> Union[bool, Message]: # We override this method to call self._replace_keyboard and self._insert_callback_data. # This covers most methods that have a reply_markup result = super()._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, timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) if isinstance(result, Message): self._insert_callback_data(result) return result def get_updates( self, offset: int = None, limit: int = 100, timeout: float = 0, read_latency: float = 2.0, allowed_updates: List[str] = None, api_kwargs: JSONDict = None, ) -> List[Update]: updates = super().get_updates( offset=offset, limit=limit, timeout=timeout, read_latency=read_latency, allowed_updates=allowed_updates, api_kwargs=api_kwargs, ) for update in updates: self.insert_callback_data(update) return updates def _effective_inline_results( # pylint: disable=R0201 self, results: Union[ Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]] ], next_offset: str = None, current_offset: 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 not self.arbitrary_callback_data: 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) markup = self._replace_keyboard(result.reply_markup) # type: ignore[attr-defined] new_result.reply_markup = markup results.append(new_result) return results, next_offset def stop_poll( self, chat_id: Union[int, str], message_id: int, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Poll: # We override this method to call self._replace_keyboard return super().stop_poll( chat_id=chat_id, message_id=message_id, reply_markup=self._replace_keyboard(reply_markup), timeout=timeout, api_kwargs=api_kwargs, ) def copy_message( self, chat_id: Union[int, str], from_chat_id: Union[str, int], message_id: int, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, ) -> MessageId: # We override this method to call self._replace_keyboard return 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), timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) def get_chat( self, chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Chat: # We override this method to call self._insert_callback_data result = super().get_chat(chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs) return self._insert_callback_data(result) # updated camelCase aliases getChat = get_chat """Alias for :meth:`get_chat`""" copyMessage = copy_message """Alias for :meth:`copy_message`""" getUpdates = get_updates """Alias for :meth:`get_updates`""" stopPoll = stop_poll """Alias for :meth:`stop_poll`""" python-telegram-bot-13.11/telegram/ext/filters.py000066400000000000000000002531261417656324400220670ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=C0112, C0103, W0221 """This module contains the Filters for use with the MessageHandler class.""" import re import warnings from abc import ABC, abstractmethod from sys import version_info as py_ver from threading import Lock from typing import ( Dict, FrozenSet, List, Match, Optional, Pattern, Set, Tuple, Union, cast, NoReturn, ) from telegram import Chat, Message, MessageEntity, Update, User __all__ = [ 'Filters', 'BaseFilter', 'MessageFilter', 'UpdateFilter', 'InvertedFilter', 'MergedFilter', 'XORFilter', ] from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated from telegram.utils.types import SLT DataDict = Dict[str, list] class BaseFilter(ABC): """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 `and`, `or` and `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 :meth:`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. Attributes: 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). """ if py_ver < (3, 7): __slots__ = ('_name', '_data_filter') else: __slots__ = ('_name', '_data_filter', '__dict__') # type: ignore[assignment] def __new__(cls, *args: object, **kwargs: object) -> 'BaseFilter': # pylint: disable=W0613 instance = super().__new__(cls) instance._name = None instance._data_filter = False return instance @abstractmethod def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: ... def __and__(self, other: 'BaseFilter') -> 'BaseFilter': return MergedFilter(self, and_filter=other) def __or__(self, other: 'BaseFilter') -> 'BaseFilter': return MergedFilter(self, or_filter=other) def __xor__(self, other: 'BaseFilter') -> 'BaseFilter': return XORFilter(self, other) def __invert__(self) -> 'BaseFilter': return InvertedFilter(self) def __setattr__(self, key: str, value: object) -> None: # Allow setting custom attributes w/o warning for user defined custom filters. # To differentiate between a custom and a PTB filter, we use this hacky but # simple way of checking the module name where the class is defined from. if ( issubclass(self.__class__, (UpdateFilter, MessageFilter)) and self.__class__.__module__ != __name__ ): # __name__ is telegram.ext.filters object.__setattr__(self, key, value) return set_new_attribute_deprecated(self, key, value) @property def data_filter(self) -> bool: return self._data_filter @data_filter.setter def data_filter(self, value: bool) -> None: self._data_filter = value @property def name(self) -> Optional[str]: return self._name @name.setter def name(self, name: Optional[str]) -> None: self._name = name # pylint: disable=E0237 def __repr__(self) -> str: # We do this here instead of in a __init__ so filter don't have to call __init__ or super() if self.name is None: self.name = self.__class__.__name__ return self.name class MessageFilter(BaseFilter): """Base class for all Message Filters. In contrast to :class:`UpdateFilter`, the object passed to :meth:`filter` is ``update.effective_message``. Please see :class:`telegram.ext.filters.BaseFilter` for details on how to create custom filters. Attributes: 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__ = () def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: return self.filter(update.effective_message) @abstractmethod def filter(self, message: Message) -> Optional[Union[bool, DataDict]]: """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 ``update``, which allows to create filters like :attr:`Filters.update.edited_message`. Please see :class:`telegram.ext.filters.BaseFilter` for details on how to create custom filters. Attributes: 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__ = () def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: return self.filter(update) @abstractmethod def filter(self, update: Update) -> Optional[Union[bool, DataDict]]: """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__ = ('f',) def __init__(self, f: BaseFilter): self.f = f def filter(self, update: Update) -> bool: return not bool(self.f(update)) @property def name(self) -> str: return f"" @name.setter def name(self, name: str) -> NoReturn: raise RuntimeError('Cannot set name for InvertedFilter') 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__ = ('base_filter', 'and_filter', 'or_filter') def __init__( self, base_filter: BaseFilter, and_filter: BaseFilter = None, or_filter: BaseFilter = None ): 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]) -> DataDict: base = base_output if isinstance(base_output, dict) else {} comp = comp_output if isinstance(comp_output, dict) else {} for k in comp.keys(): # 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 def filter(self, update: Update) -> Union[bool, DataDict]: # pylint: disable=R0911 base_output = self.base_filter(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 falsey if base_output: comp_output = self.and_filter(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 truthey if base_output: if self.data_filter: return base_output return True comp_output = self.or_filter(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 MergedFilter') 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', 'xor_filter', 'merged_filter') def __init__(self, base_filter: BaseFilter, xor_filter: BaseFilter): 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, DataDict]]: return self.merged_filter(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 XORFilter') class _DiceEmoji(MessageFilter): __slots__ = ('emoji',) def __init__(self, emoji: str = None, name: str = None): self.name = f'Filters.dice.{name}' if name else 'Filters.dice' self.emoji = emoji class _DiceValues(MessageFilter): __slots__ = ('values', 'emoji') def __init__( self, values: SLT[int], name: str, emoji: str = None, ): self.values = [values] if isinstance(values, int) else values self.emoji = emoji self.name = f'{name}({values})' def filter(self, message: Message) -> bool: if message.dice and message.dice.value in self.values: if self.emoji: return message.dice.emoji == self.emoji return True return False def __call__( # type: ignore[override] self, update: Union[Update, List[int], Tuple[int]] ) -> Union[bool, '_DiceValues']: if isinstance(update, Update): return self.filter(update.effective_message) return self._DiceValues(update, self.name, emoji=self.emoji) def filter(self, message: Message) -> bool: if bool(message.dice): if self.emoji: return message.dice.emoji == self.emoji return True return False class Filters: """Predefined filters for use as the ``filter`` argument of :class:`telegram.ext.MessageHandler`. Examples: Use ``MessageHandler(Filters.video, callback_method)`` to filter all video messages. Use ``MessageHandler(Filters.contact, callback_method)`` for all contacts. etc. """ __slots__ = ('__dict__',) def __setattr__(self, key: str, value: object) -> None: set_new_attribute_deprecated(self, key, value) class _All(MessageFilter): __slots__ = () name = 'Filters.all' def filter(self, message: Message) -> bool: return True all = _All() """All Messages.""" class _Text(MessageFilter): __slots__ = () name = 'Filters.text' class _TextStrings(MessageFilter): __slots__ = ('strings',) def __init__(self, strings: Union[List[str], Tuple[str]]): self.strings = strings self.name = f'Filters.text({strings})' def filter(self, message: Message) -> bool: if message.text: return message.text in self.strings return False def __call__( # type: ignore[override] self, update: Union[Update, List[str], Tuple[str]] ) -> Union[bool, '_TextStrings']: if isinstance(update, Update): return self.filter(update.effective_message) return self._TextStrings(update) def filter(self, message: Message) -> bool: return bool(message.text) text = _Text() """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: To allow any text message, simply use ``MessageHandler(Filters.text, callback_method)``. 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) Note: * Dice messages don't have text. If you want to filter either text or dice messages, use ``Filters.text | Filters.dice``. * 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: update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which messages to allow. Only exact matches are allowed. If not specified, will allow any text message. """ class _Caption(MessageFilter): __slots__ = () name = 'Filters.caption' class _CaptionStrings(MessageFilter): __slots__ = ('strings',) def __init__(self, strings: Union[List[str], Tuple[str]]): self.strings = strings self.name = f'Filters.caption({strings})' def filter(self, message: Message) -> bool: if message.caption: return message.caption in self.strings return False def __call__( # type: ignore[override] self, update: Union[Update, List[str], Tuple[str]] ) -> Union[bool, '_CaptionStrings']: if isinstance(update, Update): return self.filter(update.effective_message) return self._CaptionStrings(update) def filter(self, message: Message) -> bool: return bool(message.caption) caption = _Caption() """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, callback_method)`` Args: update (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. """ class _Command(MessageFilter): __slots__ = () name = 'Filters.command' class _CommandOnlyStart(MessageFilter): __slots__ = ('only_start',) def __init__(self, only_start: bool): self.only_start = only_start self.name = f'Filters.command({only_start})' def filter(self, message: Message) -> bool: return bool( message.entities and any(e.type == MessageEntity.BOT_COMMAND for e in message.entities) ) def __call__( # type: ignore[override] self, update: Union[bool, Update] ) -> Union[bool, '_CommandOnlyStart']: if isinstance(update, Update): return self.filter(update.effective_message) return self._CommandOnlyStart(update) def filter(self, message: Message) -> bool: return bool( message.entities and message.entities[0].type == MessageEntity.BOT_COMMAND and message.entities[0].offset == 0 ) command = _Command() """ 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, command_at_start_callback) MessageHandler(Filters.command(False), command_anywhere_callback) Note: ``Filters.text`` also accepts messages containing a command. Args: update (:obj:`bool`, optional): Whether to only allow messages that `start` with a bot command. Defaults to :obj:`True`. """ class regex(MessageFilter): """ Filters updates by searching for an occurrence of ``pattern`` in the message text. The ``re.search()`` function is used to determine whether an update should be filtered. Refer to the documentation of the ``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 `and`, `or` and `not`. This means that for example: >>> Filters.regex(r'(a?x)') | Filters.regex(r'(b?x)') With a message.text of `x`, will only ever return the matches for the first filter, since the second one is never evaluated. Args: pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. """ __slots__ = ('pattern',) data_filter = True def __init__(self, pattern: Union[str, Pattern]): if isinstance(pattern, str): pattern = re.compile(pattern) pattern = cast(Pattern, pattern) self.pattern: Pattern = pattern self.name = f'Filters.regex({self.pattern})' def filter(self, message: Message) -> Optional[Dict[str, List[Match]]]: """""" # remove method from docs if message.text: match = self.pattern.search(message.text) if match: return {'matches': [match]} return {} class caption_regex(MessageFilter): """ Filters updates by searching for an occurrence of ``pattern`` in the message caption. This filter works similarly to :class:`Filters.regex`, with the only exception being that it applies to the message caption instead of the text. Examples: Use ``MessageHandler(Filters.photo & Filters.caption_regex(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` | :obj:`Pattern`): The regex pattern. """ __slots__ = ('pattern',) data_filter = True def __init__(self, pattern: Union[str, Pattern]): if isinstance(pattern, str): pattern = re.compile(pattern) pattern = cast(Pattern, pattern) self.pattern: Pattern = pattern self.name = f'Filters.caption_regex({self.pattern})' def filter(self, message: Message) -> Optional[Dict[str, List[Match]]]: """""" # remove method from docs if message.caption: match = self.pattern.search(message.caption) if match: return {'matches': [match]} return {} class _Reply(MessageFilter): __slots__ = () name = 'Filters.reply' def filter(self, message: Message) -> bool: return bool(message.reply_to_message) reply = _Reply() """Messages that are a reply to another message.""" class _Audio(MessageFilter): __slots__ = () name = 'Filters.audio' def filter(self, message: Message) -> bool: return bool(message.audio) audio = _Audio() """Messages that contain :class:`telegram.Audio`.""" class _Document(MessageFilter): __slots__ = () name = 'Filters.document' class category(MessageFilter): """Filters documents by their category in the mime-type attribute. 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. Example: Filters.document.category('audio/') returns :obj:`True` for all types of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'. """ __slots__ = ('_category',) def __init__(self, category: Optional[str]): """Initialize the category you want to filter Args: category (str, optional): category of the media you want to filter """ self._category = category self.name = f"Filters.document.category('{self._category}')" def filter(self, message: Message) -> bool: """""" # remove method from docs if message.document: return message.document.mime_type.startswith(self._category) return False application = category('application/') audio = category('audio/') image = category('image/') video = category('video/') text = category('text/') class mime_type(MessageFilter): """This Filter filters documents by their mime-type attribute 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. Example: ``Filters.document.mime_type('audio/mpeg')`` filters all audio in mp3 format. """ __slots__ = ('mimetype',) def __init__(self, mimetype: Optional[str]): self.mimetype = mimetype self.name = f"Filters.document.mime_type('{self.mimetype}')" def filter(self, message: Message) -> bool: """""" # remove method from docs if message.document: return message.document.mime_type == self.mimetype return False apk = mime_type('application/vnd.android.package-archive') doc = mime_type('application/msword') docx = mime_type('application/vnd.openxmlformats-officedocument.wordprocessingml.document') exe = mime_type('application/x-ms-dos-executable') gif = mime_type('video/mp4') jpg = mime_type('image/jpeg') mp3 = mime_type('audio/mpeg') pdf = mime_type('application/pdf') py = mime_type('text/x-python') svg = mime_type('image/svg+xml') txt = mime_type('text/plain') targz = mime_type('application/x-compressed-tar') wav = mime_type('audio/x-wav') xml = mime_type('application/xml') zip = mime_type('application/zip') class file_extension(MessageFilter): """This filter filters documents by their file ending/extension. 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. Example: * ``Filters.document.file_extension("jpg")`` filters files with extension ``".jpg"``. * ``Filters.document.file_extension(".jpg")`` filters files with extension ``"..jpg"``. * ``Filters.document.file_extension("Dockerfile", case_sensitive=True)`` filters files with extension ``".Dockerfile"`` minding the case. * ``Filters.document.file_extension(None)`` filters files without a dot in the filename. """ __slots__ = ('_file_extension', 'is_case_sensitive') def __init__(self, file_extension: Optional[str], case_sensitive: bool = False): """Initialize the extension you want to filter. 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`. """ self.is_case_sensitive = case_sensitive if file_extension is None: self._file_extension = None self.name = "Filters.document.file_extension(None)" elif self.is_case_sensitive: self._file_extension = f".{file_extension}" self.name = ( f"Filters.document.file_extension({file_extension!r}," " case_sensitive=True)" ) else: self._file_extension = f".{file_extension}".lower() self.name = f"Filters.document.file_extension({file_extension.lower()!r})" def filter(self, message: Message) -> bool: """""" # remove method from docs if message.document 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) def filter(self, message: Message) -> bool: return bool(message.document) document = _Document() """ Subset for messages containing a document/file. Examples: Use these filters like: ``Filters.document.mp3``, ``Filters.document.mime_type("text/plain")`` etc. Or use just ``Filters.document`` for all document messages. Attributes: category: Filters documents by their category in the mime-type attribute 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. Example: ``Filters.document.category('audio/')`` filters all types of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'. application: Same as ``Filters.document.category("application")``. audio: Same as ``Filters.document.category("audio")``. image: Same as ``Filters.document.category("image")``. video: Same as ``Filters.document.category("video")``. text: Same as ``Filters.document.category("text")``. mime_type: Filters documents by their mime-type attribute 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. Example: ``Filters.document.mime_type('audio/mpeg')`` filters all audio in mp3 format. apk: Same as ``Filters.document.mime_type("application/vnd.android.package-archive")``. doc: Same as ``Filters.document.mime_type("application/msword")``. docx: Same as ``Filters.document.mime_type("application/vnd.openxmlformats-\ officedocument.wordprocessingml.document")``. exe: Same as ``Filters.document.mime_type("application/x-ms-dos-executable")``. gif: Same as ``Filters.document.mime_type("video/mp4")``. jpg: Same as ``Filters.document.mime_type("image/jpeg")``. mp3: Same as ``Filters.document.mime_type("audio/mpeg")``. pdf: Same as ``Filters.document.mime_type("application/pdf")``. py: Same as ``Filters.document.mime_type("text/x-python")``. svg: Same as ``Filters.document.mime_type("image/svg+xml")``. txt: Same as ``Filters.document.mime_type("text/plain")``. targz: Same as ``Filters.document.mime_type("application/x-compressed-tar")``. wav: Same as ``Filters.document.mime_type("audio/x-wav")``. xml: Same as ``Filters.document.mime_type("application/xml")``. zip: Same as ``Filters.document.mime_type("application/zip")``. file_extension: This filter filters documents by their file ending/extension. 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. Example: * ``Filters.document.file_extension("jpg")`` filters files with extension ``".jpg"``. * ``Filters.document.file_extension(".jpg")`` filters files with extension ``"..jpg"``. * ``Filters.document.file_extension("Dockerfile", case_sensitive=True)`` filters files with extension ``".Dockerfile"`` minding the case. * ``Filters.document.file_extension(None)`` filters files without a dot in the filename. """ class _Animation(MessageFilter): __slots__ = () name = 'Filters.animation' def filter(self, message: Message) -> bool: return bool(message.animation) animation = _Animation() """Messages that contain :class:`telegram.Animation`.""" class _Photo(MessageFilter): __slots__ = () name = 'Filters.photo' def filter(self, message: Message) -> bool: return bool(message.photo) photo = _Photo() """Messages that contain :class:`telegram.PhotoSize`.""" class _Sticker(MessageFilter): __slots__ = () name = 'Filters.sticker' def filter(self, message: Message) -> bool: return bool(message.sticker) sticker = _Sticker() """Messages that contain :class:`telegram.Sticker`.""" class _Video(MessageFilter): __slots__ = () name = 'Filters.video' def filter(self, message: Message) -> bool: return bool(message.video) video = _Video() """Messages that contain :class:`telegram.Video`.""" class _Voice(MessageFilter): __slots__ = () name = 'Filters.voice' def filter(self, message: Message) -> bool: return bool(message.voice) voice = _Voice() """Messages that contain :class:`telegram.Voice`.""" class _VideoNote(MessageFilter): __slots__ = () name = 'Filters.video_note' def filter(self, message: Message) -> bool: return bool(message.video_note) video_note = _VideoNote() """Messages that contain :class:`telegram.VideoNote`.""" class _Contact(MessageFilter): __slots__ = () name = 'Filters.contact' def filter(self, message: Message) -> bool: return bool(message.contact) contact = _Contact() """Messages that contain :class:`telegram.Contact`.""" class _Location(MessageFilter): __slots__ = () name = 'Filters.location' def filter(self, message: Message) -> bool: return bool(message.location) location = _Location() """Messages that contain :class:`telegram.Location`.""" class _Venue(MessageFilter): __slots__ = () name = 'Filters.venue' def filter(self, message: Message) -> bool: return bool(message.venue) venue = _Venue() """Messages that contain :class:`telegram.Venue`.""" class _StatusUpdate(UpdateFilter): """Subset for messages containing a status update. Examples: Use these filters like: ``Filters.status_update.new_chat_members`` etc. Or use just ``Filters.status_update`` for all status update messages. """ __slots__ = () class _NewChatMembers(MessageFilter): __slots__ = () name = 'Filters.status_update.new_chat_members' def filter(self, message: Message) -> bool: return bool(message.new_chat_members) new_chat_members = _NewChatMembers() """Messages that contain :attr:`telegram.Message.new_chat_members`.""" class _LeftChatMember(MessageFilter): __slots__ = () name = 'Filters.status_update.left_chat_member' def filter(self, message: Message) -> bool: return bool(message.left_chat_member) left_chat_member = _LeftChatMember() """Messages that contain :attr:`telegram.Message.left_chat_member`.""" class _NewChatTitle(MessageFilter): __slots__ = () name = 'Filters.status_update.new_chat_title' def filter(self, message: Message) -> bool: return bool(message.new_chat_title) new_chat_title = _NewChatTitle() """Messages that contain :attr:`telegram.Message.new_chat_title`.""" class _NewChatPhoto(MessageFilter): __slots__ = () name = 'Filters.status_update.new_chat_photo' def filter(self, message: Message) -> bool: return bool(message.new_chat_photo) new_chat_photo = _NewChatPhoto() """Messages that contain :attr:`telegram.Message.new_chat_photo`.""" class _DeleteChatPhoto(MessageFilter): __slots__ = () name = 'Filters.status_update.delete_chat_photo' def filter(self, message: Message) -> bool: return bool(message.delete_chat_photo) delete_chat_photo = _DeleteChatPhoto() """Messages that contain :attr:`telegram.Message.delete_chat_photo`.""" class _ChatCreated(MessageFilter): __slots__ = () name = 'Filters.status_update.chat_created' 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() """Messages that contain :attr:`telegram.Message.group_chat_created`, :attr: `telegram.Message.supergroup_chat_created` or :attr: `telegram.Message.channel_chat_created`.""" class _MessageAutoDeleteTimerChanged(MessageFilter): __slots__ = () name = 'MessageAutoDeleteTimerChanged' def filter(self, message: Message) -> bool: return bool(message.message_auto_delete_timer_changed) message_auto_delete_timer_changed = _MessageAutoDeleteTimerChanged() """Messages that contain :attr:`message_auto_delete_timer_changed`""" class _Migrate(MessageFilter): __slots__ = () name = 'Filters.status_update.migrate' def filter(self, message: Message) -> bool: return bool(message.migrate_from_chat_id or message.migrate_to_chat_id) migrate = _Migrate() """Messages that contain :attr:`telegram.Message.migrate_from_chat_id` or :attr:`telegram.Message.migrate_to_chat_id`.""" class _PinnedMessage(MessageFilter): __slots__ = () name = 'Filters.status_update.pinned_message' def filter(self, message: Message) -> bool: return bool(message.pinned_message) pinned_message = _PinnedMessage() """Messages that contain :attr:`telegram.Message.pinned_message`.""" class _ConnectedWebsite(MessageFilter): __slots__ = () name = 'Filters.status_update.connected_website' def filter(self, message: Message) -> bool: return bool(message.connected_website) connected_website = _ConnectedWebsite() """Messages that contain :attr:`telegram.Message.connected_website`.""" class _ProximityAlertTriggered(MessageFilter): __slots__ = () name = 'Filters.status_update.proximity_alert_triggered' def filter(self, message: Message) -> bool: return bool(message.proximity_alert_triggered) proximity_alert_triggered = _ProximityAlertTriggered() """Messages that contain :attr:`telegram.Message.proximity_alert_triggered`.""" class _VoiceChatScheduled(MessageFilter): __slots__ = () name = 'Filters.status_update.voice_chat_scheduled' def filter(self, message: Message) -> bool: return bool(message.voice_chat_scheduled) voice_chat_scheduled = _VoiceChatScheduled() """Messages that contain :attr:`telegram.Message.voice_chat_scheduled`.""" class _VoiceChatStarted(MessageFilter): __slots__ = () name = 'Filters.status_update.voice_chat_started' def filter(self, message: Message) -> bool: return bool(message.voice_chat_started) voice_chat_started = _VoiceChatStarted() """Messages that contain :attr:`telegram.Message.voice_chat_started`.""" class _VoiceChatEnded(MessageFilter): __slots__ = () name = 'Filters.status_update.voice_chat_ended' def filter(self, message: Message) -> bool: return bool(message.voice_chat_ended) voice_chat_ended = _VoiceChatEnded() """Messages that contain :attr:`telegram.Message.voice_chat_ended`.""" class _VoiceChatParticipantsInvited(MessageFilter): __slots__ = () name = 'Filters.status_update.voice_chat_participants_invited' def filter(self, message: Message) -> bool: return bool(message.voice_chat_participants_invited) voice_chat_participants_invited = _VoiceChatParticipantsInvited() """Messages that contain :attr:`telegram.Message.voice_chat_participants_invited`.""" name = 'Filters.status_update' def filter(self, message: Update) -> bool: return bool( self.new_chat_members(message) or self.left_chat_member(message) or self.new_chat_title(message) or self.new_chat_photo(message) or self.delete_chat_photo(message) or self.chat_created(message) or self.message_auto_delete_timer_changed(message) or self.migrate(message) or self.pinned_message(message) or self.connected_website(message) or self.proximity_alert_triggered(message) or self.voice_chat_scheduled(message) or self.voice_chat_started(message) or self.voice_chat_ended(message) or self.voice_chat_participants_invited(message) ) status_update = _StatusUpdate() """Subset for messages containing a status update. Examples: Use these filters like: ``Filters.status_update.new_chat_members`` etc. Or use just ``Filters.status_update`` for all status update messages. Attributes: chat_created: Messages that contain :attr:`telegram.Message.group_chat_created`, :attr:`telegram.Message.supergroup_chat_created` or :attr:`telegram.Message.channel_chat_created`. connected_website: Messages that contain :attr:`telegram.Message.connected_website`. delete_chat_photo: Messages that contain :attr:`telegram.Message.delete_chat_photo`. left_chat_member: Messages that contain :attr:`telegram.Message.left_chat_member`. migrate: Messages that contain :attr:`telegram.Message.migrate_to_chat_id` or :attr:`telegram.Message.migrate_from_chat_id`. new_chat_members: Messages that contain :attr:`telegram.Message.new_chat_members`. new_chat_photo: Messages that contain :attr:`telegram.Message.new_chat_photo`. new_chat_title: Messages that contain :attr:`telegram.Message.new_chat_title`. message_auto_delete_timer_changed: Messages that contain :attr:`message_auto_delete_timer_changed`. .. versionadded:: 13.4 pinned_message: Messages that contain :attr:`telegram.Message.pinned_message`. proximity_alert_triggered: Messages that contain :attr:`telegram.Message.proximity_alert_triggered`. voice_chat_scheduled: Messages that contain :attr:`telegram.Message.voice_chat_scheduled`. .. versionadded:: 13.5 voice_chat_started: Messages that contain :attr:`telegram.Message.voice_chat_started`. .. versionadded:: 13.4 voice_chat_ended: Messages that contain :attr:`telegram.Message.voice_chat_ended`. .. versionadded:: 13.4 voice_chat_participants_invited: Messages that contain :attr:`telegram.Message.voice_chat_participants_invited`. .. versionadded:: 13.4 """ class _Forwarded(MessageFilter): __slots__ = () name = 'Filters.forwarded' def filter(self, message: Message) -> bool: return bool(message.forward_date) forwarded = _Forwarded() """Messages that are forwarded.""" class _Game(MessageFilter): __slots__ = () name = 'Filters.game' def filter(self, message: Message) -> bool: return bool(message.game) game = _Game() """Messages that contain :class:`telegram.Game`.""" class entity(MessageFilter): """ Filters messages to only allow those which have a :class:`telegram.MessageEntity` where their `type` matches `entity_type`. Examples: Example ``MessageHandler(Filters.entity("hashtag"), callback_method)`` Args: entity_type: 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 = entity_type self.name = f'Filters.entity({self.entity_type})' def filter(self, message: Message) -> bool: """""" # remove method from docs return any(entity.type == self.entity_type for entity in message.entities) class caption_entity(MessageFilter): """ Filters media messages to only allow those which have a :class:`telegram.MessageEntity` where their `type` matches `entity_type`. Examples: Example ``MessageHandler(Filters.caption_entity("hashtag"), callback_method)`` Args: entity_type: 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 = entity_type self.name = f'Filters.caption_entity({self.entity_type})' def filter(self, message: Message) -> bool: """""" # remove method from docs return any(entity.type == self.entity_type for entity in message.caption_entities) class _Private(MessageFilter): __slots__ = () name = 'Filters.private' def filter(self, message: Message) -> bool: warnings.warn( 'Filters.private is deprecated. Use Filters.chat_type.private instead.', TelegramDeprecationWarning, stacklevel=2, ) return message.chat.type == Chat.PRIVATE private = _Private() """ Messages sent in a private chat. Note: DEPRECATED. Use :attr:`telegram.ext.Filters.chat_type.private` instead. """ class _Group(MessageFilter): __slots__ = () name = 'Filters.group' def filter(self, message: Message) -> bool: warnings.warn( 'Filters.group is deprecated. Use Filters.chat_type.groups instead.', TelegramDeprecationWarning, stacklevel=2, ) return message.chat.type in [Chat.GROUP, Chat.SUPERGROUP] group = _Group() """ Messages sent in a group or a supergroup chat. Note: DEPRECATED. Use :attr:`telegram.ext.Filters.chat_type.groups` instead. """ class _ChatType(MessageFilter): __slots__ = () name = 'Filters.chat_type' class _Channel(MessageFilter): __slots__ = () name = 'Filters.chat_type.channel' def filter(self, message: Message) -> bool: return message.chat.type == Chat.CHANNEL channel = _Channel() class _Group(MessageFilter): __slots__ = () name = 'Filters.chat_type.group' def filter(self, message: Message) -> bool: return message.chat.type == Chat.GROUP group = _Group() class _SuperGroup(MessageFilter): __slots__ = () name = 'Filters.chat_type.supergroup' def filter(self, message: Message) -> bool: return message.chat.type == Chat.SUPERGROUP supergroup = _SuperGroup() class _Groups(MessageFilter): __slots__ = () name = 'Filters.chat_type.groups' def filter(self, message: Message) -> bool: return message.chat.type in [Chat.GROUP, Chat.SUPERGROUP] groups = _Groups() class _Private(MessageFilter): __slots__ = () name = 'Filters.chat_type.private' def filter(self, message: Message) -> bool: return message.chat.type == Chat.PRIVATE private = _Private() def filter(self, message: Message) -> bool: return bool(message.chat.type) chat_type = _ChatType() """Subset for filtering the type of chat. Examples: Use these filters like: ``Filters.chat_type.channel`` or ``Filters.chat_type.supergroup`` etc. Or use just ``Filters.chat_type`` for all chat types. Attributes: channel: Updates from channel group: Updates from group supergroup: Updates from supergroup groups: Updates from group *or* supergroup private: Updates sent in private chat """ class _ChatUserBaseFilter(MessageFilter, ABC): __slots__ = ( 'chat_id_name', 'username_name', 'allow_empty', '__lock', '_chat_ids', '_usernames', ) def __init__( self, chat_id: SLT[int] = None, username: SLT[str] = None, allow_empty: bool = False, ): self.chat_id_name = 'chat_id' self.username_name = 'username' self.allow_empty = allow_empty self.__lock = Lock() 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[Chat, User, None]: ... @staticmethod def _parse_chat_id(chat_id: SLT[int]) -> Set[int]: if chat_id is None: return set() if isinstance(chat_id, int): return {chat_id} return set(chat_id) @staticmethod def _parse_username(username: SLT[str]) -> Set[str]: if username is None: return set() if isinstance(username, str): return {username[1:] if username.startswith('@') else username} return {chat[1:] if chat.startswith('@') else chat for chat in username} def _set_chat_ids(self, chat_id: SLT[int]) -> None: with self.__lock: 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 = self._parse_chat_id(chat_id) def _set_usernames(self, username: SLT[str]) -> None: with self.__lock: 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 = self._parse_username(username) @property def chat_ids(self) -> FrozenSet[int]: with self.__lock: return frozenset(self._chat_ids) @chat_ids.setter def chat_ids(self, chat_id: SLT[int]) -> None: self._set_chat_ids(chat_id) @property def usernames(self) -> FrozenSet[str]: with self.__lock: return frozenset(self._usernames) @usernames.setter def usernames(self, username: SLT[str]) -> None: self._set_usernames(username) def add_usernames(self, username: SLT[str]) -> None: with self.__lock: 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 = self._parse_username(username) self._usernames |= parsed_username def add_chat_ids(self, chat_id: SLT[int]) -> None: with self.__lock: 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 = self._parse_chat_id(chat_id) self._chat_ids |= parsed_chat_id def remove_usernames(self, username: SLT[str]) -> None: with self.__lock: 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 = self._parse_username(username) self._usernames -= parsed_username def remove_chat_ids(self, chat_id: SLT[int]) -> None: with self.__lock: 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 = self._parse_chat_id(chat_id) self._chat_ids -= parsed_chat_id def filter(self, message: Message) -> bool: """""" # remove method from docs 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 user(_ChatUserBaseFilter): # pylint: disable=W0235 """Filters messages to allow only those which are from specified user ID(s) or username(s). Examples: ``MessageHandler(Filters.user(1234), callback_method)`` Warning: :attr:`user_ids` will give a *copy* of the saved user ids as :class:`frozenset`. This is to ensure thread safety. To add/remove a user, you should use :meth:`add_usernames`, :meth:`add_user_ids`, :meth:`remove_usernames` and :meth:`remove_user_ids`. Only update the entire set by ``filter.user_ids/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. Args: user_id(:class:`telegram.utils.types.SLT[int]`, optional): Which user ID(s) to allow through. username(:class:`telegram.utils.types.SLT[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: user_ids(set(:obj:`int`), optional): Which user ID(s) to allow through. usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to allow through. allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user is specified in :attr:`user_ids` and :attr:`usernames`. """ __slots__ = () def __init__( self, user_id: SLT[int] = None, username: SLT[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[User]: return message.from_user @property def user_ids(self) -> FrozenSet[int]: return self.chat_ids @user_ids.setter def user_ids(self, user_id: SLT[int]) -> None: self.chat_ids = user_id # type: ignore[assignment] def add_usernames(self, username: SLT[str]) -> None: """ Add one or more users to the allowed usernames. Args: username(:class:`telegram.utils.types.SLT[str]`, optional): Which username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. """ return super().add_usernames(username) def add_user_ids(self, user_id: SLT[int]) -> None: """ Add one or more users to the allowed user ids. Args: user_id(:class:`telegram.utils.types.SLT[int]`, optional): Which user ID(s) to allow through. """ return super().add_chat_ids(user_id) def remove_usernames(self, username: SLT[str]) -> None: """ Remove one or more users from allowed usernames. Args: username(:class:`telegram.utils.types.SLT[str]`, optional): Which username(s) to disallow through. Leading ``'@'`` s in usernames will be discarded. """ return super().remove_usernames(username) def remove_user_ids(self, user_id: SLT[int]) -> None: """ Remove one or more users from allowed user ids. Args: user_id(:class:`telegram.utils.types.SLT[int]`, optional): Which user ID(s) to disallow through. """ return super().remove_chat_ids(user_id) class via_bot(_ChatUserBaseFilter): # pylint: disable=W0235 """Filters messages to allow only those which are from specified via_bot ID(s) or username(s). Examples: ``MessageHandler(Filters.via_bot(1234), callback_method)`` Warning: :attr:`bot_ids` will give a *copy* of the saved bot ids as :class:`frozenset`. This is to ensure thread safety. To add/remove a bot, you should use :meth:`add_usernames`, :meth:`add_bot_ids`, :meth:`remove_usernames` and :meth:`remove_bot_ids`. Only update the entire set by ``filter.bot_ids/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 bots. Args: bot_id(:class:`telegram.utils.types.SLT[int]`, optional): Which bot ID(s) to allow through. username(:class:`telegram.utils.types.SLT[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: bot_ids(set(:obj:`int`), optional): Which bot ID(s) to allow through. usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to allow through. allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no bot is specified in :attr:`bot_ids` and :attr:`usernames`. """ __slots__ = () def __init__( self, bot_id: SLT[int] = None, username: SLT[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[User]: return message.via_bot @property def bot_ids(self) -> FrozenSet[int]: return self.chat_ids @bot_ids.setter def bot_ids(self, bot_id: SLT[int]) -> None: self.chat_ids = bot_id # type: ignore[assignment] def add_usernames(self, username: SLT[str]) -> None: """ Add one or more users to the allowed usernames. Args: username(:class:`telegram.utils.types.SLT[str]`, optional): Which username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. """ return super().add_usernames(username) def add_bot_ids(self, bot_id: SLT[int]) -> None: """ Add one or more users to the allowed user ids. Args: bot_id(:class:`telegram.utils.types.SLT[int]`, optional): Which bot ID(s) to allow through. """ return super().add_chat_ids(bot_id) def remove_usernames(self, username: SLT[str]) -> None: """ Remove one or more users from allowed usernames. Args: username(:class:`telegram.utils.types.SLT[str]`, optional): Which username(s) to disallow through. Leading ``'@'`` s in usernames will be discarded. """ return super().remove_usernames(username) def remove_bot_ids(self, bot_id: SLT[int]) -> None: """ Remove one or more users from allowed user ids. Args: bot_id(:class:`telegram.utils.types.SLT[int]`, optional): Which bot ID(s) to disallow through. """ return super().remove_chat_ids(bot_id) class chat(_ChatUserBaseFilter): # pylint: disable=W0235 """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_usernames`, :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update the entire set by ``filter.chat_ids/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 chats. Args: chat_id(:class:`telegram.utils.types.SLT[int]`, optional): Which chat ID(s) to allow through. username(:class:`telegram.utils.types.SLT[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` Raises: RuntimeError: If chat_id and username are both present. Attributes: chat_ids(set(:obj:`int`), optional): Which chat ID(s) to allow through. usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to allow through. allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat is specified in :attr:`chat_ids` and :attr:`usernames`. """ __slots__ = () def get_chat_or_user(self, message: Message) -> Optional[Chat]: return message.chat def add_usernames(self, username: SLT[str]) -> None: """ Add one or more chats to the allowed usernames. Args: username(:class:`telegram.utils.types.SLT[str]`, optional): Which username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. """ return super().add_usernames(username) def add_chat_ids(self, chat_id: SLT[int]) -> None: """ Add one or more chats to the allowed chat ids. Args: chat_id(:class:`telegram.utils.types.SLT[int]`, optional): Which chat ID(s) to allow through. """ return super().add_chat_ids(chat_id) def remove_usernames(self, username: SLT[str]) -> None: """ Remove one or more chats from allowed usernames. Args: username(:class:`telegram.utils.types.SLT[str]`, optional): Which username(s) to disallow through. Leading ``'@'`` s in usernames will be discarded. """ return super().remove_usernames(username) def remove_chat_ids(self, chat_id: SLT[int]) -> None: """ Remove one or more chats from allowed chat ids. Args: chat_id(:class:`telegram.utils.types.SLT[int]`, optional): Which chat ID(s) to disallow through. """ return super().remove_chat_ids(chat_id) class forwarded_from(_ChatUserBaseFilter): # pylint: disable=W0235 """Filters messages to allow only those which are forwarded from the specified chat ID(s) or username(s) based on :attr:`telegram.Message.forward_from` and :attr:`telegram.Message.forward_from_chat`. .. versionadded:: 13.5 Examples: ``MessageHandler(Filters.forwarded_from(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 both :attr:`telegram.Message.forwarded_from` and :attr:`telegram.Message.forwarded_from_chat` are :obj:`None`. 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_usernames`, :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update the entire set by ``filter.chat_ids/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 chats. Args: chat_id(:class:`telegram.utils.types.SLT[int]`, optional): Which chat/user ID(s) to allow through. username(:class:`telegram.utils.types.SLT[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`. Raises: RuntimeError: If both chat_id and username are present. Attributes: chat_ids(set(:obj:`int`), optional): Which chat/user ID(s) to allow through. usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to allow through. allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat is specified in :attr:`chat_ids` and :attr:`usernames`. """ __slots__ = () def get_chat_or_user(self, message: Message) -> Union[User, Chat, None]: return message.forward_from or message.forward_from_chat def add_usernames(self, username: SLT[str]) -> None: """ Add one or more chats to the allowed usernames. Args: username(:class:`telegram.utils.types.SLT[str]`, optional): Which username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. """ return super().add_usernames(username) def add_chat_ids(self, chat_id: SLT[int]) -> None: """ Add one or more chats to the allowed chat ids. Args: chat_id(:class:`telegram.utils.types.SLT[int]`, optional): Which chat/user ID(s) to allow through. """ return super().add_chat_ids(chat_id) def remove_usernames(self, username: SLT[str]) -> None: """ Remove one or more chats from allowed usernames. Args: username(:class:`telegram.utils.types.SLT[str]`, optional): Which username(s) to disallow through. Leading ``'@'`` s in usernames will be discarded. """ return super().remove_usernames(username) def remove_chat_ids(self, chat_id: SLT[int]) -> None: """ Remove one or more chats from allowed chat ids. Args: chat_id(:class:`telegram.utils.types.SLT[int]`, optional): Which chat/user ID(s) to disallow through. """ return super().remove_chat_ids(chat_id) class sender_chat(_ChatUserBaseFilter): # pylint: disable=W0235 """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.sender_chat(-1234), callback_method)``. * To filter for messages of anonymous admins in a super group with username ``@anonymous``, use ``MessageHandler(Filters.sender_chat(username='anonymous'), callback_method)``. * To filter for messages sent to a group by *any* channel, use ``MessageHandler(Filters.sender_chat.channel, callback_method)``. * To filter for messages of anonymous admins in *any* super group, use ``MessageHandler(Filters.sender_chat.super_group, callback_method)``. 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:`Filters.is_automatic_forward` Warning: :attr:`chat_ids` will return 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_usernames`, :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update the entire set by ``filter.chat_ids/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 chats. Args: chat_id(:class:`telegram.utils.types.SLT[int]`, optional): Which sender chat chat ID(s) to allow through. username(:class:`telegram.utils.types.SLT[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` Raises: RuntimeError: If both chat_id and username are present. Attributes: chat_ids(set(:obj:`int`), optional): Which sender chat chat ID(s) to allow through. usernames(set(:obj:`str`), optional): Which sender chat username(s) (without leading ``'@'``) to allow through. allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no sender chat is specified in :attr:`chat_ids` and :attr:`usernames`. super_group: Messages whose sender chat is a super group. Examples: ``Filters.sender_chat.supergroup`` channel: Messages whose sender chat is a channel. Examples: ``Filters.sender_chat.channel`` """ __slots__ = () def get_chat_or_user(self, message: Message) -> Optional[Chat]: return message.sender_chat def add_usernames(self, username: SLT[str]) -> None: """ Add one or more sender chats to the allowed usernames. Args: username(:class:`telegram.utils.types.SLT[str]`, optional): Which sender chat username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. """ return super().add_usernames(username) def add_chat_ids(self, chat_id: SLT[int]) -> None: """ Add one or more sender chats to the allowed chat ids. Args: chat_id(:class:`telegram.utils.types.SLT[int]`, optional): Which sender chat ID(s) to allow through. """ return super().add_chat_ids(chat_id) def remove_usernames(self, username: SLT[str]) -> None: """ Remove one or more sender chats from allowed usernames. Args: username(:class:`telegram.utils.types.SLT[str]`, optional): Which sender chat username(s) to disallow through. Leading ``'@'`` s in usernames will be discarded. """ return super().remove_usernames(username) def remove_chat_ids(self, chat_id: SLT[int]) -> None: """ Remove one or more sender chats from allowed chat ids. Args: chat_id(:class:`telegram.utils.types.SLT[int]`, optional): Which sender chat ID(s) to disallow through. """ return super().remove_chat_ids(chat_id) class _SuperGroup(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: if message.sender_chat: return message.sender_chat.type == Chat.SUPERGROUP return False class _Channel(MessageFilter): __slots__ = () def filter(self, message: Message) -> bool: if message.sender_chat: return message.sender_chat.type == Chat.CHANNEL return False super_group = _SuperGroup() channel = _Channel() class _IsAutomaticForward(MessageFilter): __slots__ = () name = 'Filters.is_automatic_forward' def filter(self, message: Message) -> bool: return bool(message.is_automatic_forward) is_automatic_forward = _IsAutomaticForward() """Messages that contain :attr:`telegram.Message.is_automatic_forward`. .. versionadded:: 13.9 """ class _HasProtectedContent(MessageFilter): __slots__ = () name = 'Filters.has_protected_content' def filter(self, message: Message) -> bool: return bool(message.has_protected_content) has_protected_content = _HasProtectedContent() """Messages that contain :attr:`telegram.Message.has_protected_content`. .. versionadded:: 13.9 """ class _Invoice(MessageFilter): __slots__ = () name = 'Filters.invoice' def filter(self, message: Message) -> bool: return bool(message.invoice) invoice = _Invoice() """Messages that contain :class:`telegram.Invoice`.""" class _SuccessfulPayment(MessageFilter): __slots__ = () name = 'Filters.successful_payment' def filter(self, message: Message) -> bool: return bool(message.successful_payment) successful_payment = _SuccessfulPayment() """Messages that confirm a :class:`telegram.SuccessfulPayment`.""" class _PassportData(MessageFilter): __slots__ = () name = 'Filters.passport_data' def filter(self, message: Message) -> bool: return bool(message.passport_data) passport_data = _PassportData() """Messages that contain a :class:`telegram.PassportData`""" class _Poll(MessageFilter): __slots__ = () name = 'Filters.poll' def filter(self, message: Message) -> bool: return bool(message.poll) poll = _Poll() """Messages that contain a :class:`telegram.Poll`.""" class _Dice(_DiceEmoji): __slots__ = () dice = _DiceEmoji('🎲', 'dice') darts = _DiceEmoji('🎯', 'darts') basketball = _DiceEmoji('🏀', 'basketball') football = _DiceEmoji('⚽') slot_machine = _DiceEmoji('🎰') bowling = _DiceEmoji('🎳', 'bowling') 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. Examples: To allow any dice message, simply use ``MessageHandler(Filters.dice, 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``. Args: update (:class:`telegram.utils.types.SLT[int]`, optional): Which values to allow. If not specified, will allow any dice message. Attributes: dice: Dice messages with the emoji 🎲. Passing a list of integers is supported just as for :attr:`Filters.dice`. darts: Dice messages with the emoji 🎯. Passing a list of integers is supported just as for :attr:`Filters.dice`. basketball: Dice messages with the emoji 🏀. Passing a list of integers is supported just as for :attr:`Filters.dice`. football: Dice messages with the emoji ⚽. Passing a list of integers is supported just as for :attr:`Filters.dice`. slot_machine: Dice messages with the emoji 🎰. Passing a list of integers is supported just as for :attr:`Filters.dice`. bowling: Dice messages with the emoji 🎳. Passing a list of integers is supported just as for :attr:`Filters.dice`. .. versionadded:: 13.4 """ class language(MessageFilter): """Filters messages to only allow those which are from users with a certain language code. Note: According to official Telegram 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 (:class:`telegram.utils.types.SLT[str]`): Which language code(s) to allow through. This will be matched using ``.startswith`` meaning that 'en' will match both 'en_US' and 'en_GB'. """ __slots__ = ('lang',) def __init__(self, lang: SLT[str]): if isinstance(lang, str): lang = cast(str, lang) self.lang = [lang] else: lang = cast(List[str], lang) self.lang = lang self.name = f'Filters.language({self.lang})' def filter(self, message: Message) -> bool: """""" # remove method from docs return bool( message.from_user.language_code and any(message.from_user.language_code.startswith(x) for x in self.lang) ) class _Attachment(MessageFilter): __slots__ = () name = 'Filters.attachment' def filter(self, message: Message) -> bool: return bool(message.effective_attachment) attachment = _Attachment() """Messages that contain :meth:`telegram.Message.effective_attachment`. .. versionadded:: 13.6""" class _UpdateType(UpdateFilter): __slots__ = () name = 'Filters.update' class _Message(UpdateFilter): __slots__ = () name = 'Filters.update.message' def filter(self, update: Update) -> bool: return update.message is not None message = _Message() class _EditedMessage(UpdateFilter): __slots__ = () name = 'Filters.update.edited_message' def filter(self, update: Update) -> bool: return update.edited_message is not None edited_message = _EditedMessage() class _Messages(UpdateFilter): __slots__ = () name = 'Filters.update.messages' def filter(self, update: Update) -> bool: return update.message is not None or update.edited_message is not None messages = _Messages() class _ChannelPost(UpdateFilter): __slots__ = () name = 'Filters.update.channel_post' def filter(self, update: Update) -> bool: return update.channel_post is not None channel_post = _ChannelPost() class _EditedChannelPost(UpdateFilter): __slots__ = () name = 'Filters.update.edited_channel_post' def filter(self, update: Update) -> bool: return update.edited_channel_post is not None edited_channel_post = _EditedChannelPost() class _ChannelPosts(UpdateFilter): __slots__ = () name = 'Filters.update.channel_posts' def filter(self, update: Update) -> bool: return update.channel_post is not None or update.edited_channel_post is not None channel_posts = _ChannelPosts() def filter(self, update: Update) -> bool: return bool(self.messages(update) or self.channel_posts(update)) update = _UpdateType() """Subset for filtering the type of update. Examples: Use these filters like: ``Filters.update.message`` or ``Filters.update.channel_posts`` etc. Or use just ``Filters.update`` for all types. Attributes: message: Updates with :attr:`telegram.Update.message` edited_message: Updates with :attr:`telegram.Update.edited_message` messages: Updates with either :attr:`telegram.Update.message` or :attr:`telegram.Update.edited_message` channel_post: Updates with :attr:`telegram.Update.channel_post` edited_channel_post: Updates with :attr:`telegram.Update.edited_channel_post` channel_posts: Updates with either :attr:`telegram.Update.channel_post` or :attr:`telegram.Update.edited_channel_post` """ python-telegram-bot-13.11/telegram/ext/handler.py000066400000000000000000000257321417656324400220340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Dispatcher.""" from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar, Union, Generic from sys import version_info as py_ver from telegram.utils.deprecate import set_new_attribute_deprecated from telegram import Update from telegram.ext.utils.promise import Promise from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from telegram.ext.utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher RT = TypeVar('RT') UT = TypeVar('UT') class Handler(Generic[UT, CCT], ABC): """The base class for all update handlers. Create custom handlers by inheriting from it. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: callback (:obj:`callable`): The callback function for this handler. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ # Apparently Py 3.7 and below have '__dict__' in ABC if py_ver < (3, 7): __slots__ = ( 'callback', 'pass_update_queue', 'pass_job_queue', 'pass_user_data', 'pass_chat_data', 'run_async', ) else: __slots__ = ( 'callback', # type: ignore[assignment] 'pass_update_queue', 'pass_job_queue', 'pass_user_data', 'pass_chat_data', 'run_async', '__dict__', ) def __init__( self, callback: Callable[[UT, CCT], RT], pass_update_queue: bool = False, pass_job_queue: bool = False, pass_user_data: bool = False, pass_chat_data: bool = False, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): self.callback = callback self.pass_update_queue = pass_update_queue self.pass_job_queue = pass_job_queue self.pass_user_data = pass_user_data self.pass_chat_data = pass_chat_data self.run_async = run_async def __setattr__(self, key: str, value: object) -> None: # See comment on BaseFilter to know why this was done. if key.startswith('__'): key = f"_{self.__class__.__name__}{key}" if issubclass(self.__class__, Handler) and not self.__class__.__module__.startswith( 'telegram.ext.' ): object.__setattr__(self, key, value) return set_new_attribute_deprecated(self, key, value) @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 dispatcher. Therefore, an implementation of this method should always check the type of :attr:`update`. Args: update (:obj:`str` | :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. """ def handle_update( self, update: UT, dispatcher: 'Dispatcher', check_result: object, context: CCT = None, ) -> Union[RT, Promise]: """ 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. dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher. check_result (:obj:`obj`): The result from :attr:`check_update`. context (:class:`telegram.ext.CallbackContext`, optional): The context as provided by the dispatcher. """ run_async = self.run_async if ( self.run_async is DEFAULT_FALSE and dispatcher.bot.defaults and dispatcher.bot.defaults.run_async ): run_async = True if context: self.collect_additional_context(context, update, dispatcher, check_result) if run_async: return dispatcher.run_async(self.callback, update, context, update=update) return self.callback(update, context) optional_args = self.collect_optional_args(dispatcher, update, check_result) if run_async: return dispatcher.run_async( self.callback, dispatcher.bot, update, update=update, **optional_args ) return self.callback(dispatcher.bot, update, **optional_args) # type: ignore def collect_additional_context( self, context: CCT, update: UT, dispatcher: 'Dispatcher', 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. dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher. check_result: The result (return value) from :attr:`check_update`. """ def collect_optional_args( self, dispatcher: 'Dispatcher', update: UT = None, check_result: Any = None, # pylint: disable=W0613 ) -> Dict[str, object]: """ Prepares the optional arguments. If the handler has additional optional args, it should subclass this method, but remember to call this super method. DEPRECATED: This method is being replaced by new context based callbacks. Please see https://git.io/fxJuV for more info. Args: dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher. update (:class:`telegram.Update`): The update to gather chat/user id from. check_result: The result from check_update """ optional_args: Dict[str, object] = {} if self.pass_update_queue: optional_args['update_queue'] = dispatcher.update_queue if self.pass_job_queue: optional_args['job_queue'] = dispatcher.job_queue if self.pass_user_data and isinstance(update, Update): user = update.effective_user optional_args['user_data'] = dispatcher.user_data[ user.id if user else None # type: ignore[index] ] if self.pass_chat_data and isinstance(update, Update): chat = update.effective_chat optional_args['chat_data'] = dispatcher.chat_data[ chat.id if chat else None # type: ignore[index] ] return optional_args python-telegram-bot-13.11/telegram/ext/inlinequeryhandler.py000066400000000000000000000231021417656324400243060ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Callable, Dict, Match, Optional, Pattern, TypeVar, Union, cast, List, ) from telegram import Update from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler from .utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher RT = TypeVar('RT') class InlineQueryHandler(Handler[Update, CCT]): """ Handler class to handle Telegram inline queries. Optionally based on a regex. Read the documentation of the ``re`` module for more information. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: * When setting ``run_async`` to :obj:`True`, 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. Args: callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pattern (:obj:`str` | :obj:`Pattern`, optional): Regex pattern. If not :obj:`None`, ``re.match`` is used on :attr:`telegram.InlineQuery.query` to determine if an update should be handled by this handler. 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 pass_groups (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groups()`` as a keyword argument called ``groups``. Default is :obj:`False` DEPRECATED: Please switch to context based callbacks. pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``. Default is :obj:`False` DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: callback (:obj:`callable`): The callback function for this handler. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pattern (:obj:`str` | :obj:`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 pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the callback function. pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = ('pattern', 'chat_types', 'pass_groups', 'pass_groupdict') def __init__( self, callback: Callable[[Update, CCT], RT], pass_update_queue: bool = False, pass_job_queue: bool = False, pattern: Union[str, Pattern] = None, pass_groups: bool = False, pass_groupdict: bool = False, pass_user_data: bool = False, pass_chat_data: bool = False, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, chat_types: List[str] = None, ): super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, pass_chat_data=pass_chat_data, run_async=run_async, ) if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern = pattern self.chat_types = chat_types self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict def check_update(self, update: object) -> Optional[Union[bool, Match]]: """ Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ 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: if update.inline_query.query: match = re.match(self.pattern, update.inline_query.query) if match: return match else: return True return None def collect_optional_args( self, dispatcher: 'Dispatcher', update: Update = None, check_result: Optional[Union[bool, Match]] = None, ) -> Dict[str, object]: """Pass the results of ``re.match(pattern, query).{groups(), groupdict()}`` to the callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if needed. """ optional_args = super().collect_optional_args(dispatcher, update, check_result) if self.pattern: check_result = cast(Match, check_result) if self.pass_groups: optional_args['groups'] = check_result.groups() if self.pass_groupdict: optional_args['groupdict'] = check_result.groupdict() return optional_args def collect_additional_context( self, context: CCT, update: Update, dispatcher: 'Dispatcher', check_result: Optional[Union[bool, Match]], ) -> 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-13.11/telegram/ext/jobqueue.py000066400000000000000000000646301417656324400222360ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 datetime import logging from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union, cast, overload import pytz from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED, JobEvent from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.combining import OrTrigger from apscheduler.triggers.cron import CronTrigger from apscheduler.job import Job as APSJob from telegram.ext.callbackcontext import CallbackContext from telegram.utils.types import JSONDict from telegram.utils.deprecate import set_new_attribute_deprecated if TYPE_CHECKING: from telegram import Bot from telegram.ext import Dispatcher import apscheduler.job # noqa: F401 class JobQueue: """This class allows you to periodically perform tasks with the bot. It is a convenience wrapper for the APScheduler library. Attributes: scheduler (:class:`apscheduler.schedulers.background.BackgroundScheduler`): The APScheduler bot (:class:`telegram.Bot`): The bot instance that should be passed to the jobs. DEPRECATED: Use :attr:`set_dispatcher` instead. """ __slots__ = ('_dispatcher', 'logger', 'scheduler', '__dict__') def __init__(self) -> None: self._dispatcher: 'Dispatcher' = None # type: ignore[assignment] self.logger = logging.getLogger(self.__class__.__name__) self.scheduler = BackgroundScheduler(timezone=pytz.utc) self.scheduler.add_listener( self._update_persistence, mask=EVENT_JOB_EXECUTED | EVENT_JOB_ERROR ) # Dispatch errors and don't log them in the APS logger def aps_log_filter(record): # type: ignore return 'raised an exception' not in record.msg logging.getLogger('apscheduler.executors.default').addFilter(aps_log_filter) self.scheduler.add_listener(self._dispatch_error, EVENT_JOB_ERROR) def __setattr__(self, key: str, value: object) -> None: set_new_attribute_deprecated(self, key, value) def _build_args(self, job: 'Job') -> List[Union[CallbackContext, 'Bot', 'Job']]: if self._dispatcher.use_context: return [self._dispatcher.context_types.context.from_job(job, self._dispatcher)] return [self._dispatcher.bot, job] def _tz_now(self) -> datetime.datetime: return datetime.datetime.now(self.scheduler.timezone) def _update_persistence(self, _: JobEvent) -> None: self._dispatcher.update_persistence() def _dispatch_error(self, event: JobEvent) -> None: try: self._dispatcher.dispatch_error(None, event.exception) # Errors should not stop the thread. except Exception: self.logger.exception( 'An error was raised while processing the job and an ' 'uncaught error was raised while handling the error ' 'with an error_handler.' ) @overload def _parse_time_input(self, time: None, shift_day: bool = False) -> None: ... @overload def _parse_time_input( self, time: Union[float, int, datetime.timedelta, datetime.datetime, datetime.time], shift_day: bool = False, ) -> datetime.datetime: ... def _parse_time_input( self, time: Union[float, int, 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 # isinstance(time, datetime.datetime): return time def set_dispatcher(self, dispatcher: 'Dispatcher') -> None: """Set the dispatcher to be used by this JobQueue. Use this instead of passing a :class:`telegram.Bot` to the JobQueue, which is deprecated. Args: dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher. """ self._dispatcher = dispatcher if dispatcher.bot.defaults: self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc) def run_once( self, callback: Callable[['CallbackContext'], None], when: Union[float, datetime.timedelta, datetime.datetime, datetime.time], context: object = None, name: str = None, job_kwargs: JSONDict = None, ) -> 'Job': """Creates a new ``Job`` that runs once and adds it to the queue. Args: callback (:obj:`callable`): The callback function that should be executed by the new job. Callback signature for context based API: ``def callback(CallbackContext)`` ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access its ``job.context`` or change it to a repeating job. 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 (``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 (``time.tzinfo``) is :obj:`None`, the default timezone of the bot will be used. context (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``. job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the ``scheduler.add_job()``. Returns: :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job queue. """ if not job_kwargs: job_kwargs = {} name = name or callback.__name__ job = Job(callback, context, name, self) date_time = self._parse_time_input(when, shift_day=True) j = self.scheduler.add_job( callback, name=name, trigger='date', run_date=date_time, args=self._build_args(job), timezone=date_time.tzinfo or self.scheduler.timezone, **job_kwargs, ) job.job = j return job def run_repeating( self, callback: Callable[['CallbackContext'], None], interval: Union[float, datetime.timedelta], first: Union[float, datetime.timedelta, datetime.datetime, datetime.time] = None, last: Union[float, datetime.timedelta, datetime.datetime, datetime.time] = None, context: object = None, name: str = None, job_kwargs: JSONDict = None, ) -> 'Job': """Creates a new ``Job`` 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 (:obj:`callable`): The callback function that should be executed by the new job. Callback signature for context based API: ``def callback(CallbackContext)`` ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access its ``job.context`` or change it to a repeating job. 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 (``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 (``time.tzinfo``) is :obj:`None`, the default timezone of the bot will be used. Defaults to ``interval`` 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 ``first`` for details. If ``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. Defaults to :obj:`None`. context (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``. job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the ``scheduler.add_job()``. Returns: :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job queue. """ if not job_kwargs: job_kwargs = {} name = name or callback.__name__ job = Job(callback, context, name, self) 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( callback, trigger='interval', args=self._build_args(job), start_date=dt_first, end_date=dt_last, seconds=interval, name=name, **job_kwargs, ) job.job = j return job def run_monthly( self, callback: Callable[['CallbackContext'], None], when: datetime.time, day: int, context: object = None, name: str = None, day_is_strict: bool = True, job_kwargs: JSONDict = None, ) -> 'Job': """Creates a new ``Job`` that runs on a monthly basis and adds it to the queue. Args: callback (:obj:`callable`): The callback function that should be executed by the new job. Callback signature for context based API: ``def callback(CallbackContext)`` ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access its ``job.context`` or change it to a repeating job. 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. 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. context (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``. day_is_strict (:obj:`bool`, optional): If :obj:`False` and day > month.days, will pick the last day in the month. Defaults to :obj:`True`. job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the ``scheduler.add_job()``. Returns: :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job queue. """ if not job_kwargs: job_kwargs = {} name = name or callback.__name__ job = Job(callback, context, name, self) if day_is_strict: j = self.scheduler.add_job( callback, trigger='cron', args=self._build_args(job), name=name, day=day, hour=when.hour, minute=when.minute, second=when.second, timezone=when.tzinfo or self.scheduler.timezone, **job_kwargs, ) else: trigger = OrTrigger( [ CronTrigger( day=day, hour=when.hour, minute=when.minute, second=when.second, timezone=when.tzinfo, **job_kwargs, ), CronTrigger( day='last', hour=when.hour, minute=when.minute, second=when.second, timezone=when.tzinfo or self.scheduler.timezone, **job_kwargs, ), ] ) j = self.scheduler.add_job( callback, trigger=trigger, args=self._build_args(job), name=name, **job_kwargs ) job.job = j return job def run_daily( self, callback: Callable[['CallbackContext'], None], time: datetime.time, days: Tuple[int, ...] = tuple(range(7)), context: object = None, name: str = None, job_kwargs: JSONDict = None, ) -> 'Job': """Creates a new ``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 (:obj:`callable`): The callback function that should be executed by the new job. Callback signature for context based API: ``def callback(CallbackContext)`` ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access its ``job.context`` or change it to a repeating job. time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone (``time.tzinfo``) is :obj:`None`, the default timezone of the bot will be used. days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should run (where ``0-6`` correspond to monday - sunday). Defaults to ``EVERY_DAY`` context (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``. job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the ``scheduler.add_job()``. Returns: :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job queue. """ if not job_kwargs: job_kwargs = {} name = name or callback.__name__ job = Job(callback, context, name, self) j = self.scheduler.add_job( callback, name=name, args=self._build_args(job), trigger='cron', day_of_week=','.join([str(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 return job def run_custom( self, callback: Callable[['CallbackContext'], None], job_kwargs: JSONDict, context: object = None, name: str = None, ) -> 'Job': """Creates a new customly defined ``Job``. Args: callback (:obj:`callable`): The callback function that should be executed by the new job. Callback signature for context based API: ``def callback(CallbackContext)`` ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access its ``job.context`` or change it to a repeating job. job_kwargs (:obj:`dict`): Arbitrary keyword arguments. Used as arguments for ``scheduler.add_job``. context (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through ``job.context`` in the callback. Defaults to ``None``. name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``. Returns: :class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job queue. """ name = name or callback.__name__ job = Job(callback, context, name, self) j = self.scheduler.add_job(callback, args=self._build_args(job), name=name, **job_kwargs) job.job = j return job def start(self) -> None: """Starts the job_queue thread.""" if not self.scheduler.running: self.scheduler.start() def stop(self) -> None: """Stops the thread.""" if self.scheduler.running: self.scheduler.shutdown() def jobs(self) -> Tuple['Job', ...]: """Returns a tuple of all *scheduled* jobs that are currently in the ``JobQueue``.""" return tuple( Job._from_aps_job(job, self) # pylint: disable=W0212 for job in self.scheduler.get_jobs() ) def get_jobs_by_name(self, name: str) -> Tuple['Job', ...]: """Returns a tuple of all *pending/scheduled* jobs with the given name that are currently in the ``JobQueue``. """ return tuple(job for job in self.jobs() if job.name == name) class Job: """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 :attr:`id` is equal. Note: * All attributes and instance methods of :attr:`job` are also directly available as attributes/methods of the corresponding :class:`telegram.ext.Job` object. * Two instances of :class:`telegram.ext.Job` are considered equal, if their corresponding ``job`` attributes have the same ``id``. * If :attr:`job` isn't passed on initialization, it must be set manually afterwards for this :class:`telegram.ext.Job` to be useful. Args: callback (:obj:`callable`): The callback function that should be executed by the new job. Callback signature for context based API: ``def callback(CallbackContext)`` a ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access its ``job.context`` or change it to a repeating job. context (:obj:`object`, optional): Additional data needed for the callback function. Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`. name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``. job_queue (:class:`telegram.ext.JobQueue`, optional): The ``JobQueue`` this job belongs to. Only optional for backward compatibility with ``JobQueue.put()``. job (:class:`apscheduler.job.Job`, optional): The APS Job this job is a wrapper for. Attributes: callback (:obj:`callable`): The callback function that should be executed by the new job. context (:obj:`object`): Optional. Additional data needed for the callback function. name (:obj:`str`): Optional. The name of the new job. job_queue (:class:`telegram.ext.JobQueue`): Optional. The ``JobQueue`` this job belongs to. job (:class:`apscheduler.job.Job`): Optional. The APS Job this job is a wrapper for. """ __slots__ = ( 'callback', 'context', 'name', 'job_queue', '_removed', '_enabled', 'job', '__dict__', ) def __init__( self, callback: Callable[['CallbackContext'], None], context: object = None, name: str = None, job_queue: JobQueue = None, job: APSJob = None, ): self.callback = callback self.context = context self.name = name or callback.__name__ self.job_queue = job_queue self._removed = False self._enabled = False self.job = cast(APSJob, job) # skipcq: PTC-W0052 def __setattr__(self, key: str, value: object) -> None: set_new_attribute_deprecated(self, key, value) def run(self, dispatcher: 'Dispatcher') -> None: """Executes the callback function independently of the jobs schedule.""" try: if dispatcher.use_context: self.callback(dispatcher.context_types.context.from_job(self, dispatcher)) else: self.callback(dispatcher.bot, self) # type: ignore[arg-type,call-arg] except Exception as exc: try: dispatcher.dispatch_error(None, exc) # Errors should not stop the thread. except Exception: dispatcher.logger.exception( 'An error was raised while processing the job and an ' 'uncaught error was raised while handling the error ' 'with an error_handler.' ) def schedule_removal(self) -> None: """ Schedules this job for removal from the ``JobQueue``. It will be removed without executing its callback function again. """ self.job.remove() self._removed = True @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]: """ :obj:`datetime.datetime`: Datetime for the next job execution. Datetime is localized according to :attr:`tzinfo`. If job is removed or already ran it equals to :obj:`None`. """ return self.job.next_run_time @classmethod def _from_aps_job(cls, job: APSJob, job_queue: JobQueue) -> 'Job': # context based callbacks if len(job.args) == 1: context = job.args[0].job.context else: context = job.args[1].context return cls(job.func, context=context, name=job.name, job_queue=job_queue, job=job) def __getattr__(self, item: str) -> object: return getattr(self.job, item) def __lt__(self, other: object) -> bool: return False def __eq__(self, other: object) -> bool: if isinstance(other, self.__class__): return self.id == other.id return False python-telegram-bot-13.11/telegram/ext/messagehandler.py000066400000000000000000000227561417656324400234040ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. # TODO: Remove allow_edited """This module contains the MessageHandler class.""" import warnings from typing import TYPE_CHECKING, Callable, Dict, Optional, TypeVar, Union from telegram import Update from telegram.ext import BaseFilter, Filters from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler from .utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher RT = TypeVar('RT') class MessageHandler(Handler[Update, CCT]): """Handler class to handle telegram messages. They might contain text, media or status updates. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from :class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in :class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise operators (& for and, | for or, ~ for not). Default is :attr:`telegram.ext.filters.Filters.update`. This defaults to all message_type updates being: ``message``, ``edited_message``, ``channel_post`` and ``edited_channel_post``. If you don't want or need any of those pass ``~Filters.update.*`` in the filter argument. callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. message_updates (:obj:`bool`, optional): Should "normal" message updates be handled? Default is :obj:`None`. DEPRECATED: Please switch to filters for update filtering. channel_post_updates (:obj:`bool`, optional): Should channel posts updates be handled? Default is :obj:`None`. DEPRECATED: Please switch to filters for update filtering. edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default is :obj:`None`. DEPRECATED: Please switch to filters for update filtering. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Raises: ValueError Attributes: filters (:obj:`Filter`): Only allow updates with these Filters. See :mod:`telegram.ext.filters` for a full list of all available filters. callback (:obj:`callable`): The callback function for this handler. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. message_updates (:obj:`bool`): Should "normal" message updates be handled? Default is :obj:`None`. channel_post_updates (:obj:`bool`): Should channel posts updates be handled? Default is :obj:`None`. edited_updates (:obj:`bool`): Should "edited" message updates be handled? Default is :obj:`None`. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = ('filters',) def __init__( self, filters: BaseFilter, callback: Callable[[Update, CCT], RT], pass_update_queue: bool = False, pass_job_queue: bool = False, pass_user_data: bool = False, pass_chat_data: bool = False, message_updates: bool = None, channel_post_updates: bool = None, edited_updates: bool = None, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, pass_chat_data=pass_chat_data, run_async=run_async, ) if message_updates is False and channel_post_updates is False and edited_updates is False: raise ValueError( 'message_updates, channel_post_updates and edited_updates are all False' ) if filters is not None: self.filters = Filters.update & filters else: self.filters = Filters.update if message_updates is not None: warnings.warn( 'message_updates is deprecated. See https://git.io/fxJuV for more info', TelegramDeprecationWarning, stacklevel=2, ) if message_updates is False: self.filters &= ~Filters.update.message if channel_post_updates is not None: warnings.warn( 'channel_post_updates is deprecated. See https://git.io/fxJuV ' 'for more info', TelegramDeprecationWarning, stacklevel=2, ) if channel_post_updates is False: self.filters &= ~Filters.update.channel_post if edited_updates is not None: warnings.warn( 'edited_updates is deprecated. See https://git.io/fxJuV for more info', TelegramDeprecationWarning, stacklevel=2, ) if edited_updates is False: self.filters &= ~( Filters.update.edited_message | Filters.update.edited_channel_post ) def check_update(self, update: object) -> Optional[Union[bool, Dict[str, list]]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ if isinstance(update, Update) and update.effective_message: return self.filters(update) return None def collect_additional_context( self, context: CCT, update: Update, dispatcher: 'Dispatcher', 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-13.11/telegram/ext/messagequeue.py000066400000000000000000000346651417656324400231150ustar00rootroot00000000000000#!/usr/bin/env python # # Module author: # Tymofii A. Khodniev (thodnev) # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 throughput-limiting message processor for Telegram bots.""" import functools import queue as q import threading import time import warnings from typing import TYPE_CHECKING, Callable, List, NoReturn from telegram.ext.utils.promise import Promise from telegram.utils.deprecate import TelegramDeprecationWarning if TYPE_CHECKING: from telegram import Bot # We need to count < 1s intervals, so the most accurate timer is needed curtime = time.perf_counter class DelayQueueError(RuntimeError): """Indicates processing errors.""" __slots__ = () class DelayQueue(threading.Thread): """ Processes callbacks from queue with specified throughput limits. Creates a separate thread to process callbacks with delays. .. deprecated:: 13.3 :class:`telegram.ext.DelayQueue` in its current form is deprecated and will be reinvented in a future release. See `this thread `_ for a list of known bugs. Args: queue (:obj:`Queue`, optional): Used to pass callbacks to thread. Creates ``Queue`` implicitly if not provided. burst_limit (:obj:`int`, optional): Number of maximum callbacks to process per time-window defined by :attr:`time_limit_ms`. Defaults to 30. time_limit_ms (:obj:`int`, optional): Defines width of time-window used when each processing limit is calculated. Defaults to 1000. exc_route (:obj:`callable`, optional): A callable, accepting 1 positional argument; used to route exceptions from processor thread to main thread; is called on `Exception` subclass exceptions. If not provided, exceptions are routed through dummy handler, which re-raises them. autostart (:obj:`bool`, optional): If :obj:`True`, processor is started immediately after object's creation; if :obj:`False`, should be started manually by `start` method. Defaults to :obj:`True`. name (:obj:`str`, optional): Thread's name. Defaults to ``'DelayQueue-N'``, where N is sequential number of object created. Attributes: burst_limit (:obj:`int`): Number of maximum callbacks to process per time-window. time_limit (:obj:`int`): Defines width of time-window used when each processing limit is calculated. exc_route (:obj:`callable`): A callable, accepting 1 positional argument; used to route exceptions from processor thread to main thread; name (:obj:`str`): Thread's name. """ _instcnt = 0 # instance counter def __init__( self, queue: q.Queue = None, burst_limit: int = 30, time_limit_ms: int = 1000, exc_route: Callable[[Exception], None] = None, autostart: bool = True, name: str = None, ): warnings.warn( 'DelayQueue in its current form is deprecated and will be reinvented in a future ' 'release. See https://git.io/JtDbF for a list of known bugs.', category=TelegramDeprecationWarning, ) self._queue = queue if queue is not None else q.Queue() self.burst_limit = burst_limit self.time_limit = time_limit_ms / 1000 self.exc_route = exc_route if exc_route is not None else self._default_exception_handler self.__exit_req = False # flag to gently exit thread self.__class__._instcnt += 1 if name is None: name = f'{self.__class__.__name__}-{self.__class__._instcnt}' super().__init__(name=name) self.daemon = False if autostart: # immediately start processing super().start() def run(self) -> None: """ Do not use the method except for unthreaded testing purposes, the method normally is automatically called by autostart argument. """ times: List[float] = [] # used to store each callable processing time while True: item = self._queue.get() if self.__exit_req: return # shutdown thread # delay routine now = time.perf_counter() t_delta = now - self.time_limit # calculate early to improve perf. if times and t_delta > times[-1]: # if last call was before the limit time-window # used to impr. perf. in long-interval calls case times = [now] else: # collect last in current limit time-window times = [t for t in times if t >= t_delta] times.append(now) if len(times) >= self.burst_limit: # if throughput limit was hit time.sleep(times[1] - t_delta) # finally process one try: func, args, kwargs = item func(*args, **kwargs) except Exception as exc: # re-route any exceptions self.exc_route(exc) # to prevent thread exit def stop(self, timeout: float = None) -> None: """Used to gently stop processor and shutdown its thread. Args: timeout (:obj:`float`): Indicates maximum time to wait for processor to stop and its thread to exit. If timeout exceeds and processor has not stopped, method silently returns. :attr:`is_alive` could be used afterwards to check the actual status. ``timeout`` set to :obj:`None`, blocks until processor is shut down. Defaults to :obj:`None`. """ self.__exit_req = True # gently request self._queue.put(None) # put something to unfreeze if frozen super().join(timeout=timeout) @staticmethod def _default_exception_handler(exc: Exception) -> NoReturn: """ Dummy exception handler which re-raises exception in thread. Could be possibly overwritten by subclasses. """ raise exc def __call__(self, func: Callable, *args: object, **kwargs: object) -> None: """Used to process callbacks in throughput-limiting thread through queue. Args: func (:obj:`callable`): The actual function (or any callable) that is processed through queue. *args (:obj:`list`): Variable-length `func` arguments. **kwargs (:obj:`dict`): Arbitrary keyword-arguments to `func`. """ if not self.is_alive() or self.__exit_req: raise DelayQueueError('Could not process callback in stopped thread') self._queue.put((func, args, kwargs)) # The most straightforward way to implement this is to use 2 sequential delay # queues, like on classic delay chain schematics in electronics. # So, message path is: # msg --> group delay if group msg, else no delay --> normal msg delay --> out # This way OS threading scheduler cares of timings accuracy. # (see time.time, time.clock, time.perf_counter, time.sleep @ docs.python.org) class MessageQueue: """ Implements callback processing with proper delays to avoid hitting Telegram's message limits. Contains two ``DelayQueue``, for group and for all messages, interconnected in delay chain. Callables are processed through *group* ``DelayQueue``, then through *all* ``DelayQueue`` for group-type messages. For non-group messages, only the *all* ``DelayQueue`` is used. .. deprecated:: 13.3 :class:`telegram.ext.MessageQueue` in its current form is deprecated and will be reinvented in a future release. See `this thread `_ for a list of known bugs. Args: all_burst_limit (:obj:`int`, optional): Number of maximum *all-type* callbacks to process per time-window defined by :attr:`all_time_limit_ms`. Defaults to 30. all_time_limit_ms (:obj:`int`, optional): Defines width of *all-type* time-window used when each processing limit is calculated. Defaults to 1000 ms. group_burst_limit (:obj:`int`, optional): Number of maximum *group-type* callbacks to process per time-window defined by :attr:`group_time_limit_ms`. Defaults to 20. group_time_limit_ms (:obj:`int`, optional): Defines width of *group-type* time-window used when each processing limit is calculated. Defaults to 60000 ms. exc_route (:obj:`callable`, optional): A callable, accepting one positional argument; used to route exceptions from processor threads to main thread; is called on ``Exception`` subclass exceptions. If not provided, exceptions are routed through dummy handler, which re-raises them. autostart (:obj:`bool`, optional): If :obj:`True`, processors are started immediately after object's creation; if :obj:`False`, should be started manually by :attr:`start` method. Defaults to :obj:`True`. """ def __init__( self, all_burst_limit: int = 30, all_time_limit_ms: int = 1000, group_burst_limit: int = 20, group_time_limit_ms: int = 60000, exc_route: Callable[[Exception], None] = None, autostart: bool = True, ): warnings.warn( 'MessageQueue in its current form is deprecated and will be reinvented in a future ' 'release. See https://git.io/JtDbF for a list of known bugs.', category=TelegramDeprecationWarning, ) # create according delay queues, use composition self._all_delayq = DelayQueue( burst_limit=all_burst_limit, time_limit_ms=all_time_limit_ms, exc_route=exc_route, autostart=autostart, ) self._group_delayq = DelayQueue( burst_limit=group_burst_limit, time_limit_ms=group_time_limit_ms, exc_route=exc_route, autostart=autostart, ) def start(self) -> None: """Method is used to manually start the ``MessageQueue`` processing.""" self._all_delayq.start() self._group_delayq.start() def stop(self, timeout: float = None) -> None: """Stops the ``MessageQueue``.""" self._group_delayq.stop(timeout=timeout) self._all_delayq.stop(timeout=timeout) stop.__doc__ = DelayQueue.stop.__doc__ or '' # reuse docstring if any def __call__(self, promise: Callable, is_group_msg: bool = False) -> Callable: """ Processes callables in throughput-limiting queues to avoid hitting limits (specified with :attr:`burst_limit` and :attr:`time_limit`. Args: promise (:obj:`callable`): Mainly the ``telegram.utils.promise.Promise`` (see Notes for other callables), that is processed in delay queues. is_group_msg (:obj:`bool`, optional): Defines whether ``promise`` would be processed in group*+*all* ``DelayQueue``s (if set to :obj:`True`), or only through *all* ``DelayQueue`` (if set to :obj:`False`), resulting in needed delays to avoid hitting specified limits. Defaults to :obj:`False`. Note: Method is designed to accept ``telegram.utils.promise.Promise`` as ``promise`` argument, but other callables could be used too. For example, lambdas or simple functions could be used to wrap original func to be called with needed args. In that case, be sure that either wrapper func does not raise outside exceptions or the proper :attr:`exc_route` handler is provided. Returns: :obj:`callable`: Used as ``promise`` argument. """ if not is_group_msg: # ignore middle group delay self._all_delayq(promise) else: # use middle group delay self._group_delayq(self._all_delayq, promise) return promise def queuedmessage(method: Callable) -> Callable: """A decorator to be used with :attr:`telegram.Bot` send* methods. Note: As it probably wouldn't be a good idea to make this decorator a property, it has been coded as decorator function, so it implies that first positional argument to wrapped MUST be self. The next object attributes are used by decorator: Attributes: self._is_messages_queued_default (:obj:`bool`): Value to provide class-defaults to ``queued`` kwarg if not provided during wrapped method call. self._msg_queue (:class:`telegram.ext.messagequeue.MessageQueue`): The actual ``MessageQueue`` used to delay outbound messages according to specified time-limits. Wrapped method starts accepting the next kwargs: Args: queued (:obj:`bool`, optional): If set to :obj:`True`, the ``MessageQueue`` is used to process output messages. Defaults to `self._is_queued_out`. isgroup (:obj:`bool`, optional): If set to :obj:`True`, the message is meant to be group-type(as there's no obvious way to determine its type in other way at the moment). Group-type messages could have additional processing delay according to limits set in `self._out_queue`. Defaults to :obj:`False`. Returns: ``telegram.utils.promise.Promise``: In case call is queued or original method's return value if it's not. """ @functools.wraps(method) def wrapped(self: 'Bot', *args: object, **kwargs: object) -> object: # pylint: disable=W0212 queued = kwargs.pop( 'queued', self._is_messages_queued_default # type: ignore[attr-defined] ) isgroup = kwargs.pop('isgroup', False) if queued: prom = Promise(method, (self,) + args, kwargs) return self._msg_queue(prom, isgroup) # type: ignore[attr-defined] return method(self, *args, **kwargs) return wrapped python-telegram-bot-13.11/telegram/ext/picklepersistence.py000066400000000000000000000442701417656324400241310ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 collections import defaultdict from typing import ( Any, Dict, Optional, Tuple, overload, cast, DefaultDict, ) from telegram.ext import BasePersistence from .utils.types import UD, CD, BD, ConversationDict, CDCData from .contexttypes import ContextTypes class PicklePersistence(BasePersistence[UD, CD, BD]): """Using python's builtin pickle for making your bot persistent. Warning: :class:`PicklePersistence` will try to replace :class:`telegram.Bot` instances by :attr:`REPLACED_BOT` and insert the bot set with :meth:`telegram.ext.BasePersistence.set_bot` upon loading of the data. This is to ensure that changes to the bot apply to the saved objects, too. If you change the bots token, this may lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see :meth:`telegram.ext.BasePersistence.replace_bot` and :meth:`telegram.ext.BasePersistence.insert_bot`. Args: filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` is :obj:`False` this will be used as a prefix. store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this persistence class. Default is :obj:`True`. store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this persistence class. Default is :obj:`True`. store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this persistence class. Default is :obj:`True`. store_callback_data (:obj:`bool`, optional): Whether callback_data should be saved by this persistence class. Default is :obj:`False`. .. versionadded:: 13.6 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 Attributes: filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` is :obj:`False` this will be used as a prefix. store_user_data (:obj:`bool`): Optional. Whether user_data should be saved by this persistence class. store_chat_data (:obj:`bool`): Optional. Whether chat_data should be saved by this persistence class. store_bot_data (:obj:`bool`): Optional. Whether bot_data should be saved by this persistence class. store_callback_data (:obj:`bool`): Optional. Whether callback_data be saved by this persistence class. .. versionadded:: 13.6 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__ = ( 'filename', 'single_file', 'on_flush', 'user_data', 'chat_data', 'bot_data', 'callback_data', 'conversations', 'context_types', ) @overload def __init__( self: 'PicklePersistence[Dict, Dict, Dict]', filename: str, store_user_data: bool = True, store_chat_data: bool = True, store_bot_data: bool = True, single_file: bool = True, on_flush: bool = False, store_callback_data: bool = False, ): ... @overload def __init__( self: 'PicklePersistence[UD, CD, BD]', filename: str, store_user_data: bool = True, store_chat_data: bool = True, store_bot_data: bool = True, single_file: bool = True, on_flush: bool = False, store_callback_data: bool = False, context_types: ContextTypes[Any, UD, CD, BD] = None, ): ... def __init__( self, filename: str, store_user_data: bool = True, store_chat_data: bool = True, store_bot_data: bool = True, single_file: bool = True, on_flush: bool = False, store_callback_data: bool = False, context_types: ContextTypes[Any, UD, CD, BD] = None, ): super().__init__( store_user_data=store_user_data, store_chat_data=store_chat_data, store_bot_data=store_bot_data, store_callback_data=store_callback_data, ) self.filename = filename self.single_file = single_file self.on_flush = on_flush self.user_data: Optional[DefaultDict[int, UD]] = None self.chat_data: Optional[DefaultDict[int, CD]] = None self.bot_data: Optional[BD] = None self.callback_data: Optional[CDCData] = None self.conversations: Optional[Dict[str, Dict[Tuple, object]]] = None self.context_types = cast(ContextTypes[Any, UD, CD, BD], context_types or ContextTypes()) def _load_singlefile(self) -> None: try: filename = self.filename with open(self.filename, "rb") as file: data = pickle.load(file) self.user_data = defaultdict(self.context_types.user_data, data['user_data']) self.chat_data = defaultdict(self.context_types.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 = defaultdict(self.context_types.user_data) self.chat_data = defaultdict(self.context_types.chat_data) self.bot_data = self.context_types.bot_data() self.callback_data = None except pickle.UnpicklingError as exc: raise TypeError(f"File {filename} does not contain valid pickle data") from exc except Exception as exc: raise TypeError(f"Something went wrong unpickling {filename}") from exc @staticmethod def _load_file(filename: str) -> Any: try: with open(filename, "rb") as file: return pickle.load(file) except OSError: return None except pickle.UnpicklingError as exc: raise TypeError(f"File {filename} does not contain valid pickle data") from exc except Exception as exc: raise TypeError(f"Something went wrong unpickling {filename}") from exc def _dump_singlefile(self) -> None: with open(self.filename, "wb") as file: data = { 'conversations': self.conversations, 'user_data': self.user_data, 'chat_data': self.chat_data, 'bot_data': self.bot_data, 'callback_data': self.callback_data, } pickle.dump(data, file) @staticmethod def _dump_file(filename: str, data: object) -> None: with open(filename, "wb") as file: pickle.dump(data, file) def get_user_data(self) -> DefaultDict[int, UD]: """Returns the user_data from the pickle file if it exists or an empty :obj:`defaultdict`. Returns: DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.UD`]: The restored user data. """ if self.user_data: pass elif not self.single_file: filename = f"{self.filename}_user_data" data = self._load_file(filename) if not data: data = defaultdict(self.context_types.user_data) else: data = defaultdict(self.context_types.user_data, data) self.user_data = data else: self._load_singlefile() return self.user_data # type: ignore[return-value] def get_chat_data(self) -> DefaultDict[int, CD]: """Returns the chat_data from the pickle file if it exists or an empty :obj:`defaultdict`. Returns: DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.CD`]: The restored chat data. """ if self.chat_data: pass elif not self.single_file: filename = f"{self.filename}_chat_data" data = self._load_file(filename) if not data: data = defaultdict(self.context_types.chat_data) else: data = defaultdict(self.context_types.chat_data, data) self.chat_data = data else: self._load_singlefile() return self.chat_data # type: ignore[return-value] def get_bot_data(self) -> BD: """Returns the bot_data from the pickle file if it exists or an empty object of type :class:`telegram.ext.utils.types.BD`. Returns: :class:`telegram.ext.utils.types.BD`: The restored bot data. """ if self.bot_data: pass elif not self.single_file: filename = f"{self.filename}_bot_data" data = self._load_file(filename) if not data: data = self.context_types.bot_data() self.bot_data = data else: self._load_singlefile() return self.bot_data # type: ignore[return-value] 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: Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or :obj:`None`, if no data was stored. """ if self.callback_data: pass elif not self.single_file: filename = f"{self.filename}_callback_data" data = self._load_file(filename) if not data: data = None self.callback_data = data else: self._load_singlefile() if self.callback_data is None: return None return self.callback_data[0], self.callback_data[1].copy() 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: filename = f"{self.filename}_conversations" data = self._load_file(filename) if not data: data = {name: {}} self.conversations = data else: self._load_singlefile() return self.conversations.get(name, {}).copy() # type: ignore[union-attr] def update_conversation( self, name: str, key: Tuple[int, ...], 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 (:obj:`tuple` | :obj:`any`): 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: filename = f"{self.filename}_conversations" self._dump_file(filename, self.conversations) else: self._dump_singlefile() 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 (:class:`telegram.ext.utils.types.UD`): The :attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``. """ if self.user_data is None: self.user_data = defaultdict(self.context_types.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: filename = f"{self.filename}_user_data" self._dump_file(filename, self.user_data) else: self._dump_singlefile() 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 (:class:`telegram.ext.utils.types.CD`): The :attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``. """ if self.chat_data is None: self.chat_data = defaultdict(self.context_types.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: filename = f"{self.filename}_chat_data" self._dump_file(filename, self.chat_data) else: self._dump_singlefile() 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 (:class:`telegram.ext.utils.types.BD`): The :attr:`telegram.ext.Dispatcher.bot_data`. """ if self.bot_data == data: return self.bot_data = data if not self.on_flush: if not self.single_file: filename = f"{self.filename}_bot_data" self._dump_file(filename, self.bot_data) else: self._dump_singlefile() 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 (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore :class:`telegram.ext.CallbackDataCache`. """ if self.callback_data == data: return self.callback_data = (data[0], data[1].copy()) if not self.on_flush: if not self.single_file: filename = f"{self.filename}_callback_data" self._dump_file(filename, self.callback_data) else: self._dump_singlefile() 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` """ 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` """ def refresh_bot_data(self, bot_data: BD) -> None: """Does nothing. .. versionadded:: 13.6 .. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data` """ 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(f"{self.filename}_user_data", self.user_data) if self.chat_data: self._dump_file(f"{self.filename}_chat_data", self.chat_data) if self.bot_data: self._dump_file(f"{self.filename}_bot_data", self.bot_data) if self.callback_data: self._dump_file(f"{self.filename}_callback_data", self.callback_data) if self.conversations: self._dump_file(f"{self.filename}_conversations", self.conversations) python-telegram-bot-13.11/telegram/ext/pollanswerhandler.py000066400000000000000000000113421417656324400241330ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 .handler import Handler from .utils.types import CCT class PollAnswerHandler(Handler[Update, CCT]): """Handler class to handle Telegram updates that contain a poll answer. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: callback (:obj:`callable`): The callback function for this handler. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = () def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :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-13.11/telegram/ext/pollhandler.py000066400000000000000000000113101417656324400227060ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 classes.""" from telegram import Update from .handler import Handler from .utils.types import CCT class PollHandler(Handler[Update, CCT]): """Handler class to handle Telegram updates that contain a poll. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: callback (:obj:`callable`): The callback function for this handler. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = () def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :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-13.11/telegram/ext/precheckoutqueryhandler.py000066400000000000000000000113551417656324400253530ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" from telegram import Update from .handler import Handler from .utils.types import CCT class PreCheckoutQueryHandler(Handler[Update, CCT]): """Handler class to handle Telegram PreCheckout callback queries. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` DEPRECATED: Please switch to context based callbacks. instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: callback (:obj:`callable`): The callback function for this handler. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = () def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update` | :obj:`object`): Incoming update. Returns: :obj:`bool` """ return isinstance(update, Update) and bool(update.pre_checkout_query) python-telegram-bot-13.11/telegram/ext/regexhandler.py000066400000000000000000000172171417656324400230660ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. # TODO: Remove allow_edited """This module contains the RegexHandler class.""" import warnings from typing import TYPE_CHECKING, Callable, Dict, Optional, Pattern, TypeVar, Union, Any from telegram import Update from telegram.ext import Filters, MessageHandler from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from telegram.ext.utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher RT = TypeVar('RT') class RegexHandler(MessageHandler): """Handler class to handle Telegram updates based on a regex. It uses a regular expression to check text messages. Read the documentation of the ``re`` module for more information. The ``re.match`` function is used to determine if an update should be handled by this handler. Note: This handler is being deprecated. For the same use case use: ``MessageHandler(Filters.regex(r'pattern'), callback)`` Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_groups (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groups()`` as a keyword argument called ``groups``. Default is :obj:`False` pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``. Default is :obj:`False` pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. message_updates (:obj:`bool`, optional): Should "normal" message updates be handled? Default is :obj:`True`. channel_post_updates (:obj:`bool`, optional): Should channel posts updates be handled? Default is :obj:`True`. edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default is :obj:`False`. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Raises: ValueError Attributes: pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. callback (:obj:`callable`): The callback function for this handler. pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the callback function. pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to the callback function. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = ('pass_groups', 'pass_groupdict') def __init__( self, pattern: Union[str, Pattern], callback: Callable[[Update, CCT], RT], pass_groups: bool = False, pass_groupdict: bool = False, pass_update_queue: bool = False, pass_job_queue: bool = False, pass_user_data: bool = False, pass_chat_data: bool = False, allow_edited: bool = False, # pylint: disable=W0613 message_updates: bool = True, channel_post_updates: bool = False, edited_updates: bool = False, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): warnings.warn( 'RegexHandler is deprecated. See https://git.io/fxJuV for more info', TelegramDeprecationWarning, stacklevel=2, ) super().__init__( Filters.regex(pattern), callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, pass_chat_data=pass_chat_data, message_updates=message_updates, channel_post_updates=channel_post_updates, edited_updates=edited_updates, run_async=run_async, ) self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict def collect_optional_args( self, dispatcher: 'Dispatcher', update: Update = None, check_result: Optional[Union[bool, Dict[str, Any]]] = None, ) -> Dict[str, object]: """Pass the results of ``re.match(pattern, text).{groups(), groupdict()}`` to the callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if needed. """ optional_args = super().collect_optional_args(dispatcher, update, check_result) if isinstance(check_result, dict): if self.pass_groups: optional_args['groups'] = check_result['matches'][0].groups() if self.pass_groupdict: optional_args['groupdict'] = check_result['matches'][0].groupdict() return optional_args python-telegram-bot-13.11/telegram/ext/shippingqueryhandler.py000066400000000000000000000113371417656324400246600ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 .handler import Handler from .utils.types import CCT class ShippingQueryHandler(Handler[Update, CCT]): """Handler class to handle Telegram shipping callback queries. Note: :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you can use to keep any data in will be sent to the :attr:`callback` function. Related to either the user or the chat that the update was sent in. For each update from the same user or in the same chat, it will be the same ``dict``. Note that this is DEPRECATED, and you should use context based callbacks. See https://git.io/fxJuV for more info. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``user_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``chat_data`` will be passed to the callback function. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: callback (:obj:`callable`): The callback function for this handler. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to the callback function. pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = () def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :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-13.11/telegram/ext/stringcommandhandler.py000066400000000000000000000144551417656324400246220ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Callable, Dict, List, Optional, TypeVar, Union from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler from .utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher RT = TypeVar('RT') class StringCommandHandler(Handler[str, CCT]): """Handler class to handle string commands. Commands are string updates that start with ``/``. The handler will add a ``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 :attr:`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 ``run_async`` to :obj:`True`, 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 (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the arguments passed to the command as a keyword argument called ``args``. It will contain a list of strings, which is the text following the command split on single or consecutive whitespace characters. Default is :obj:`False` DEPRECATED: Please switch to context based callbacks. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: command (:obj:`str`): The command this handler should listen for. callback (:obj:`callable`): The callback function for this handler. pass_args (:obj:`bool`): Determines whether the handler should be passed ``args``. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = ('command', 'pass_args') def __init__( self, command: str, callback: Callable[[str, CCT], RT], pass_args: bool = False, pass_update_queue: bool = False, pass_job_queue: bool = False, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, run_async=run_async, ) self.command = command self.pass_args = pass_args def check_update(self, update: object) -> Optional[List[str]]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:obj:`object`): The incoming update. Returns: :obj:`bool` """ if isinstance(update, str) and update.startswith('/'): args = update[1:].split(' ') if args[0] == self.command: return args[1:] return None def collect_optional_args( self, dispatcher: 'Dispatcher', update: str = None, check_result: Optional[List[str]] = None, ) -> Dict[str, object]: """Provide text after the command to the callback the ``args`` argument as list, split on single whitespaces. """ optional_args = super().collect_optional_args(dispatcher, update, check_result) if self.pass_args: optional_args['args'] = check_result return optional_args def collect_additional_context( self, context: CCT, update: str, dispatcher: 'Dispatcher', 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-13.11/telegram/ext/stringregexhandler.py000066400000000000000000000157741417656324400243230ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Callable, Dict, Match, Optional, Pattern, TypeVar, Union from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler from .utils.types import CCT if TYPE_CHECKING: from telegram.ext import Dispatcher RT = TypeVar('RT') class StringRegexHandler(Handler[str, CCT]): """Handler class to handle string updates based on a regex which checks the update content. Read the documentation of the ``re`` module for more information. The ``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 :attr:`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 ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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`. pass_groups (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groups()`` as a keyword argument called ``groups``. Default is :obj:`False` DEPRECATED: Please switch to context based callbacks. pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of ``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``. Default is :obj:`False` DEPRECATED: Please switch to context based callbacks. pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. callback (:obj:`callable`): The callback function for this handler. pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the callback function. pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to the callback function. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = ('pass_groups', 'pass_groupdict', 'pattern') def __init__( self, pattern: Union[str, Pattern], callback: Callable[[str, CCT], RT], pass_groups: bool = False, pass_groupdict: bool = False, pass_update_queue: bool = False, pass_job_queue: bool = False, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, run_async=run_async, ) if isinstance(pattern, str): pattern = re.compile(pattern) self.pattern = pattern self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict def check_update(self, update: object) -> Optional[Match]: """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:obj:`object`): The incoming update. Returns: :obj:`bool` """ if isinstance(update, str): match = re.match(self.pattern, update) if match: return match return None def collect_optional_args( self, dispatcher: 'Dispatcher', update: str = None, check_result: Optional[Match] = None, ) -> Dict[str, object]: """Pass the results of ``re.match(pattern, update).{groups(), groupdict()}`` to the callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if needed. """ optional_args = super().collect_optional_args(dispatcher, update, check_result) if self.pattern: if self.pass_groups and check_result: optional_args['groups'] = check_result.groups() if self.pass_groupdict and check_result: optional_args['groupdict'] = check_result.groupdict() return optional_args def collect_additional_context( self, context: CCT, update: str, dispatcher: 'Dispatcher', check_result: Optional[Match], ) -> 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-13.11/telegram/ext/typehandler.py000066400000000000000000000112071417656324400227260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Callable, Type, TypeVar, Union from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE from .handler import Handler from .utils.types import CCT RT = TypeVar('RT') UT = TypeVar('UT') class TypeHandler(Handler[UT, CCT]): """Handler class to handle updates of custom types. Warning: When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: type (:obj:`type`): The ``type`` of updates this handler should process, as determined by ``isinstance`` callback (:obj:`callable`): The callback function for this handler. Will be called when :attr:`check_update` has determined that an update should be processed by this handler. Callback signature for context based API: ``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 ``isinstance``. Default is :obj:`False` pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``update_queue`` will be passed to the callback function. It will be the ``Queue`` instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` that contains new updates which can be used to insert updates. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called ``job_queue`` will be passed to the callback function. It will be a :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` which can be used to schedule new jobs. Default is :obj:`False`. DEPRECATED: Please switch to context based callbacks. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. Attributes: type (:obj:`type`): The ``type`` of updates this handler should process. callback (:obj:`callable`): The callback function for this handler. strict (:obj:`bool`): Use ``type`` instead of ``isinstance``. Default is :obj:`False`. pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be passed to the callback function. pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to the callback function. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. """ __slots__ = ('type', 'strict') def __init__( self, type: Type[UT], # pylint: disable=W0622 callback: Callable[[UT, CCT], RT], strict: bool = False, pass_update_queue: bool = False, pass_job_queue: bool = False, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): super().__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, run_async=run_async, ) self.type = type # pylint: disable=E0237 self.strict = strict # pylint: disable=E0237 def check_update(self, update: object) -> bool: """Determines whether an update should be passed to this handlers :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=C0123 python-telegram-bot-13.11/telegram/ext/updater.py000066400000000000000000001057661417656324400220710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 logging import ssl import warnings from queue import Queue from signal import SIGABRT, SIGINT, SIGTERM, signal from threading import Event, Lock, Thread, current_thread from time import sleep from typing import ( TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union, no_type_check, Generic, overload, ) from telegram import Bot, TelegramError from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized from telegram.ext import Dispatcher, JobQueue, ContextTypes, ExtBot from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated from telegram.utils.helpers import get_signal_name, DEFAULT_FALSE, DefaultValue from telegram.utils.request import Request from telegram.ext.utils.types import CCT, UD, CD, BD from telegram.ext.utils.webhookhandler import WebhookAppClass, WebhookServer if TYPE_CHECKING: from telegram.ext import BasePersistence, Defaults, CallbackContext class Updater(Generic[CCT, UD, CD, BD]): """ This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to :class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to receive the updates from Telegram and to deliver them to said dispatcher. It also runs in a separate thread, so the user can interact with the bot, for example on the command line. The dispatcher supports handlers for different kinds of data: Updates from Telegram, basic text commands and even arbitrary types. The updater can be started as a polling service or, for production, use a webhook to receive updates. This is achieved using the WebhookServer and WebhookHandler classes. Note: * You must supply either a :attr:`bot` or a :attr:`token` argument. * If you supply a :attr:`bot`, you will need to pass :attr:`arbitrary_callback_data`, and :attr:`defaults` to the bot instead of the :class:`telegram.ext.Updater`. In this case, you'll have to use the class :class:`telegram.ext.ExtBot`. .. versionchanged:: 13.6 Args: token (:obj:`str`, optional): The bot's token given by the @BotFather. base_url (:obj:`str`, optional): Base_url for the bot. base_file_url (:obj:`str`, optional): Base_file_url for the bot. workers (:obj:`int`, optional): Amount of threads in the thread pool for functions decorated with ``@run_async`` (ignored if `dispatcher` argument is used). bot (:class:`telegram.Bot`, optional): A pre-initialized bot instance (ignored if `dispatcher` argument is used). If a pre-initialized bot is used, it is the user's responsibility to create it using a `Request` instance with a large enough connection pool. dispatcher (:class:`telegram.ext.Dispatcher`, optional): A pre-initialized dispatcher instance. If a pre-initialized dispatcher is used, it is the user's responsibility to create it with proper arguments. private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data. private_key_password (:obj:`bytes`, optional): Password for above private key. user_sig_handler (:obj:`function`, optional): Takes ``signum, frame`` as positional arguments. This will be called when a signal is received, defaults are (SIGINT, SIGTERM, SIGABRT) settable with :attr:`idle`. request_kwargs (:obj:`dict`, optional): Keyword args to control the creation of a `telegram.utils.request.Request` object (ignored if `bot` or `dispatcher` argument is used). The request_kwargs are very useful for the advanced users who would like to control the default timeouts and/or control the proxy used for http communication. use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`. **New users**: set this to :obj:`True`. persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to store data that should be persistent over restarts (ignored if `dispatcher` argument is used). 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` | :obj:`None`, optional): Whether to allow arbitrary objects as callback data for :class:`telegram.InlineKeyboardButton`. Pass an integer to specify the maximum number of cached objects. For more details, please see our wiki. Defaults to :obj:`False`. .. versionadded:: 13.6 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 Raises: ValueError: If both :attr:`token` and :attr:`bot` are passed or none of them. Attributes: bot (:class:`telegram.Bot`): The bot used with this Updater. user_sig_handler (:obj:`function`): Optional. Function to be called when a signal is received. update_queue (:obj:`Queue`): Queue for the updates. job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater. dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and dispatches them to the handlers. running (:obj:`bool`): Indicates if the updater is running. persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to store data that should be persistent over restarts. use_context (:obj:`bool`): Optional. :obj:`True` if using context based callbacks. """ __slots__ = ( 'persistence', 'dispatcher', 'user_sig_handler', 'bot', 'logger', 'update_queue', 'job_queue', '__exception_event', 'last_update_id', 'running', '_request', 'is_idle', 'httpd', '__lock', '__threads', '__dict__', ) @overload def __init__( self: 'Updater[CallbackContext, dict, dict, dict]', token: str = None, base_url: str = None, workers: int = 4, bot: Bot = None, private_key: bytes = None, private_key_password: bytes = None, user_sig_handler: Callable = None, request_kwargs: Dict[str, Any] = None, persistence: 'BasePersistence' = None, # pylint: disable=E0601 defaults: 'Defaults' = None, use_context: bool = True, base_file_url: str = None, arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE, ): ... @overload def __init__( self: 'Updater[CCT, UD, CD, BD]', token: str = None, base_url: str = None, workers: int = 4, bot: Bot = None, private_key: bytes = None, private_key_password: bytes = None, user_sig_handler: Callable = None, request_kwargs: Dict[str, Any] = None, persistence: 'BasePersistence' = None, defaults: 'Defaults' = None, use_context: bool = True, base_file_url: str = None, arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE, context_types: ContextTypes[CCT, UD, CD, BD] = None, ): ... @overload def __init__( self: 'Updater[CCT, UD, CD, BD]', user_sig_handler: Callable = None, dispatcher: Dispatcher[CCT, UD, CD, BD] = None, ): ... def __init__( # type: ignore[no-untyped-def,misc] self, token: str = None, base_url: str = None, workers: int = 4, bot: Bot = None, private_key: bytes = None, private_key_password: bytes = None, user_sig_handler: Callable = None, request_kwargs: Dict[str, Any] = None, persistence: 'BasePersistence' = None, defaults: 'Defaults' = None, use_context: bool = True, dispatcher=None, base_file_url: str = None, arbitrary_callback_data: Union[DefaultValue, bool, int, None] = DEFAULT_FALSE, context_types: ContextTypes[CCT, UD, CD, BD] = None, ): if defaults and bot: warnings.warn( 'Passing defaults to an Updater has no effect when a Bot is passed ' 'as well. Pass them to the Bot instead.', TelegramDeprecationWarning, stacklevel=2, ) if arbitrary_callback_data is not DEFAULT_FALSE and bot: warnings.warn( 'Passing arbitrary_callback_data to an Updater has no ' 'effect when a Bot is passed as well. Pass them to the Bot instead.', stacklevel=2, ) if dispatcher is None: if (token is None) and (bot is None): raise ValueError('`token` or `bot` must be passed') if (token is not None) and (bot is not None): raise ValueError('`token` and `bot` are mutually exclusive') if (private_key is not None) and (bot is not None): raise ValueError('`bot` and `private_key` are mutually exclusive') else: if bot is not None: raise ValueError('`dispatcher` and `bot` are mutually exclusive') if persistence is not None: raise ValueError('`dispatcher` and `persistence` are mutually exclusive') if use_context != dispatcher.use_context: raise ValueError('`dispatcher` and `use_context` are mutually exclusive') if context_types is not None: raise ValueError('`dispatcher` and `context_types` are mutually exclusive') if workers is not None: raise ValueError('`dispatcher` and `workers` are mutually exclusive') self.logger = logging.getLogger(__name__) self._request = None if dispatcher is None: con_pool_size = workers + 4 if bot is not None: self.bot = bot if bot.request.con_pool_size < con_pool_size: self.logger.warning( 'Connection pool of Request object is smaller than optimal value (%s)', con_pool_size, ) else: # we need a connection pool the size of: # * for each of the workers # * 1 for Dispatcher # * 1 for polling Updater (even if webhook is used, we can spare a connection) # * 1 for JobQueue # * 1 for main thread if request_kwargs is None: request_kwargs = {} if 'con_pool_size' not in request_kwargs: request_kwargs['con_pool_size'] = con_pool_size self._request = Request(**request_kwargs) self.bot = ExtBot( token, # type: ignore[arg-type] base_url, base_file_url=base_file_url, request=self._request, private_key=private_key, private_key_password=private_key_password, defaults=defaults, arbitrary_callback_data=( False # type: ignore[arg-type] if arbitrary_callback_data is DEFAULT_FALSE else arbitrary_callback_data ), ) self.update_queue: Queue = Queue() self.job_queue = JobQueue() self.__exception_event = Event() self.persistence = persistence self.dispatcher = Dispatcher( self.bot, self.update_queue, job_queue=self.job_queue, workers=workers, exception_event=self.__exception_event, persistence=persistence, use_context=use_context, context_types=context_types, ) self.job_queue.set_dispatcher(self.dispatcher) else: con_pool_size = dispatcher.workers + 4 self.bot = dispatcher.bot if self.bot.request.con_pool_size < con_pool_size: self.logger.warning( 'Connection pool of Request object is smaller than optimal value (%s)', con_pool_size, ) self.update_queue = dispatcher.update_queue self.__exception_event = dispatcher.exception_event self.persistence = dispatcher.persistence self.job_queue = dispatcher.job_queue self.dispatcher = dispatcher self.user_sig_handler = user_sig_handler self.last_update_id = 0 self.running = False self.is_idle = False self.httpd = None self.__lock = Lock() self.__threads: List[Thread] = [] def __setattr__(self, key: str, value: object) -> None: if key.startswith('__'): key = f"_{self.__class__.__name__}{key}" if issubclass(self.__class__, Updater) and self.__class__ is not Updater: object.__setattr__(self, key, value) return set_new_attribute_deprecated(self, key, value) def _init_thread(self, target: Callable, name: str, *args: object, **kwargs: object) -> None: thr = Thread( target=self._thread_wrapper, name=f"Bot:{self.bot.id}:{name}", args=(target,) + args, kwargs=kwargs, ) thr.start() self.__threads.append(thr) def _thread_wrapper(self, target: Callable, *args: object, **kwargs: object) -> None: thr_name = current_thread().name self.logger.debug('%s - started', thr_name) try: target(*args, **kwargs) except Exception: self.__exception_event.set() self.logger.exception('unhandled exception in %s', thr_name) raise self.logger.debug('%s - ended', thr_name) def start_polling( self, poll_interval: float = 0.0, timeout: float = 10, clean: bool = None, bootstrap_retries: int = -1, read_latency: float = 2.0, allowed_updates: List[str] = None, drop_pending_updates: bool = None, ) -> Optional[Queue]: """Starts polling updates from Telegram. Args: poll_interval (:obj:`float`, optional): Time to wait between polling updates from Telegram in seconds. Default is ``0.0``. timeout (:obj:`float`, 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 clean (:obj:`bool`, optional): Alias for ``drop_pending_updates``. .. deprecated:: 13.4 Use ``drop_pending_updates`` instead. 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 allowed_updates (List[:obj:`str`], optional): Passed to :meth:`telegram.Bot.get_updates`. read_latency (:obj:`float` | :obj:`int`, optional): Grace time in seconds for receiving the reply from server. Will be added to the ``timeout`` value and used as the read timeout from server (Default: ``2``). Returns: :obj:`Queue`: The update queue that can be filled from the main thread. """ if (clean is not None) and (drop_pending_updates is not None): raise TypeError('`clean` and `drop_pending_updates` are mutually exclusive.') if clean is not None: warnings.warn( 'The argument `clean` of `start_polling` is deprecated. Please use ' '`drop_pending_updates` instead.', category=TelegramDeprecationWarning, stacklevel=2, ) drop_pending_updates = drop_pending_updates if drop_pending_updates is not None else clean with self.__lock: if not self.running: self.running = True # Create & start threads self.job_queue.start() dispatcher_ready = Event() polling_ready = Event() self._init_thread(self.dispatcher.start, "dispatcher", ready=dispatcher_ready) self._init_thread( self._start_polling, "updater", poll_interval, timeout, read_latency, bootstrap_retries, drop_pending_updates, allowed_updates, ready=polling_ready, ) self.logger.debug('Waiting for Dispatcher and polling to start') dispatcher_ready.wait() polling_ready.wait() # Return the update queue so the main thread can insert updates return self.update_queue return None def start_webhook( self, listen: str = '127.0.0.1', port: int = 80, url_path: str = '', cert: str = None, key: str = None, clean: bool = None, bootstrap_retries: int = 0, webhook_url: str = None, allowed_updates: List[str] = None, force_event_loop: bool = None, drop_pending_updates: bool = None, ip_address: str = None, max_connections: int = 40, ) -> Optional[Queue]: """ Starts a small http server to listen for updates via webhook. If :attr:`cert` and :attr:`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. .. 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. Args: listen (:obj:`str`, optional): IP-Address to listen on. Default ``127.0.0.1``. port (:obj:`int`, optional): Port the bot should be listening on. Default ``80``. url_path (:obj:`str`, optional): Path inside url. cert (:obj:`str`, optional): Path to the SSL certificate file. key (: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 clean (:obj:`bool`, optional): Alias for ``drop_pending_updates``. .. deprecated:: 13.4 Use ``drop_pending_updates`` instead. 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 webhook_url (:obj:`str`, optional): Explicitly specify the webhook url. Useful behind NAT, reverse proxy, etc. Default is derived from ``listen``, ``port`` & ``url_path``. ip_address (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`. .. versionadded :: 13.4 allowed_updates (List[:obj:`str`], optional): Passed to :meth:`telegram.Bot.set_webhook`. force_event_loop (:obj:`bool`, optional): Legacy parameter formerly used for a workaround on Windows + Python 3.8+. No longer has any effect. .. deprecated:: 13.6 Since version 13.6, ``tornade>=6.1`` is required, which resolves the former issue. max_connections (:obj:`int`, optional): Passed to :meth:`telegram.Bot.set_webhook`. .. versionadded:: 13.6 Returns: :obj:`Queue`: The update queue that can be filled from the main thread. """ if (clean is not None) and (drop_pending_updates is not None): raise TypeError('`clean` and `drop_pending_updates` are mutually exclusive.') if clean is not None: warnings.warn( 'The argument `clean` of `start_webhook` is deprecated. Please use ' '`drop_pending_updates` instead.', category=TelegramDeprecationWarning, stacklevel=2, ) if force_event_loop is not None: warnings.warn( 'The argument `force_event_loop` of `start_webhook` is deprecated and no longer ' 'has any effect.', category=TelegramDeprecationWarning, stacklevel=2, ) drop_pending_updates = drop_pending_updates if drop_pending_updates is not None else clean with self.__lock: if not self.running: self.running = True # Create & start threads webhook_ready = Event() dispatcher_ready = Event() self.job_queue.start() self._init_thread(self.dispatcher.start, "dispatcher", dispatcher_ready) self._init_thread( self._start_webhook, "updater", listen, port, url_path, cert, key, bootstrap_retries, drop_pending_updates, webhook_url, allowed_updates, ready=webhook_ready, ip_address=ip_address, max_connections=max_connections, ) self.logger.debug('Waiting for Dispatcher and Webhook to start') webhook_ready.wait() dispatcher_ready.wait() # Return the update queue so the main thread can insert updates return self.update_queue return None @no_type_check def _start_polling( self, poll_interval, timeout, read_latency, bootstrap_retries, drop_pending_updates, allowed_updates, ready=None, ): # pragma: no cover # Thread target of thread 'updater'. Runs in background, pulls # updates from Telegram and inserts them in the update queue of the # Dispatcher. self.logger.debug('Updater thread started (polling)') self._bootstrap( bootstrap_retries, drop_pending_updates=drop_pending_updates, webhook_url='', allowed_updates=None, ) self.logger.debug('Bootstrap done') def polling_action_cb(): updates = self.bot.get_updates( self.last_update_id, timeout=timeout, read_latency=read_latency, allowed_updates=allowed_updates, ) if updates: if not self.running: self.logger.debug('Updates ignored and will be pulled again on restart') else: for update in updates: self.update_queue.put(update) self.last_update_id = updates[-1].update_id + 1 return True def polling_onerr_cb(exc): # Put the error into the update queue and let the Dispatcher # broadcast it self.update_queue.put(exc) if ready is not None: ready.set() self._network_loop_retry( polling_action_cb, polling_onerr_cb, 'getting Updates', poll_interval ) @no_type_check def _network_loop_retry(self, action_cb, onerr_cb, description, interval): """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 (:obj:`callable`): Network oriented callback function to call. onerr_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`. """ self.logger.debug('Start network loop retry %s', description) cur_interval = interval while self.running: try: if not action_cb(): break except RetryAfter as exc: self.logger.info('%s', exc) cur_interval = 0.5 + exc.retry_after except TimedOut as toe: self.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: self.logger.error('Invalid token; aborting') raise pex except TelegramError as telegram_exc: self.logger.error('Error while %s: %s', description, telegram_exc) onerr_cb(telegram_exc) cur_interval = self._increase_poll_interval(cur_interval) else: cur_interval = interval if cur_interval: sleep(cur_interval) @staticmethod def _increase_poll_interval(current_interval: float) -> float: # increase waiting times on subsequent errors up to 30secs if current_interval == 0: current_interval = 1 elif current_interval < 30: current_interval *= 1.5 else: current_interval = min(30.0, current_interval) return current_interval @no_type_check def _start_webhook( self, listen, port, url_path, cert, key, bootstrap_retries, drop_pending_updates, webhook_url, allowed_updates, ready=None, ip_address=None, max_connections: int = 40, ): self.logger.debug('Updater thread started (webhook)') # 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 use_ssl = cert is not None and key is not None if not url_path.startswith('/'): url_path = f'/{url_path}' # Create Tornado app instance app = WebhookAppClass(url_path, self.bot, self.update_queue) # Form SSL Context # An SSLError is raised if the private key does not match with the certificate if use_ssl: try: ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_ctx.load_cert_chain(cert, key) 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) if not webhook_url: webhook_url = self._gen_webhook_url(listen, port, url_path) # We pass along the cert to the webhook if present. cert_file = open(cert, 'rb') if cert is not None else None self._bootstrap( max_retries=bootstrap_retries, drop_pending_updates=drop_pending_updates, webhook_url=webhook_url, allowed_updates=allowed_updates, cert=cert_file, ip_address=ip_address, max_connections=max_connections, ) if cert_file is not None: cert_file.close() self.httpd.serve_forever(ready=ready) @staticmethod def _gen_webhook_url(listen: str, port: int, url_path: str) -> str: return f'https://{listen}:{port}{url_path}' @no_type_check def _bootstrap( self, max_retries, drop_pending_updates, webhook_url, allowed_updates, cert=None, bootstrap_interval=5, ip_address=None, max_connections: int = 40, ): retries = [0] def bootstrap_del_webhook(): self.logger.debug('Deleting webhook') if drop_pending_updates: self.logger.debug('Dropping pending updates from Telegram server') self.bot.delete_webhook(drop_pending_updates=drop_pending_updates) return False def bootstrap_set_webhook(): self.logger.debug('Setting webhook') if drop_pending_updates: self.logger.debug('Dropping pending updates from Telegram server') self.bot.set_webhook( url=webhook_url, certificate=cert, allowed_updates=allowed_updates, ip_address=ip_address, drop_pending_updates=drop_pending_updates, max_connections=max_connections, ) return False def bootstrap_onerr_cb(exc): if not isinstance(exc, Unauthorized) and (max_retries < 0 or retries[0] < max_retries): retries[0] += 1 self.logger.warning( 'Failed bootstrap phase; try=%s max_retries=%s', retries[0], max_retries ) else: self.logger.error('Failed bootstrap phase after %s retries (%s)', retries[0], 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: self._network_loop_retry( bootstrap_del_webhook, bootstrap_onerr_cb, 'bootstrap del webhook', bootstrap_interval, ) retries[0] = 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: self._network_loop_retry( bootstrap_set_webhook, bootstrap_onerr_cb, 'bootstrap set webhook', bootstrap_interval, ) def stop(self) -> None: """Stops the polling/webhook thread, the dispatcher and the job queue.""" self.job_queue.stop() with self.__lock: if self.running or self.dispatcher.has_running_threads: self.logger.debug('Stopping Updater and Dispatcher...') self.running = False self._stop_httpd() self._stop_dispatcher() self._join_threads() # Stop the Request instance only if it was created by the Updater if self._request: self._request.stop() @no_type_check def _stop_httpd(self) -> None: if self.httpd: self.logger.debug( 'Waiting for current webhook connection to be ' 'closed... Send a Telegram message to the bot to exit ' 'immediately.' ) self.httpd.shutdown() self.httpd = None @no_type_check def _stop_dispatcher(self) -> None: self.logger.debug('Requesting Dispatcher to stop...') self.dispatcher.stop() @no_type_check def _join_threads(self) -> None: for thr in self.__threads: self.logger.debug('Waiting for %s thread to end', thr.name) thr.join() self.logger.debug('%s thread has ended', thr.name) self.__threads = [] @no_type_check def _signal_handler(self, signum, frame) -> None: self.is_idle = False if self.running: self.logger.info( 'Received signal %s (%s), stopping...', signum, get_signal_name(signum) ) if self.persistence: # Update user_data, chat_data and bot_data before flushing self.dispatcher.update_persistence() self.persistence.flush() self.stop() if self.user_sig_handler: self.user_sig_handler(signum, frame) else: self.logger.warning('Exiting immediately!') # pylint: disable=C0415,W0212 import os os._exit(1) def idle(self, stop_signals: Union[List, Tuple] = (SIGINT, SIGTERM, SIGABRT)) -> None: """Blocks until one of the signals are received and stops the updater. Args: stop_signals (:obj:`list` | :obj:`tuple`): List containing signals from the signal module that should be subscribed to. :meth:`Updater.stop()` will be called on receiving one of those signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``). """ for sig in stop_signals: signal(sig, self._signal_handler) self.is_idle = True while self.is_idle: sleep(1) python-telegram-bot-13.11/telegram/ext/utils/000077500000000000000000000000001417656324400211745ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/ext/utils/__init__.py000066400000000000000000000014401417656324400233040ustar00rootroot00000000000000# # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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-13.11/telegram/ext/utils/promise.py000066400000000000000000000132541417656324400232310ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Promise class.""" import logging from threading import Event from typing import Callable, List, Optional, Tuple, TypeVar, Union from telegram.utils.deprecate import set_new_attribute_deprecated from telegram.utils.types import JSONDict RT = TypeVar('RT') logger = logging.getLogger(__name__) class Promise: """A simple Promise implementation for use with the run_async decorator, DelayQueue etc. Args: pooled_function (:obj:`callable`): The callable that will be called concurrently. args (:obj:`list` | :obj:`tuple`): Positional arguments for :attr:`pooled_function`. kwargs (:obj:`dict`): Keyword arguments for :attr:`pooled_function`. update (:class:`telegram.Update` | :obj:`object`, optional): The update this promise is associated with. error_handling (:obj:`bool`, optional): Whether exceptions raised by :attr:`func` may be handled by error handlers. Defaults to :obj:`True`. Attributes: pooled_function (:obj:`callable`): The callable that will be called concurrently. args (:obj:`list` | :obj:`tuple`): Positional arguments for :attr:`pooled_function`. kwargs (:obj:`dict`): Keyword arguments for :attr:`pooled_function`. done (:obj:`threading.Event`): Is set when the result is available. update (:class:`telegram.Update` | :obj:`object`): Optional. The update this promise is associated with. error_handling (:obj:`bool`): Optional. Whether exceptions raised by :attr:`func` may be handled by error handlers. Defaults to :obj:`True`. """ __slots__ = ( 'pooled_function', 'args', 'kwargs', 'update', 'error_handling', 'done', '_done_callback', '_result', '_exception', '__dict__', ) # TODO: Remove error_handling parameter once we drop the @run_async decorator def __init__( self, pooled_function: Callable[..., RT], args: Union[List, Tuple], kwargs: JSONDict, update: object = None, error_handling: bool = True, ): self.pooled_function = pooled_function self.args = args self.kwargs = kwargs self.update = update self.error_handling = error_handling self.done = Event() self._done_callback: Optional[Callable] = None self._result: Optional[RT] = None self._exception: Optional[Exception] = None def __setattr__(self, key: str, value: object) -> None: set_new_attribute_deprecated(self, key, value) def run(self) -> None: """Calls the :attr:`pooled_function` callable.""" try: self._result = self.pooled_function(*self.args, **self.kwargs) except Exception as exc: self._exception = exc finally: self.done.set() if self._exception is None and self._done_callback: try: self._done_callback(self.result()) except Exception as exc: logger.warning( "`done_callback` of a Promise raised the following exception." " The exception won't be handled by error handlers." ) logger.warning("Full traceback:", exc_info=exc) def __call__(self) -> None: self.run() def result(self, timeout: float = None) -> Optional[RT]: """Return the result of the ``Promise``. Args: timeout (:obj:`float`, optional): Maximum time in seconds to wait for the result to be calculated. ``None`` means indefinite. Default is ``None``. Returns: Returns the return value of :attr:`pooled_function` or ``None`` if the ``timeout`` expires. Raises: object exception raised by :attr:`pooled_function`. """ self.done.wait(timeout=timeout) if self._exception is not None: raise self._exception # pylint: disable=raising-bad-type return self._result def add_done_callback(self, callback: Callable) -> None: """ Callback to be run when :class:`telegram.ext.utils.promise.Promise` becomes done. Note: Callback won't be called if :attr:`pooled_function` raises an exception. Args: callback (:obj:`callable`): The callable that will be called when promise is done. callback will be called by passing ``Promise.result()`` as only positional argument. """ if self.done.wait(0): callback(self.result()) else: self._done_callback = callback @property def exception(self) -> Optional[Exception]: """The exception raised by :attr:`pooled_function` or ``None`` if no exception has been raised (yet). """ return self._exception python-telegram-bot-13.11/telegram/ext/utils/types.py000066400000000000000000000035661417656324400227240ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 """ from typing import TypeVar, TYPE_CHECKING, Tuple, List, Dict, Any, Optional if TYPE_CHECKING: from telegram.ext import CallbackContext # noqa: F401 ConversationDict = Dict[Tuple[int, ...], Optional[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`, :obj:`any`]]], \ Dict[:obj:`str`, :obj:`str`]]: Data returned by :attr:`telegram.ext.CallbackDataCache.persistence_data`. .. versionadded:: 13.6 """ CCT = TypeVar('CCT', bound='CallbackContext') """An instance of :class:`telegram.ext.CallbackContext` or a custom subclass. .. versionadded:: 13.6 """ 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 """ python-telegram-bot-13.11/telegram/ext/utils/webhookhandler.py000066400000000000000000000137251417656324400245520ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=C0114 import logging from queue import Queue from ssl import SSLContext from threading import Event, Lock from typing import TYPE_CHECKING, Any, Optional import tornado.web from tornado import httputil from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop from telegram import Update from telegram.ext import ExtBot from telegram.utils.deprecate import set_new_attribute_deprecated from telegram.utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot try: import ujson as json except ImportError: import json # type: ignore[no-redef] class WebhookServer: __slots__ = ( 'http_server', 'listen', 'port', 'loop', 'logger', 'is_running', 'server_lock', 'shutdown_lock', '__dict__', ) def __init__( self, listen: str, port: int, webhook_app: 'WebhookAppClass', ssl_ctx: SSLContext ): self.http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx) self.listen = listen self.port = port self.loop: Optional[IOLoop] = None self.logger = logging.getLogger(__name__) self.is_running = False self.server_lock = Lock() self.shutdown_lock = Lock() def __setattr__(self, key: str, value: object) -> None: set_new_attribute_deprecated(self, key, value) def serve_forever(self, ready: Event = None) -> None: with self.server_lock: IOLoop().make_current() self.is_running = True self.logger.debug('Webhook Server started.') self.loop = IOLoop.current() self.http_server.listen(self.port, address=self.listen) if ready is not None: ready.set() self.loop.start() self.logger.debug('Webhook Server stopped.') self.is_running = False def shutdown(self) -> None: with self.shutdown_lock: if not self.is_running: self.logger.warning('Webhook Server already stopped.') return self.loop.add_callback(self.loop.stop) # type: ignore def handle_error(self, request: object, client_address: str) -> None: # pylint: disable=W0613 """Handle an error gracefully.""" self.logger.debug( 'Exception happened during processing of request from %s', client_address, exc_info=True, ) class WebhookAppClass(tornado.web.Application): def __init__(self, webhook_path: str, bot: 'Bot', update_queue: Queue): self.shared_objects = {"bot": bot, "update_queue": update_queue} handlers = [(rf"{webhook_path}/?", WebhookHandler, self.shared_objects)] # noqa tornado.web.Application.__init__(self, handlers) # type: ignore def log_request(self, handler: tornado.web.RequestHandler) -> None: # skipcq: PTC-W0049 pass # WebhookHandler, process webhook calls # pylint: disable=W0223 class WebhookHandler(tornado.web.RequestHandler): SUPPORTED_METHODS = ["POST"] # type: ignore def __init__( self, application: tornado.web.Application, request: httputil.HTTPServerRequest, **kwargs: JSONDict, ): super().__init__(application, request, **kwargs) self.logger = logging.getLogger(__name__) def initialize(self, bot: 'Bot', update_queue: Queue) -> None: # pylint: disable=W0201 self.bot = bot self.update_queue = update_queue def set_default_headers(self) -> None: self.set_header("Content-Type", 'application/json; charset="utf-8"') def post(self) -> None: self.logger.debug('Webhook triggered') self._validate_post() json_string = self.request.body.decode() data = json.loads(json_string) self.set_status(200) self.logger.debug('Webhook received data: %s', json_string) update = Update.de_json(data, self.bot) if update: self.logger.debug('Received Update with ID %d on Webhook', update.update_id) # handle arbitrary callback data, if necessary if isinstance(self.bot, ExtBot): self.bot.insert_callback_data(update) self.update_queue.put(update) def _validate_post(self) -> None: ct_header = self.request.headers.get("Content-Type", None) if ct_header != 'application/json': raise tornado.web.HTTPError(403) def write_error(self, status_code: int, **kwargs: Any) -> None: """Log an arbitrary message. This is used by all other logging functions. It overrides ``BaseHTTPRequestHandler.log_message``, which logs to ``sys.stderr``. The first argument, FORMAT, is a format string for the message to be logged. If the format string contains any % escapes requiring parameters, they should be specified as subsequent arguments (it's just like printf!). The client ip is prefixed to every message. """ super().write_error(status_code, **kwargs) self.logger.debug( "%s - - %s", self.request.remote_ip, "Exception in WebhookHandler", exc_info=kwargs['exc_info'], ) python-telegram-bot-13.11/telegram/files/000077500000000000000000000000001417656324400203365ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/files/__init__.py000066400000000000000000000000001417656324400224350ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/files/animation.py000066400000000000000000000121161417656324400226700ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 TYPE_CHECKING, Any, Optional from telegram import PhotoSize, TelegramObject from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot, File class Animation(TelegramObject): """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. 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. thumb (:class:`telegram.PhotoSize`, optional): Animation thumbnail 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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. 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. thumb (:class:`telegram.PhotoSize`): Optional. Animation thumbnail 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. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ( 'bot', 'width', 'file_id', 'file_size', 'file_name', 'thumb', 'duration', 'mime_type', 'height', 'file_unique_id', '_id_attrs', ) def __init__( self, file_id: str, file_unique_id: str, width: int, height: int, duration: int, thumb: PhotoSize = None, file_name: str = None, mime_type: str = None, file_size: int = None, bot: 'Bot' = None, **_kwargs: Any, ): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) self.width = int(width) self.height = int(height) self.duration = duration # Optionals self.thumb = thumb self.file_name = file_name self.mime_type = mime_type self.file_size = file_size self.bot = bot self._id_attrs = (self.file_unique_id,) @classmethod def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Animation']: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) def get_file( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> 'File': """Convenience wrapper over :attr:`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 self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) python-telegram-bot-13.11/telegram/files/audio.py000066400000000000000000000124251417656324400220150ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 TYPE_CHECKING, Any, Optional from telegram import PhotoSize, TelegramObject from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot, File class Audio(TelegramObject): """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. 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. thumb (:class:`telegram.PhotoSize`, optional): Thumbnail of the album cover to which the music file belongs. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: file_id (:obj:`str`): Identifier for this 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. 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. thumb (:class:`telegram.PhotoSize`): Optional. Thumbnail of the album cover to which the music file belongs. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ( 'file_id', 'bot', 'file_size', 'file_name', 'thumb', 'title', 'duration', 'performer', 'mime_type', 'file_unique_id', '_id_attrs', ) def __init__( self, file_id: str, file_unique_id: str, duration: int, performer: str = None, title: str = None, mime_type: str = None, file_size: int = None, thumb: PhotoSize = None, bot: 'Bot' = None, file_name: str = None, **_kwargs: Any, ): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) self.duration = int(duration) # Optionals self.performer = performer self.title = title self.file_name = file_name self.mime_type = mime_type self.file_size = file_size self.thumb = thumb self.bot = bot self._id_attrs = (self.file_unique_id,) @classmethod def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Audio']: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) def get_file( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> 'File': """Convenience wrapper over :attr:`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 self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) python-telegram-bot-13.11/telegram/files/chatphoto.py000066400000000000000000000120731417656324400227040ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any from telegram import TelegramObject from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot, 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`): Unique file identifier of small (160x160) 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 (160x160) 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`): Unique file identifier of big (640x640) 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 (640x640) 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: small_file_id (:obj:`str`): File identifier of small (160x160) 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 (160x160) 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 (640x640) 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 (640x640) 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_unique_id', 'bot', 'small_file_id', 'small_file_unique_id', 'big_file_id', '_id_attrs', ) def __init__( self, small_file_id: str, small_file_unique_id: str, big_file_id: str, big_file_unique_id: str, bot: 'Bot' = None, **_kwargs: Any, ): self.small_file_id = small_file_id self.small_file_unique_id = small_file_unique_id self.big_file_id = big_file_id self.big_file_unique_id = big_file_unique_id self.bot = bot self._id_attrs = ( self.small_file_unique_id, self.big_file_unique_id, ) def get_small_file( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the small (160x160) 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 self.bot.get_file( file_id=self.small_file_id, timeout=timeout, api_kwargs=api_kwargs ) def get_big_file( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> 'File': """Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the big (640x640) 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 self.bot.get_file(file_id=self.big_file_id, timeout=timeout, api_kwargs=api_kwargs) python-telegram-bot-13.11/telegram/files/contact.py000066400000000000000000000047541417656324400223550ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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__ = ('vcard', 'user_id', 'first_name', 'last_name', 'phone_number', '_id_attrs') def __init__( self, phone_number: str, first_name: str, last_name: str = None, user_id: int = None, vcard: str = None, **_kwargs: Any, ): # Required self.phone_number = str(phone_number) self.first_name = first_name # Optionals self.last_name = last_name self.user_id = user_id self.vcard = vcard self._id_attrs = (self.phone_number,) python-telegram-bot-13.11/telegram/files/document.py000066400000000000000000000106061417656324400225310ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 TYPE_CHECKING, Any, Optional from telegram import PhotoSize, TelegramObject from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot, File class Document(TelegramObject): """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. 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. thumb (:class:`telegram.PhotoSize`, optional): Document thumbnail as defined by sender. 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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. thumb (:class:`telegram.PhotoSize`): Optional. Document thumbnail. file_name (:obj:`str`): Original filename. mime_type (:obj:`str`): Optional. MIME type of the file. file_size (:obj:`int`): Optional. File size. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ( 'bot', 'file_id', 'file_size', 'file_name', 'thumb', 'mime_type', 'file_unique_id', '_id_attrs', ) _id_keys = ('file_id',) def __init__( self, file_id: str, file_unique_id: str, thumb: PhotoSize = None, file_name: str = None, mime_type: str = None, file_size: int = None, bot: 'Bot' = None, **_kwargs: Any, ): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) # Optionals self.thumb = thumb self.file_name = file_name self.mime_type = mime_type self.file_size = file_size self.bot = bot self._id_attrs = (self.file_unique_id,) @classmethod def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Document']: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) def get_file( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> 'File': """Convenience wrapper over :attr:`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 self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) python-telegram-bot-13.11/telegram/files/file.py000066400000000000000000000202271417656324400216320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 os import shutil import urllib.parse as urllib_parse from base64 import b64decode from os.path import basename from typing import IO, TYPE_CHECKING, Any, Optional, Union from telegram import TelegramObject from telegram.passport.credentials import decrypt from telegram.utils.helpers import is_local_file if TYPE_CHECKING: from telegram import Bot, FileCredentials class File(TelegramObject): """ This object represents a file ready to be downloaded. The file can be downloaded with :attr:`download`. 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. Note: * Maximum file size to download is 20 MB. * 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 :attr:`download()`. 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): Optional. File size, if known. file_path (:obj:`str`, optional): File path. Use :attr:`download` to get the file. bot (:obj:`telegram.Bot`, optional): Bot to use with shortcut method. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: file_id (:obj:`str`): Identifier for this 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:`str`): Optional. File size. file_path (:obj:`str`): Optional. File path. Use :attr:`download` to get the file. """ __slots__ = ( 'bot', 'file_id', 'file_size', 'file_unique_id', 'file_path', '_credentials', '_id_attrs', ) def __init__( self, file_id: str, file_unique_id: str, bot: 'Bot' = None, file_size: int = None, file_path: str = None, **_kwargs: Any, ): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) # Optionals self.file_size = file_size self.file_path = file_path self.bot = bot self._credentials: Optional['FileCredentials'] = None self._id_attrs = (self.file_unique_id,) def download( self, custom_path: str = None, out: IO = None, timeout: int = None ) -> Union[str, IO]: """ Download this file. By default, the file is saved in the current working directory with its original filename as reported by Telegram. If the file has no filename, it the file ID will be used as filename. If a :attr:`custom_path` is supplied, it will be saved to that path instead. If :attr:`out` is defined, the file contents will be saved to that object using the ``out.write`` method. Note: * :attr:`custom_path` and :attr:`out` are mutually exclusive. * If neither :attr:`custom_path` nor :attr:`out` is 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. Args: custom_path (:obj:`str`, optional): Custom path. out (:obj:`io.BufferedWriter`, optional): A file-like object. Must be opened for writing in binary mode, if applicable. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). Returns: :obj:`str` | :obj:`io.BufferedWriter`: The same object as :attr:`out` if specified. Otherwise, returns the filename downloaded to or the file path of the local file. Raises: ValueError: If both :attr:`custom_path` and :attr:`out` are passed. """ if custom_path is not None and out is not None: raise ValueError('custom_path and out are mutually exclusive') local_file = is_local_file(self.file_path) if local_file: url = self.file_path else: # Convert any UTF-8 char into a url encoded ASCII string. url = self._get_encoded_url() if out: if local_file: with open(url, 'rb') as file: buf = file.read() else: buf = self.bot.request.retrieve(url) if self._credentials: buf = decrypt( b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf ) out.write(buf) return out if custom_path and local_file: shutil.copyfile(self.file_path, custom_path) return custom_path if custom_path: filename = custom_path elif local_file: return self.file_path elif self.file_path: filename = basename(self.file_path) else: filename = os.path.join(os.getcwd(), self.file_id) buf = self.bot.request.retrieve(url, timeout=timeout) if self._credentials: buf = decrypt( b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf ) with open(filename, 'wb') as fobj: fobj.write(buf) return filename 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(self.file_path) return urllib_parse.urlunsplit( urllib_parse.SplitResult( sres.scheme, sres.netloc, urllib_parse.quote(sres.path), sres.query, sres.fragment ) ) def download_as_bytearray(self, buf: bytearray = None) -> bytes: """Download this file and return it as a bytearray. Args: buf (:obj:`bytearray`, optional): Extend the given bytearray with the downloaded data. Returns: :obj:`bytearray`: The same object as :attr:`buf` if it was specified. Otherwise a newly allocated :obj:`bytearray`. """ if buf is None: buf = bytearray() if is_local_file(self.file_path): with open(self.file_path, "rb") as file: buf.extend(file.read()) else: buf.extend(self.bot.request.retrieve(self._get_encoded_url())) 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-13.11/telegram/files/inputfile.py000066400000000000000000000102331417656324400227060ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=W0622,E0611 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 imghdr import logging import mimetypes import os from typing import IO, Optional, Tuple, Union from uuid import uuid4 from telegram.utils.deprecate import set_new_attribute_deprecated DEFAULT_MIME_TYPE = 'application/octet-stream' logger = logging.getLogger(__name__) class InputFile: """This object represents a Telegram InputFile. Args: obj (:obj:`File handler` | :obj:`bytes`): An open file descriptor or the files content as bytes. filename (:obj:`str`, optional): Filename for this InputFile. attach (:obj:`bool`, optional): Whether this should be send as one file or is part of a collection of files. Raises: TelegramError Attributes: input_file_content (:obj:`bytes`): The binary content of the file to send. filename (:obj:`str`): Optional. Filename for the file to be sent. attach (:obj:`str`): Optional. Attach id for sending multiple files. """ __slots__ = ('filename', 'attach', 'input_file_content', 'mimetype', '__dict__') def __init__(self, obj: Union[IO, bytes], filename: str = None, attach: bool = None): self.filename = None if isinstance(obj, bytes): self.input_file_content = obj else: self.input_file_content = obj.read() self.attach = 'attached' + uuid4().hex if attach else None if filename: self.filename = filename elif hasattr(obj, 'name') and not isinstance(obj.name, int): # type: ignore[union-attr] self.filename = os.path.basename(obj.name) # type: ignore[union-attr] image_mime_type = self.is_image(self.input_file_content) if image_mime_type: self.mimetype = image_mime_type elif self.filename: self.mimetype = mimetypes.guess_type(self.filename)[0] or DEFAULT_MIME_TYPE else: self.mimetype = DEFAULT_MIME_TYPE if not self.filename: self.filename = self.mimetype.replace('/', '.') def __setattr__(self, key: str, value: object) -> None: set_new_attribute_deprecated(self, key, value) @property def field_tuple(self) -> Tuple[str, bytes, str]: # skipcq: PY-D0003 return self.filename, self.input_file_content, self.mimetype @staticmethod def is_image(stream: bytes) -> Optional[str]: """Check if the content file is an image by analyzing its headers. Args: stream (:obj:`bytes`): A byte stream representing the content of a file. Returns: :obj:`str` | :obj:`None`: The mime-type of an image, if the input is an image, or :obj:`None` else. """ try: image = imghdr.what(None, stream) if image: return f'image/{image}' return None except Exception: logger.debug( "Could not parse file content. Assuming that file is not an image.", exc_info=True ) return None @staticmethod def is_file(obj: object) -> bool: # skipcq: PY-D0003 return hasattr(obj, 'read') def to_dict(self) -> Optional[str]: """See :meth:`telegram.TelegramObject.to_dict`.""" if self.attach: return 'attach://' + self.attach return None python-telegram-bot-13.11/telegram/files/inputmedia.py000066400000000000000000000541151417656324400230550ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 Union, List, Tuple from telegram import ( Animation, Audio, Document, InputFile, PhotoSize, TelegramObject, Video, MessageEntity, ) from telegram.utils.helpers import DEFAULT_NONE, parse_file_input from telegram.utils.types import FileInput, JSONDict, ODVInput class InputMedia(TelegramObject): """Base class for Telegram InputMedia Objects. See :class:`telegram.InputMediaAnimation`, :class:`telegram.InputMediaAudio`, :class:`telegram.InputMediaDocument`, :class:`telegram.InputMediaPhoto` and :class:`telegram.InputMediaVideo` for detailed use. """ __slots__ = () caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...], None] = None def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() if self.caption_entities: data['caption_entities'] = [ ce.to_dict() for ce in self.caption_entities # pylint: disable=E1133 ] return data 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. Args: media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Animation`): File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. 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 thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): 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. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. caption (:obj:`str`, optional): Caption of the animation to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of parse_mode. width (:obj:`int`, optional): Animation width. height (:obj:`int`, optional): Animation height. duration (:obj:`int`, optional): Animation duration. Attributes: type (:obj:`str`): ``animation``. media (:obj:`str` | :class:`telegram.InputFile`): Animation to send. caption (:obj:`str`): Optional. Caption of the document to be sent. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption. thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. width (:obj:`int`): Optional. Animation width. height (:obj:`int`): Optional. Animation height. duration (:obj:`int`): Optional. Animation duration. """ __slots__ = ( 'caption_entities', 'width', 'media', 'thumb', 'caption', 'duration', 'parse_mode', 'height', 'type', ) def __init__( self, media: Union[FileInput, Animation], thumb: FileInput = None, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, width: int = None, height: int = None, duration: int = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, filename: str = None, ): self.type = 'animation' if isinstance(media, Animation): self.media: Union[str, InputFile] = media.file_id self.width = media.width self.height = media.height self.duration = media.duration else: self.media = parse_file_input(media, attach=True, filename=filename) if thumb: self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities if width: self.width = width if height: self.height = height if duration: self.duration = duration class InputMediaPhoto(InputMedia): """Represents a photo to be sent. Args: media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.PhotoSize`): File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of parse_mode. Attributes: type (:obj:`str`): ``photo``. media (:obj:`str` | :class:`telegram.InputFile`): Photo to send. caption (:obj:`str`): Optional. Caption of the document to be sent. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption. """ __slots__ = ('caption_entities', 'media', 'caption', 'parse_mode', 'type') def __init__( self, media: Union[FileInput, PhotoSize], caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, filename: str = None, ): self.type = 'photo' self.media = parse_file_input(media, PhotoSize, attach=True, filename=filename) if caption: self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities class InputMediaVideo(InputMedia): """Represents a video to be sent. 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. * ``thumb`` will be ignored for small video files, for which Telegram can easily generate thumb nails. However, this behaviour is undocumented and might be changed by Telegram. Args: media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Video`): File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of parse_mode. width (:obj:`int`, optional): Video width. height (:obj:`int`, optional): Video height. duration (:obj:`int`, optional): Video duration. supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is suitable for streaming. thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): 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. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. Attributes: type (:obj:`str`): ``video``. media (:obj:`str` | :class:`telegram.InputFile`): Video file to send. caption (:obj:`str`): Optional. Caption of the document to be sent. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption. width (:obj:`int`): Optional. Video width. height (:obj:`int`): Optional. Video height. duration (:obj:`int`): Optional. Video duration. supports_streaming (:obj:`bool`): Optional. Pass :obj:`True`, if the uploaded video is suitable for streaming. thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. """ __slots__ = ( 'caption_entities', 'width', 'media', 'thumb', 'supports_streaming', 'caption', 'duration', 'parse_mode', 'height', 'type', ) def __init__( self, media: Union[FileInput, Video], caption: str = None, width: int = None, height: int = None, duration: int = None, supports_streaming: bool = None, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb: FileInput = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, filename: str = None, ): self.type = 'video' if isinstance(media, Video): self.media: Union[str, InputFile] = media.file_id self.width = media.width self.height = media.height self.duration = media.duration else: self.media = parse_file_input(media, attach=True, filename=filename) if thumb: self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities if width: self.width = width if height: self.height = height if duration: self.duration = duration if supports_streaming: self.supports_streaming = supports_streaming class InputMediaAudio(InputMedia): """Represents an audio file to be treated as music to be sent. 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. Args: media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Audio`): File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of parse_mode. 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. thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): 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. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. Attributes: type (:obj:`str`): ``audio``. media (:obj:`str` | :class:`telegram.InputFile`): Audio file to send. caption (:obj:`str`): Optional. Caption of the document to be sent. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption. duration (:obj:`int`): 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. thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. """ __slots__ = ( 'caption_entities', 'media', 'thumb', 'caption', 'title', 'duration', 'type', 'parse_mode', 'performer', ) def __init__( self, media: Union[FileInput, Audio], thumb: FileInput = None, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, duration: int = None, performer: str = None, title: str = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, filename: str = None, ): self.type = 'audio' if isinstance(media, Audio): self.media: Union[str, InputFile] = media.file_id self.duration = media.duration self.performer = media.performer self.title = media.title else: self.media = parse_file_input(media, attach=True, filename=filename) if thumb: self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities if duration: self.duration = duration if performer: self.performer = performer if title: self.title = title class InputMediaDocument(InputMedia): """Represents a general file to be sent. Args: media (:obj:`str` | `filelike object` | :obj:`bytes` | :class:`pathlib.Path` | \ :class:`telegram.Document`): File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet. 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of parse_mode. thumb (`filelike object` | :obj:`bytes` | :class:`pathlib.Path`, optional): 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. .. versionchanged:: 13.2 Accept :obj:`bytes` as input. disable_content_type_detection (:obj:`bool`, optional): Disables automatic server-side content type detection for files uploaded using multipart/form-data. Always true, if the document is sent as part of an album. Attributes: type (:obj:`str`): ``document``. media (:obj:`str` | :class:`telegram.InputFile`): File to send. caption (:obj:`str`): Optional. Caption of the document to be sent. parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption. thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send. disable_content_type_detection (:obj:`bool`): Optional. Disables automatic server-side content type detection for files uploaded using multipart/form-data. Always true, if the document is sent as part of an album. """ __slots__ = ( 'caption_entities', 'media', 'thumb', 'caption', 'parse_mode', 'type', 'disable_content_type_detection', ) def __init__( self, media: Union[FileInput, Document], thumb: FileInput = None, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_content_type_detection: bool = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, filename: str = None, ): self.type = 'document' self.media = parse_file_input(media, Document, attach=True, filename=filename) if thumb: self.thumb = parse_file_input(thumb, attach=True) if caption: self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.disable_content_type_detection = disable_content_type_detection python-telegram-bot-13.11/telegram/files/location.py000066400000000000000000000073671417656324400225350ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject 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:`longitute` 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-1500. 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; 1-360. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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. 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. 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__ = ( 'longitude', 'horizontal_accuracy', 'proximity_alert_radius', 'live_period', 'latitude', 'heading', '_id_attrs', ) def __init__( self, longitude: float, latitude: float, horizontal_accuracy: float = None, live_period: int = None, heading: int = None, proximity_alert_radius: int = None, **_kwargs: Any, ): # Required self.longitude = float(longitude) self.latitude = float(latitude) # Optionals self.horizontal_accuracy = float(horizontal_accuracy) if horizontal_accuracy else None self.live_period = int(live_period) if live_period else None self.heading = int(heading) if heading else None self.proximity_alert_radius = ( int(proximity_alert_radius) if proximity_alert_radius else None ) self._id_attrs = (self.longitude, self.latitude) python-telegram-bot-13.11/telegram/files/photosize.py000066400000000000000000000070521417656324400227400ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 TYPE_CHECKING, Any from telegram import TelegramObject from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot, File class PhotoSize(TelegramObject): """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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: file_id (:obj:`str`): Identifier for this 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. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ('bot', 'width', 'file_id', 'file_size', 'height', 'file_unique_id', '_id_attrs') def __init__( self, file_id: str, file_unique_id: str, width: int, height: int, file_size: int = None, bot: 'Bot' = None, **_kwargs: Any, ): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) self.width = int(width) self.height = int(height) # Optionals self.file_size = file_size self.bot = bot self._id_attrs = (self.file_unique_id,) def get_file( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> 'File': """Convenience wrapper over :attr:`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 self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) python-telegram-bot-13.11/telegram/files/sticker.py000066400000000000000000000273241417656324400223640ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 stickers.""" from typing import TYPE_CHECKING, Any, List, Optional, ClassVar from telegram import PhotoSize, TelegramObject, constants from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot, File class Sticker(TelegramObject): """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 ``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. 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 thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the .WEBP or .JPG format. 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (obj:`dict`): Arbitrary keyword arguments. Attributes: file_id (:obj:`str`): Identifier for this 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 thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the .webp or .jpg format. 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. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ( 'bot', 'width', 'file_id', 'is_animated', 'is_video', 'file_size', 'thumb', 'set_name', 'mask_position', 'height', 'file_unique_id', 'emoji', '_id_attrs', ) def __init__( self, file_id: str, file_unique_id: str, width: int, height: int, is_animated: bool, is_video: bool, thumb: PhotoSize = None, emoji: str = None, file_size: int = None, set_name: str = None, mask_position: 'MaskPosition' = None, bot: 'Bot' = None, **_kwargs: Any, ): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) self.width = int(width) self.height = int(height) self.is_animated = is_animated self.is_video = is_video # Optionals self.thumb = thumb self.emoji = emoji self.file_size = file_size self.set_name = set_name self.mask_position = mask_position self.bot = bot self._id_attrs = (self.file_unique_id,) @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['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) data['mask_position'] = MaskPosition.de_json(data.get('mask_position'), bot) return cls(bot=bot, **data) def get_file( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> 'File': """Convenience wrapper over :attr:`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 self.bot.get_file(file_id=self.file_id, timeout=timeout, 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 ``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. 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. is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. .. versionadded:: 13.11 contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks. stickers (List[:class:`telegram.Sticker`]): List of all set stickers. thumb (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the ``.WEBP``, ``.TGS``, or ``.WEBM`` format. 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. is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. .. versionadded:: 13.11 contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks. stickers (List[:class:`telegram.Sticker`]): List of all set stickers. thumb (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the ``.WEBP``, ``.TGS`` or ``.WEBM`` format. """ __slots__ = ( 'is_animated', 'is_video', 'contains_masks', 'thumb', 'title', 'stickers', 'name', '_id_attrs', ) def __init__( self, name: str, title: str, is_animated: bool, contains_masks: bool, stickers: List[Sticker], is_video: bool, thumb: PhotoSize = None, **_kwargs: Any, ): self.name = name self.title = title self.is_animated = is_animated self.is_video = is_video self.contains_masks = contains_masks self.stickers = stickers # Optionals self.thumb = thumb self._id_attrs = (self.name,) @classmethod def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['StickerSet']: """See :meth:`telegram.TelegramObject.de_json`.""" if not data: return None data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) data['stickers'] = Sticker.de_list(data.get('stickers'), bot) return cls(bot=bot, **data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['stickers'] = [s.to_dict() for s in data.get('stickers')] return data 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. Attributes: point (:obj:`str`): The part of the face relative to which the mask should be placed. One of ``'forehead'``, ``'eyes'``, ``'mouth'``, or ``'chin'``. x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face size, from left to right. y_shift (:obj:`float`): Shift by Y-axis measured in heights of the mask scaled to the face size, from top to bottom. scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size. Note: :attr:`type` should be one of the following: `forehead`, `eyes`, `mouth` or `chin`. You can use the class constants for those. Args: point (:obj:`str`): The part of the face relative to which the mask should be placed. One of ``'forehead'``, ``'eyes'``, ``'mouth'``, or ``'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', '_id_attrs') FOREHEAD: ClassVar[str] = constants.STICKER_FOREHEAD """:const:`telegram.constants.STICKER_FOREHEAD`""" EYES: ClassVar[str] = constants.STICKER_EYES """:const:`telegram.constants.STICKER_EYES`""" MOUTH: ClassVar[str] = constants.STICKER_MOUTH """:const:`telegram.constants.STICKER_MOUTH`""" CHIN: ClassVar[str] = constants.STICKER_CHIN """:const:`telegram.constants.STICKER_CHIN`""" def __init__(self, point: str, x_shift: float, y_shift: float, scale: float, **_kwargs: Any): self.point = point self.x_shift = x_shift self.y_shift = y_shift self.scale = scale self._id_attrs = (self.point, self.x_shift, self.y_shift, self.scale) @classmethod def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['MaskPosition']: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if data is None: return None return cls(**data) python-telegram-bot-13.11/telegram/files/venue.py000066400000000000000000000076021417656324400220370ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional from telegram import Location, 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 Pace 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 `_.) **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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. google_place_id (:obj:`str`): Optional. Google Places identifier of the venue. google_place_type (:obj:`str`): Optional. Google Places type of the venue. """ __slots__ = ( 'google_place_type', 'location', 'title', 'address', 'foursquare_type', 'foursquare_id', 'google_place_id', '_id_attrs', ) def __init__( self, location: Location, title: str, address: str, foursquare_id: str = None, foursquare_type: str = None, google_place_id: str = None, google_place_type: str = None, **_kwargs: Any, ): # Required self.location = location self.title = title self.address = address # Optionals self.foursquare_id = foursquare_id self.foursquare_type = foursquare_type self.google_place_id = google_place_id self.google_place_type = google_place_type self._id_attrs = (self.location, self.title) @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 cls(**data) python-telegram-bot-13.11/telegram/files/video.py000066400000000000000000000117241417656324400220230ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 TYPE_CHECKING, Any, Optional from telegram import PhotoSize, TelegramObject from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot, File class Video(TelegramObject): """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. 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. thumb (:class:`telegram.PhotoSize`, optional): Video thumbnail. 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: file_id (:obj:`str`): Identifier for this 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. thumb (:class:`telegram.PhotoSize`): Optional. Video thumbnail. 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. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ( 'bot', 'width', 'file_id', 'file_size', 'file_name', 'thumb', 'duration', 'mime_type', 'height', 'file_unique_id', '_id_attrs', ) def __init__( self, file_id: str, file_unique_id: str, width: int, height: int, duration: int, thumb: PhotoSize = None, mime_type: str = None, file_size: int = None, bot: 'Bot' = None, file_name: str = None, **_kwargs: Any, ): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) self.width = int(width) self.height = int(height) self.duration = int(duration) # Optionals self.thumb = thumb self.file_name = file_name self.mime_type = mime_type self.file_size = file_size self.bot = bot self._id_attrs = (self.file_unique_id,) @classmethod def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Video']: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) def get_file( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> 'File': """Convenience wrapper over :attr:`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 self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) python-telegram-bot-13.11/telegram/files/videonote.py000066400000000000000000000106211417656324400227040ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 TYPE_CHECKING, Optional, Any from telegram import PhotoSize, TelegramObject from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot, File class VideoNote(TelegramObject): """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. 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. thumb (:class:`telegram.PhotoSize`, optional): Video thumbnail. file_size (:obj:`int`, optional): File size. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: file_id (:obj:`str`): Identifier for this 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 as defined by sender. duration (:obj:`int`): Duration of the video in seconds as defined by sender. thumb (:class:`telegram.PhotoSize`): Optional. Video thumbnail. file_size (:obj:`int`): Optional. File size. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ( 'bot', 'length', 'file_id', 'file_size', 'thumb', 'duration', 'file_unique_id', '_id_attrs', ) def __init__( self, file_id: str, file_unique_id: str, length: int, duration: int, thumb: PhotoSize = None, file_size: int = None, bot: 'Bot' = None, **_kwargs: Any, ): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) self.length = int(length) self.duration = int(duration) # Optionals self.thumb = thumb self.file_size = file_size self.bot = bot self._id_attrs = (self.file_unique_id,) @classmethod def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['VideoNote']: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) def get_file( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> 'File': """Convenience wrapper over :attr:`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 self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) python-telegram-bot-13.11/telegram/files/voice.py000066400000000000000000000074221417656324400220220ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 TYPE_CHECKING, Any from telegram import TelegramObject from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot, File class Voice(TelegramObject): """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`, optional): 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: file_id (:obj:`str`): Identifier for this 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. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ( 'bot', 'file_id', 'file_size', 'duration', 'mime_type', 'file_unique_id', '_id_attrs', ) def __init__( self, file_id: str, file_unique_id: str, duration: int, mime_type: str = None, file_size: int = None, bot: 'Bot' = None, **_kwargs: Any, ): # Required self.file_id = str(file_id) self.file_unique_id = str(file_unique_id) self.duration = int(duration) # Optionals self.mime_type = mime_type self.file_size = file_size self.bot = bot self._id_attrs = (self.file_unique_id,) def get_file( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> 'File': """Convenience wrapper over :attr:`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 self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) python-telegram-bot-13.11/telegram/forcereply.py000066400000000000000000000057441417656324400217720ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import ReplyMarkup class ForceReply(ReplyMarkup): """ 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. 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 (has ``reply_to_message_id``), 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; 1-64 characters. .. versionadded:: 13.7 **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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. input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input field when the reply is active. .. versionadded:: 13.7 """ __slots__ = ('selective', 'force_reply', 'input_field_placeholder', '_id_attrs') def __init__( self, force_reply: bool = True, selective: bool = False, input_field_placeholder: str = None, **_kwargs: Any, ): # Required self.force_reply = bool(force_reply) # Optionals self.selective = bool(selective) self.input_field_placeholder = input_field_placeholder self._id_attrs = (self.selective,) python-telegram-bot-13.11/telegram/games/000077500000000000000000000000001417656324400203305ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/games/__init__.py000066400000000000000000000000001417656324400224270ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/games/callbackgame.py000066400000000000000000000020621417656324400232700ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class CallbackGame(TelegramObject): """A placeholder, currently holds no information. Use BotFather to set up your game.""" __slots__ = () python-telegram-bot-13.11/telegram/games/game.py000066400000000000000000000170221417656324400216150ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" import sys from typing import TYPE_CHECKING, Any, Dict, List, Optional from telegram import Animation, MessageEntity, PhotoSize, TelegramObject 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 (List[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game message in chats. 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-4096 characters. Also found as ``telegram.constants.MAX_MESSAGE_LENGTH``. text_entities (List[:class:`telegram.MessageEntity`], optional): Special entities that appear in text, such as usernames, URLs, bot commands, etc. 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 (List[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game message in chats. 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`. text_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities that appear in text, such as usernames, URLs, bot commands, etc. animation (:class:`telegram.Animation`): Optional. Animation that will be displayed in the game message in chats. Upload via `BotFather `_. """ __slots__ = ( 'title', 'photo', 'description', 'text_entities', 'text', 'animation', '_id_attrs', ) def __init__( self, title: str, description: str, photo: List[PhotoSize], text: str = None, text_entities: List[MessageEntity] = None, animation: Animation = None, **_kwargs: Any, ): # Required self.title = title self.description = description self.photo = photo # Optionals self.text = text self.text_entities = text_entities or [] self.animation = animation self._id_attrs = (self.title, self.description, self.photo) @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 cls(**data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['photo'] = [p.to_dict() for p in self.photo] if self.text_entities: data['text_entities'] = [x.to_dict() for x in self.text_entities] return data 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'.") # Is it a narrow build, if so we don't need to convert if sys.maxunicode == 0xFFFF: return self.text[entity.offset : entity.offset + entity.length] 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: 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 ``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 ``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_text_entity(entity) for entity in (self.text_entities or []) if entity.type in types } def __hash__(self) -> int: return hash((self.title, self.description, tuple(p for p in self.photo))) python-telegram-bot-13.11/telegram/games/gamehighscore.py000066400000000000000000000044201417656324400235070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 import TelegramObject, 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', 'user', 'score', '_id_attrs') def __init__(self, position: int, user: User, score: int): self.position = position self.user = user self.score = score self._id_attrs = (self.position, self.user, self.score) @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 cls(**data) python-telegram-bot-13.11/telegram/inline/000077500000000000000000000000001417656324400205125ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/inline/__init__.py000066400000000000000000000000001417656324400226110ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/inline/inlinekeyboardbutton.py000066400000000000000000000203221417656324400253160ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any from telegram import TelegramObject if TYPE_CHECKING: from telegram import CallbackGame, LoginUrl 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` 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 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 HTTP URL used to automatically authorize the user. Can be used as a replacement for the Telegram Login Widget. callback_data (:obj:`str` | :obj:`Any`, optional): Data to be sent in a callback query to the bot when button is pressed, UTF-8 1-64 bytes. If the bot instance allows arbitrary callback data, anything can be passed. 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. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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 HTTP URL used to automatically authorize the user. Can be used as a replacement for the Telegram Login Widget. callback_data (:obj:`str` | :obj:`object`): Optional. Data to be sent in a callback query to the bot when button is pressed, UTF-8 1-64 bytes. switch_inline_query (:obj:`str`): Optional. 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. switch_inline_query_current_chat (:obj:`str`): Optional. Will insert the bot's username and the specified inline query in the current chat's input field. Can be empty, in which case just the bot’s username will be inserted. callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will be launched when the user presses the button. pay (:obj:`bool`): Optional. Specify :obj:`True`, to send a Pay button. """ __slots__ = ( 'callback_game', 'url', 'switch_inline_query_current_chat', 'callback_data', 'pay', 'switch_inline_query', 'text', '_id_attrs', 'login_url', ) def __init__( self, text: str, url: str = None, callback_data: object = None, switch_inline_query: str = None, switch_inline_query_current_chat: str = None, callback_game: 'CallbackGame' = None, pay: bool = None, login_url: 'LoginUrl' = None, **_kwargs: Any, ): # Required self.text = text # Optionals self.url = url self.login_url = login_url self.callback_data = callback_data self.switch_inline_query = switch_inline_query self.switch_inline_query_current_chat = switch_inline_query_current_chat self.callback_game = callback_game self.pay = pay self._id_attrs = () self._set_id_attrs() def _set_id_attrs(self) -> None: self._id_attrs = ( self.text, self.url, self.login_url, self.callback_data, self.switch_inline_query, self.switch_inline_query_current_chat, self.callback_game, self.pay, ) def update_callback_data(self, callback_data: 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 (:obj:`obj`): The new callback data. """ self.callback_data = callback_data self._set_id_attrs() python-telegram-bot-13.11/telegram/inline/inlinekeyboardmarkup.py000066400000000000000000000113761417656324400253130ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, List, Optional from telegram import InlineKeyboardButton, ReplyMarkup from telegram.utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot class InlineKeyboardMarkup(ReplyMarkup): """ 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 the size of :attr:`inline_keyboard` and all the buttons are equal. Args: inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): List of button rows, each represented by a list of InlineKeyboardButton objects. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): List of button rows, each represented by a list of InlineKeyboardButton objects. """ __slots__ = ('inline_keyboard', '_id_attrs') def __init__(self, inline_keyboard: List[List[InlineKeyboardButton]], **_kwargs: Any): # Required self.inline_keyboard = inline_keyboard self._id_attrs = (self.inline_keyboard,) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['inline_keyboard'] = [] for inline_keyboard in self.inline_keyboard: data['inline_keyboard'].append([x.to_dict() for x in inline_keyboard]) return data @classmethod def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['InlineKeyboardMarkup']: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) 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 **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ return cls([[button]], **kwargs) @classmethod def from_row( cls, button_row: List[InlineKeyboardButton], **kwargs: object ) -> 'InlineKeyboardMarkup': """Shortcut for:: InlineKeyboardMarkup([button_row], **kwargs) Return an InlineKeyboardMarkup from a single row of InlineKeyboardButtons Args: button_row (List[:class:`telegram.InlineKeyboardButton`]): The button to use in the markup **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ return cls([button_row], **kwargs) @classmethod def from_column( cls, button_column: List[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 (List[:class:`telegram.InlineKeyboardButton`]): The button to use in the markup **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ button_grid = [[button] for button in button_column] return cls(button_grid, **kwargs) def __hash__(self) -> int: return hash(tuple(tuple(button for button in row) for row in self.inline_keyboard)) python-telegram-bot-13.11/telegram/inline/inlinequery.py000066400000000000000000000150061417656324400234320ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0902,R0913 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional, Union, Callable, ClassVar, Sequence from telegram import Location, TelegramObject, User, constants from telegram.utils.helpers 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. Note: In Python ``from`` is a reserved word, use ``from_user`` instead. Args: id (:obj:`str`): Unique identifier for this query. from_user (:class:`telegram.User`): Sender. query (:obj:`str`): Text of the query (up to 256 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 :attr:`telegram.Chat.SENDER` for a private chat with the inline query sender, :attr:`telegram.Chat.PRIVATE`, :attr:`telegram.Chat.GROUP`, :attr:`telegram.Chat.SUPERGROUP` or :attr:`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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: id (:obj:`str`): Unique identifier for this query. from_user (:class:`telegram.User`): Sender. query (:obj:`str`): Text of the query (up to 256 characters). offset (:obj:`str`): Offset of the results to be returned, can be controlled by the bot. location (:class:`telegram.Location`): Optional. Sender location, only for bots that request user location. chat_type (:obj:`str`, optional): Type of the chat, from which the inline query was sent. .. versionadded:: 13.5 """ __slots__ = ('bot', 'location', 'chat_type', 'id', 'offset', 'from_user', 'query', '_id_attrs') def __init__( self, id: str, # pylint: disable=W0622 from_user: User, query: str, offset: str, location: Location = None, bot: 'Bot' = None, chat_type: str = None, **_kwargs: Any, ): # Required self.id = id # pylint: disable=C0103 self.from_user = from_user self.query = query self.offset = offset # Optional self.location = location self.chat_type = chat_type self.bot = bot self._id_attrs = (self.id,) @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.get('from'), bot) data['location'] = Location.de_json(data.get('location'), bot) return cls(bot=bot, **data) def answer( self, results: Union[ Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]] ], cache_time: int = 300, is_personal: bool = None, next_offset: str = None, switch_pm_text: str = None, switch_pm_parameter: str = None, timeout: ODVInput[float] = DEFAULT_NONE, current_offset: str = None, api_kwargs: JSONDict = None, auto_pagination: bool = False, ) -> bool: """Shortcut for:: 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`. Args: auto_pagination (:obj:`bool`, optional): If set to :obj:`True`, :attr:`offset` will be passed as :attr:`current_offset` to :meth:`telegram.Bot.answer_inline_query`. Defaults to :obj:`False`. Raises: TypeError: If both :attr:`current_offset` and :attr:`auto_pagination` are supplied. """ if current_offset and auto_pagination: # We raise TypeError instead of ValueError for backwards compatibility with versions # which didn't check this here but let Python do the checking raise TypeError('current_offset and auto_pagination are mutually exclusive!') return self.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, switch_pm_text=switch_pm_text, switch_pm_parameter=switch_pm_parameter, timeout=timeout, api_kwargs=api_kwargs, ) MAX_RESULTS: ClassVar[int] = constants.MAX_INLINE_QUERY_RESULTS """ :const:`telegram.constants.MAX_INLINE_QUERY_RESULTS` .. versionadded:: 13.2 """ python-telegram-bot-13.11/telegram/inline/inlinequeryresult.py000066400000000000000000000046211417656324400246720ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=W0622 """This module contains the classes that represent Telegram InlineQueryResult.""" from typing import Any from telegram import TelegramObject 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*. Args: type (:obj:`str`): Type of the result. id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): Type of the result. id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. """ __slots__ = ('type', 'id', '_id_attrs') def __init__(self, type: str, id: str, **_kwargs: Any): # Required self.type = str(type) self.id = str(id) # pylint: disable=C0103 self._id_attrs = (self.id,) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() # pylint: disable=E1101 if ( hasattr(self, 'caption_entities') and self.caption_entities # type: ignore[attr-defined] ): data['caption_entities'] = [ ce.to_dict() for ce in self.caption_entities # type: ignore[attr-defined] ] return data python-telegram-bot-13.11/telegram/inline/inlinequeryresultarticle.py000066400000000000000000000076341417656324400262450ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any from telegram import InlineQueryResult if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup class InlineQueryResultArticle(InlineQueryResult): """This object represents a Telegram InlineQueryResultArticle. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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.ReplyMarkup`, 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. thumb_url (:obj:`str`, optional): Url of the thumbnail for the result. thumb_width (:obj:`int`, optional): Thumbnail width. thumb_height (:obj:`int`, optional): Thumbnail height. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'article'. id (:obj:`str`): Unique identifier for this result, 1-64 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.ReplyMarkup`): 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. thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. thumb_width (:obj:`int`): Optional. Thumbnail width. thumb_height (:obj:`int`): Optional. Thumbnail height. """ __slots__ = ( 'reply_markup', 'thumb_width', 'thumb_height', 'hide_url', 'url', 'title', 'description', 'input_message_content', 'thumb_url', ) def __init__( self, id: str, # pylint: disable=W0622 title: str, input_message_content: 'InputMessageContent', reply_markup: 'ReplyMarkup' = None, url: str = None, hide_url: bool = None, description: str = None, thumb_url: str = None, thumb_width: int = None, thumb_height: int = None, **_kwargs: Any, ): # Required super().__init__('article', id) self.title = title self.input_message_content = input_message_content # Optional self.reply_markup = reply_markup self.url = url self.hide_url = hide_url self.description = description self.thumb_url = thumb_url self.thumb_width = thumb_width self.thumb_height = thumb_height python-telegram-bot-13.11/telegram/inline/inlinequeryresultaudio.py000066400000000000000000000116551417656324400257210ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'audio'. id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'reply_markup', 'caption_entities', 'caption', 'title', 'parse_mode', 'audio_url', 'performer', 'input_message_content', 'audio_duration', ) def __init__( self, id: str, # pylint: disable=W0622 audio_url: str, title: str, performer: str = None, audio_duration: int = None, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('audio', id) self.audio_url = audio_url self.title = title # Optionals self.performer = performer self.audio_duration = audio_duration self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inlinequeryresultcachedaudio.py000066400000000000000000000107011417656324400270400ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. audio_file_id (:obj:`str`): A valid file identifier for the audio file. caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'audio'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. audio_file_id (:obj:`str`): A valid file identifier for the audio file. caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'reply_markup', 'caption_entities', 'caption', 'parse_mode', 'audio_file_id', 'input_message_content', ) def __init__( self, id: str, # pylint: disable=W0622 audio_file_id: str, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('audio', id) self.audio_file_id = audio_file_id # Optionals self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inlinequeryresultcacheddocument.py000066400000000000000000000117351417656324400275650ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=W0622 """This module contains the classes that represent Telegram InlineQueryResultCachedDocument.""" from typing import TYPE_CHECKING, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'document'. id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'reply_markup', 'caption_entities', 'document_file_id', 'caption', 'title', 'description', 'parse_mode', 'input_message_content', ) def __init__( self, id: str, # pylint: disable=W0622 title: str, document_file_id: str, description: str = None, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('document', id) self.title = title self.document_file_id = document_file_id # Optionals self.description = description self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inlinequeryresultcachedgif.py000066400000000000000000000113701417656324400265070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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 (:obj:`str`, optional): Caption of the GIF file to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'gif'. id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'reply_markup', 'caption_entities', 'caption', 'title', 'input_message_content', 'parse_mode', 'gif_file_id', ) def __init__( self, id: str, # pylint: disable=W0622 gif_file_id: str, title: str = None, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('gif', id) self.gif_file_id = gif_file_id # Optionals self.title = title self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inlinequeryresultcachedmpeg4gif.py000066400000000000000000000114661417656324400274520ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'mpeg4_gif'. id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'reply_markup', 'caption_entities', 'mpeg4_file_id', 'caption', 'title', 'parse_mode', 'input_message_content', ) def __init__( self, id: str, # pylint: disable=W0622 mpeg4_file_id: str, title: str = None, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('mpeg4_gif', id) self.mpeg4_file_id = mpeg4_file_id # Optionals self.title = title self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inlinequeryresultcachedphoto.py000066400000000000000000000117241417656324400270760ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=W0622 """This module contains the classes that represent Telegram InlineQueryResultPhoto""" from typing import TYPE_CHECKING, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'photo'. id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'reply_markup', 'caption_entities', 'caption', 'title', 'description', 'parse_mode', 'photo_file_id', 'input_message_content', ) def __init__( self, id: str, # pylint: disable=W0622 photo_file_id: str, title: str = None, description: str = None, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('photo', id) self.photo_file_id = photo_file_id # Optionals self.title = title self.description = description self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inlinequeryresultcachedsticker.py000066400000000000000000000055431417656324400274130ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any from telegram import InlineQueryResult if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'sticker`. id (:obj:`str`): Unique identifier for this result, 1-64 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__ = ('reply_markup', 'input_message_content', 'sticker_file_id') def __init__( self, id: str, # pylint: disable=W0622 sticker_file_id: str, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, **_kwargs: Any, ): # Required super().__init__('sticker', id) self.sticker_file_id = sticker_file_id # Optionals self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inlinequeryresultcachedvideo.py000066400000000000000000000116761417656324400270610ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'video'. id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'reply_markup', 'caption_entities', 'caption', 'title', 'description', 'parse_mode', 'input_message_content', 'video_file_id', ) def __init__( self, id: str, # pylint: disable=W0622 video_file_id: str, title: str, description: str = None, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('video', id) self.video_file_id = video_file_id self.title = title # Optionals self.description = description self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inlinequeryresultcachedvoice.py000066400000000000000000000112021417656324400270410ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'voice'. id (:obj:`str`): Unique identifier for this result, 1-64 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-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'reply_markup', 'caption_entities', 'caption', 'title', 'parse_mode', 'voice_file_id', 'input_message_content', ) def __init__( self, id: str, # pylint: disable=W0622 voice_file_id: str, title: str, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('voice', id) self.voice_file_id = voice_file_id self.title = title # Optionals self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inlinequeryresultcontact.py000066400000000000000000000102601417656324400262420ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any from telegram import InlineQueryResult if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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-2048 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. thumb_url (:obj:`str`, optional): Url of the thumbnail for the result. thumb_width (:obj:`int`, optional): Thumbnail width. thumb_height (:obj:`int`, optional): Thumbnail height. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'contact'. id (:obj:`str`): Unique identifier for this result, 1-64 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-2048 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. thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. thumb_width (:obj:`int`): Optional. Thumbnail width. thumb_height (:obj:`int`): Optional. Thumbnail height. """ __slots__ = ( 'reply_markup', 'thumb_width', 'thumb_height', 'vcard', 'first_name', 'last_name', 'phone_number', 'input_message_content', 'thumb_url', ) def __init__( self, id: str, # pylint: disable=W0622 phone_number: str, first_name: str, last_name: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, thumb_url: str = None, thumb_width: int = None, thumb_height: int = None, vcard: str = None, **_kwargs: Any, ): # Required super().__init__('contact', id) self.phone_number = phone_number self.first_name = first_name # Optionals self.last_name = last_name self.vcard = vcard self.reply_markup = reply_markup self.input_message_content = input_message_content self.thumb_url = thumb_url self.thumb_width = thumb_width self.thumb_height = thumb_height python-telegram-bot-13.11/telegram/inline/inlinequeryresultdocument.py000066400000000000000000000137031417656324400264320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. title (:obj:`str`): Title for the result. caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. thumb_url (:obj:`str`, optional): URL of the thumbnail (jpeg only) for the file. thumb_width (:obj:`int`, optional): Thumbnail width. thumb_height (:obj:`int`, optional): Thumbnail height. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'document'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. title (:obj:`str`): Title for the result. caption (:obj:`str`): Optional. Caption of the document to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. thumb_url (:obj:`str`): Optional. URL of the thumbnail (jpeg only) for the file. thumb_width (:obj:`int`): Optional. Thumbnail width. thumb_height (:obj:`int`): Optional. Thumbnail height. """ __slots__ = ( 'reply_markup', 'caption_entities', 'document_url', 'thumb_width', 'thumb_height', 'caption', 'title', 'description', 'parse_mode', 'mime_type', 'thumb_url', 'input_message_content', ) def __init__( self, id: str, # pylint: disable=W0622 document_url: str, title: str, mime_type: str, caption: str = None, description: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, thumb_url: str = None, thumb_width: int = None, thumb_height: int = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('document', id) self.document_url = document_url self.title = title self.mime_type = mime_type # Optionals self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.description = description self.reply_markup = reply_markup self.input_message_content = input_message_content self.thumb_url = thumb_url self.thumb_width = thumb_width self.thumb_height = thumb_height python-telegram-bot-13.11/telegram/inline/inlinequeryresultgame.py000066400000000000000000000042311417656324400255210ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 TYPE_CHECKING, Any from telegram import InlineQueryResult if TYPE_CHECKING: from telegram import ReplyMarkup class InlineQueryResultGame(InlineQueryResult): """Represents a :class:`telegram.Game`. Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. game_short_name (:obj:`str`): Short name of the game. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'game'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. game_short_name (:obj:`str`): Short name of the game. reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. """ __slots__ = ('reply_markup', 'game_short_name') def __init__( self, id: str, # pylint: disable=W0622 game_short_name: str, reply_markup: 'ReplyMarkup' = None, **_kwargs: Any, ): # Required super().__init__('game', id) self.id = id # pylint: disable=W0622 self.game_short_name = game_short_name self.reply_markup = reply_markup python-telegram-bot-13.11/telegram/inline/inlinequeryresultgif.py000066400000000000000000000140551417656324400253620ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=W0622 """This module contains the classes that represent Telegram InlineQueryResultGif.""" from typing import TYPE_CHECKING, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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 thumb_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. thumb_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'``. title (:obj:`str`, optional): Title for the result. caption (:obj:`str`, optional): Caption of the GIF file to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'gif'. id (:obj:`str`): Unique identifier for this result, 1-64 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. thumb_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. thumb_mime_type (:obj:`str`): Optional. MIME type of the thumbnail. title (:obj:`str`): Optional. Title for the result. caption (:obj:`str`): Optional. Caption of the GIF file to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'reply_markup', 'gif_height', 'thumb_mime_type', 'caption_entities', 'gif_width', 'title', 'caption', 'parse_mode', 'gif_duration', 'input_message_content', 'gif_url', 'thumb_url', ) def __init__( self, id: str, # pylint: disable=W0622 gif_url: str, thumb_url: str, gif_width: int = None, gif_height: int = None, title: str = None, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, gif_duration: int = None, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb_mime_type: str = None, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('gif', id) self.gif_url = gif_url self.thumb_url = thumb_url # Optionals self.gif_width = gif_width self.gif_height = gif_height self.gif_duration = gif_duration self.title = title self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content self.thumb_mime_type = thumb_mime_type python-telegram-bot-13.11/telegram/inline/inlinequeryresultlocation.py000066400000000000000000000131651417656324400264260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any from telegram import InlineQueryResult if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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-1500. live_period (:obj:`int`, optional): Period in seconds for which the location can be updated, should be between 60 and 86400. heading (:obj:`int`, optional): For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 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 1 and 100000 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. thumb_url (:obj:`str`, optional): Url of the thumbnail for the result. thumb_width (:obj:`int`, optional): Thumbnail width. thumb_height (:obj:`int`, optional): Thumbnail height. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'location'. id (:obj:`str`): Unique identifier for this result, 1-64 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. live_period (:obj:`int`): Optional. Period in seconds for which the location can be updated, should be between 60 and 86400. heading (:obj:`int`): Optional. For live locations, a direction in which the user is moving, in degrees. proximity_alert_radius (:obj:`int`): Optional. For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. 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. thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. thumb_width (:obj:`int`): Optional. Thumbnail width. thumb_height (:obj:`int`): Optional. Thumbnail height. """ __slots__ = ( 'longitude', 'reply_markup', 'thumb_width', 'thumb_height', 'heading', 'title', 'live_period', 'proximity_alert_radius', 'input_message_content', 'latitude', 'horizontal_accuracy', 'thumb_url', ) def __init__( self, id: str, # pylint: disable=W0622 latitude: float, longitude: float, title: str, live_period: int = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, thumb_url: str = None, thumb_width: int = None, thumb_height: int = None, horizontal_accuracy: float = None, heading: int = None, proximity_alert_radius: int = None, **_kwargs: Any, ): # Required super().__init__('location', id) self.latitude = float(latitude) self.longitude = float(longitude) self.title = title # Optionals self.live_period = live_period self.reply_markup = reply_markup self.input_message_content = input_message_content self.thumb_url = thumb_url self.thumb_width = thumb_width self.thumb_height = thumb_height self.horizontal_accuracy = float(horizontal_accuracy) if horizontal_accuracy else None self.heading = int(heading) if heading else None self.proximity_alert_radius = ( int(proximity_alert_radius) if proximity_alert_radius else None ) python-telegram-bot-13.11/telegram/inline/inlinequeryresultmpeg4gif.py000066400000000000000000000141221417656324400263120ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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. thumb_url (:obj:`str`): URL of the static thumbnail (jpeg or gif) for the result. thumb_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'``. title (:obj:`str`, optional): Title for the result. caption (:obj:`str`, optional): Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'mpeg4_gif'. id (:obj:`str`): Unique identifier for this result, 1-64 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. thumb_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. thumb_mime_type (:obj:`str`): Optional. MIME type of the thumbnail. title (:obj:`str`): Optional. Title for the result. caption (:obj:`str`): Optional. Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'reply_markup', 'thumb_mime_type', 'caption_entities', 'mpeg4_duration', 'mpeg4_width', 'title', 'caption', 'parse_mode', 'input_message_content', 'mpeg4_url', 'mpeg4_height', 'thumb_url', ) def __init__( self, id: str, # pylint: disable=W0622 mpeg4_url: str, thumb_url: str, mpeg4_width: int = None, mpeg4_height: int = None, title: str = None, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, mpeg4_duration: int = None, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb_mime_type: str = None, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('mpeg4_gif', id) self.mpeg4_url = mpeg4_url self.thumb_url = thumb_url # Optional self.mpeg4_width = mpeg4_width self.mpeg4_height = mpeg4_height self.mpeg4_duration = mpeg4_duration self.title = title self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content self.thumb_mime_type = thumb_mime_type python-telegram-bot-13.11/telegram/inline/inlinequeryresultphoto.py000066400000000000000000000133501417656324400257430ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. photo_url (:obj:`str`): A valid URL of the photo. Photo must be in jpeg format. Photo size must not exceed 5MB. thumb_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-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'photo'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. photo_url (:obj:`str`): A valid URL of the photo. Photo must be in jpeg format. Photo size must not exceed 5MB. thumb_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-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'photo_url', 'reply_markup', 'caption_entities', 'photo_width', 'caption', 'title', 'description', 'parse_mode', 'input_message_content', 'photo_height', 'thumb_url', ) def __init__( self, id: str, # pylint: disable=W0622 photo_url: str, thumb_url: str, photo_width: int = None, photo_height: int = None, title: str = None, description: str = None, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('photo', id) self.photo_url = photo_url self.thumb_url = thumb_url # Optionals self.photo_width = int(photo_width) if photo_width is not None else None self.photo_height = int(photo_height) if photo_height is not None else None self.title = title self.description = description self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inlinequeryresultvenue.py000066400000000000000000000130061417656324400257320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any from telegram import InlineQueryResult if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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 location. thumb_url (:obj:`str`, optional): Url of the thumbnail for the result. thumb_width (:obj:`int`, optional): Thumbnail width. thumb_height (:obj:`int`, optional): Thumbnail height. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'venue'. id (:obj:`str`): Unique identifier for this result, 1-64 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. google_place_id (:obj:`str`): Optional. Google Places identifier of the venue. google_place_type (:obj:`str`): Optional. Google Places type of the venue. 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. thumb_url (:obj:`str`): Optional. Url of the thumbnail for the result. thumb_width (:obj:`int`): Optional. Thumbnail width. thumb_height (:obj:`int`): Optional. Thumbnail height. """ __slots__ = ( 'longitude', 'reply_markup', 'google_place_type', 'thumb_width', 'thumb_height', 'title', 'address', 'foursquare_id', 'foursquare_type', 'google_place_id', 'input_message_content', 'latitude', 'thumb_url', ) def __init__( self, id: str, # pylint: disable=W0622 latitude: float, longitude: float, title: str, address: str, foursquare_id: str = None, foursquare_type: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, thumb_url: str = None, thumb_width: int = None, thumb_height: int = None, google_place_id: str = None, google_place_type: str = None, **_kwargs: Any, ): # Required super().__init__('venue', id) self.latitude = latitude self.longitude = longitude self.title = title self.address = address # Optional self.foursquare_id = foursquare_id self.foursquare_type = foursquare_type self.google_place_id = google_place_id self.google_place_type = google_place_type self.reply_markup = reply_markup self.input_message_content = input_message_content self.thumb_url = thumb_url self.thumb_width = thumb_width self.thumb_height = thumb_height python-telegram-bot-13.11/telegram/inline/inlinequeryresultvideo.py000066400000000000000000000150221417656324400257160ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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`. Args: id (:obj:`str`): Unique identifier for this result, 1-64 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". thumb_url (:obj:`str`): URL of the thumbnail (jpeg only) for the video. title (:obj:`str`): Title for the result. caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'video'. id (:obj:`str`): Unique identifier for this result, 1-64 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". thumb_url (:obj:`str`): URL of the thumbnail (jpeg only) for the video. title (:obj:`str`): Title for the result. caption (:obj:`str`): Optional. Caption of the video to be sent, 0-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'video_url', 'reply_markup', 'caption_entities', 'caption', 'title', 'description', 'video_duration', 'parse_mode', 'mime_type', 'input_message_content', 'video_height', 'video_width', 'thumb_url', ) def __init__( self, id: str, # pylint: disable=W0622 video_url: str, mime_type: str, thumb_url: str, title: str, caption: str = None, video_width: int = None, video_height: int = None, video_duration: int = None, description: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('video', id) self.video_url = video_url self.mime_type = mime_type self.thumb_url = thumb_url self.title = title # Optional self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.video_width = video_width self.video_height = video_height self.video_duration = video_duration self.description = description self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inlinequeryresultvoice.py000066400000000000000000000115411417656324400257170ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Union, Tuple, List from telegram import InlineQueryResult, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import ODVInput if TYPE_CHECKING: from telegram import InputMessageContent, ReplyMarkup 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 the voice message. Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. voice_url (:obj:`str`): A valid URL for the voice recording. title (:obj:`str`): Recording title. caption (:obj:`str`, optional): Caption, 0-1024 characters after entities parsing. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): 'voice'. id (:obj:`str`): Unique identifier for this result, 1-64 bytes. voice_url (:obj:`str`): A valid URL for the voice recording. title (:obj:`str`): Recording title. caption (:obj:`str`): Optional. Caption, 0-1024 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. See the constants in :class:`telegram.ParseMode` for the available modes. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. 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__ = ( 'reply_markup', 'caption_entities', 'voice_duration', 'caption', 'title', 'voice_url', 'parse_mode', 'input_message_content', ) def __init__( self, id: str, # pylint: disable=W0622 voice_url: str, title: str, voice_duration: int = None, caption: str = None, reply_markup: 'ReplyMarkup' = None, input_message_content: 'InputMessageContent' = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required super().__init__('voice', id) self.voice_url = voice_url self.title = title # Optional self.voice_duration = voice_duration self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-13.11/telegram/inline/inputcontactmessagecontent.py000066400000000000000000000046261417656324400265470ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import InputMessageContent 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-2048 bytes. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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-2048 bytes. """ __slots__ = ('vcard', 'first_name', 'last_name', 'phone_number', '_id_attrs') def __init__( self, phone_number: str, first_name: str, last_name: str = None, vcard: str = None, **_kwargs: Any, ): # Required self.phone_number = phone_number self.first_name = first_name # Optionals self.last_name = last_name self.vcard = vcard self._id_attrs = (self.phone_number,) python-telegram-bot-13.11/telegram/inline/inputinvoicemessagecontent.py000066400000000000000000000254011417656324400265420ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any, List, Optional, TYPE_CHECKING from telegram import InputMessageContent, LabeledPrice 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, 1-32 characters description (:obj:`str`): Product description, 1-255 characters payload (:obj:`str`):Bot-defined invoice payload, 1-128 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 (List[:class:`telegram.LabeledPrice`]): Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) 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 (List[:obj:`int`], optional): A JSON-serialized 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`. provider_data (:obj:`str`, optional): A JSON-serialized 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: title (:obj:`str`): Product name, 1-32 characters description (:obj:`str`): Product description, 1-255 characters payload (:obj:`str`):Bot-defined invoice payload, 1-128 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 (List[:class:`telegram.LabeledPrice`]): Price breakdown, a JSON-serialized list of components. max_tip_amount (:obj:`int`): Optional. The maximum accepted amount for tips in the smallest units of the currency (integer, not float/double). suggested_tip_amounts (List[:obj:`int`]): Optional. A JSON-serialized array of suggested amounts of tip in the smallest units of the currency (integer, not float/double). provider_data (:obj:`str`): Optional. A JSON-serialized object for data about the invoice, which will be shared with the payment provider. photo_url (:obj:`str`): Optional. URL of the product photo for the invoice. 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__ = ( 'title', 'description', 'payload', 'provider_token', 'currency', 'prices', 'max_tip_amount', 'suggested_tip_amounts', 'provider_data', 'photo_url', 'photo_size', 'photo_width', 'photo_height', 'need_name', 'need_phone_number', 'need_email', 'need_shipping_address', 'send_phone_number_to_provider', 'send_email_to_provider', 'is_flexible', '_id_attrs', ) def __init__( self, title: str, description: str, payload: str, provider_token: str, currency: str, prices: List[LabeledPrice], max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, provider_data: str = None, photo_url: str = None, photo_size: int = None, photo_width: int = None, photo_height: int = None, need_name: bool = None, need_phone_number: bool = None, need_email: bool = None, need_shipping_address: bool = None, send_phone_number_to_provider: bool = None, send_email_to_provider: bool = None, is_flexible: bool = None, **_kwargs: Any, ): # Required self.title = title self.description = description self.payload = payload self.provider_token = provider_token self.currency = currency self.prices = prices # Optionals self.max_tip_amount = int(max_tip_amount) if max_tip_amount else None self.suggested_tip_amounts = ( [int(sta) for sta in suggested_tip_amounts] if suggested_tip_amounts else None ) self.provider_data = provider_data self.photo_url = photo_url self.photo_size = int(photo_size) if photo_size else None self.photo_width = int(photo_width) if photo_width else None self.photo_height = int(photo_height) if photo_height else None self.need_name = need_name self.need_phone_number = need_phone_number self.need_email = need_email self.need_shipping_address = need_shipping_address self.send_phone_number_to_provider = send_phone_number_to_provider self.send_email_to_provider = send_email_to_provider self.is_flexible = is_flexible self._id_attrs = ( self.title, self.description, self.payload, self.provider_token, self.currency, self.prices, ) def __hash__(self) -> int: # we override this as self.prices is a list and not hashable prices = tuple(self.prices) return hash( ( self.title, self.description, self.payload, self.provider_token, self.currency, prices, ) ) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['prices'] = [price.to_dict() for price in self.prices] return data @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 cls(**data, bot=bot) python-telegram-bot-13.11/telegram/inline/inputlocationmessagecontent.py000066400000000000000000000074271417656324400267260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import InputMessageContent 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-1500. live_period (:obj:`int`, optional): Period in seconds for which the location can be updated, should be between 60 and 86400. heading (:obj:`int`, optional): For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 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 1 and 100000 if specified. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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. live_period (:obj:`int`): Optional. Period in seconds for which the location can be updated. heading (:obj:`int`): Optional. For live locations, a direction in which the user is moving, in degrees. proximity_alert_radius (:obj:`int`): Optional. For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. """ __slots__ = ('longitude', 'horizontal_accuracy', 'proximity_alert_radius', 'live_period', 'latitude', 'heading', '_id_attrs') # fmt: on def __init__( self, latitude: float, longitude: float, live_period: int = None, horizontal_accuracy: float = None, heading: int = None, proximity_alert_radius: int = None, **_kwargs: Any, ): # Required self.latitude = latitude self.longitude = longitude # Optionals self.live_period = int(live_period) if live_period else None self.horizontal_accuracy = float(horizontal_accuracy) if horizontal_accuracy else None self.heading = int(heading) if heading else None self.proximity_alert_radius = ( int(proximity_alert_radius) if proximity_alert_radius else None ) self._id_attrs = (self.latitude, self.longitude) python-telegram-bot-13.11/telegram/inline/inputmessagecontent.py000066400000000000000000000024641417656324400251710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject 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__ = () python-telegram-bot-13.11/telegram/inline/inputtextmessagecontent.py000066400000000000000000000074521417656324400261000ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any, Union, Tuple, List from telegram import InputMessageContent, MessageEntity from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput 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. Args: message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters after entities parsing. Also found as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`. parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. See the constants in :class:`telegram.ParseMode` for the available modes. entities (List[:class:`telegram.MessageEntity`], optional): List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in the sent message. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters after entities parsing. parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. See the constants in :class:`telegram.ParseMode` for the available modes. entities (List[:class:`telegram.MessageEntity`]): Optional. List of special entities that appear in the caption, which can be specified instead of :attr:`parse_mode`. disable_web_page_preview (:obj:`bool`): Optional. Disables link previews for links in the sent message. """ __slots__ = ('disable_web_page_preview', 'parse_mode', 'entities', 'message_text', '_id_attrs') def __init__( self, message_text: str, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None, **_kwargs: Any, ): # Required self.message_text = message_text # Optionals self.parse_mode = parse_mode self.entities = entities self.disable_web_page_preview = disable_web_page_preview self._id_attrs = (self.message_text,) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() if self.entities: data['entities'] = [ce.to_dict() for ce in self.entities] return data python-telegram-bot-13.11/telegram/inline/inputvenuemessagecontent.py000066400000000000000000000076151417656324400262370ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import InputMessageContent 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 `_.) **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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. google_place_id (:obj:`str`): Optional. Google Places identifier of the venue. google_place_type (:obj:`str`): Optional. Google Places type of the venue. """ __slots__ = ( 'longitude', 'google_place_type', 'title', 'address', 'foursquare_id', 'foursquare_type', 'google_place_id', 'latitude', '_id_attrs', ) def __init__( self, latitude: float, longitude: float, title: str, address: str, foursquare_id: str = None, foursquare_type: str = None, google_place_id: str = None, google_place_type: str = None, **_kwargs: Any, ): # Required self.latitude = latitude self.longitude = longitude self.title = title self.address = address # Optionals self.foursquare_id = foursquare_id self.foursquare_type = foursquare_type self.google_place_id = google_place_id self.google_place_type = google_place_type self._id_attrs = ( self.latitude, self.longitude, self.title, ) python-telegram-bot-13.11/telegram/keyboardbutton.py000066400000000000000000000070361417656324400226500ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject, KeyboardButtonPollType class KeyboardButton(TelegramObject): """ This object represents one button of the reply keyboard. For simple text buttons String 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` and :attr:`request_poll` 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 ignore them. * :attr:`request_poll` option will only work in Telegram versions released after 23 January, 2020. Older clients will receive unsupported message. 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:`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. Attributes: text (:obj:`str`): Text of the button. request_contact (:obj:`bool`): Optional. The user's phone number will be sent. request_location (:obj:`bool`): Optional. The user's current location will be sent. request_poll (:class:`KeyboardButtonPollType`): Optional. If the user should create a poll. """ __slots__ = ('request_location', 'request_contact', 'request_poll', 'text', '_id_attrs') def __init__( self, text: str, request_contact: bool = None, request_location: bool = None, request_poll: KeyboardButtonPollType = None, **_kwargs: Any, ): # Required self.text = text # Optionals self.request_contact = request_contact self.request_location = request_location self.request_poll = request_poll self._id_attrs = ( self.text, self.request_contact, self.request_location, self.request_poll, ) python-telegram-bot-13.11/telegram/keyboardbuttonpolltype.py000066400000000000000000000034551417656324400244420ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject 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. Attributes: type (:obj:`str`): Optional. If :attr:`telegram.Poll.QUIZ` is passed, the user will be allowed to create only polls in the quiz mode. If :attr:`telegram.Poll.REGULAR` is passed, only regular polls will be allowed. Otherwise, the user will be allowed to create a poll of any type. """ __slots__ = ('type', '_id_attrs') def __init__(self, type: str = None, **_kwargs: Any): # pylint: disable=W0622 self.type = type self._id_attrs = (self.type,) python-telegram-bot-13.11/telegram/loginurl.py000066400000000000000000000077761417656324400214620ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject 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 HTTP 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 HTTP URL to be opened with user 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. 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', 'request_write_access', 'url', 'forward_text', '_id_attrs') def __init__( self, url: str, forward_text: bool = None, bot_username: str = None, request_write_access: bool = None, **_kwargs: Any, ): # Required self.url = url # Optional self.forward_text = forward_text self.bot_username = bot_username self.request_write_access = request_write_access self._id_attrs = (self.url,) python-telegram-bot-13.11/telegram/message.py000066400000000000000000003630621417656324400212440ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0902,R0913 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 sys from html import escape from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, ClassVar, Tuple from telegram import ( Animation, Audio, Chat, Contact, Dice, Document, Game, InlineKeyboardMarkup, Invoice, Location, MessageEntity, ParseMode, PassportData, PhotoSize, Poll, Sticker, SuccessfulPayment, TelegramObject, User, Venue, Video, VideoNote, Voice, VoiceChatStarted, VoiceChatEnded, VoiceChatParticipantsInvited, ProximityAlertTriggered, ReplyMarkup, MessageAutoDeleteTimerChanged, VoiceChatScheduled, ) from telegram.utils.helpers import ( escape_markdown, from_timestamp, to_timestamp, DEFAULT_NONE, DEFAULT_20, ) from telegram.utils.types import JSONDict, FileInput, ODVInput, DVInput if TYPE_CHECKING: from telegram import ( Bot, GameHighScore, InputMedia, MessageId, InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo, LabeledPrice, ) class Message(TelegramObject): # 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 ``from`` is a reserved word, use ``from_user`` instead. 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`. chat (:class:`telegram.Chat`): Conversation the message belongs to. forward_from (:class:`telegram.User`, optional): For forwarded messages, sender of the original message. forward_from_chat (:class:`telegram.Chat`, optional): For messages forwarded from channels or from anonymous administrators, information about the original sender chat. forward_from_message_id (:obj:`int`, optional): For forwarded channel posts, identifier of the original message in the channel. forward_sender_name (:obj:`str`, optional): Sender's name for messages forwarded from users who disallow adding a link to their account in forwarded messages. forward_date (:class:`datetime.datetime`, optional): For forwarded messages, date the original message was sent in Unix time. Converted to :class:`datetime.datetime`. 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. edit_date (:class:`datetime.datetime`, optional): Date the message was last edited in Unix time. Converted to :class:`datetime.datetime`. has_protected_content (:obj:`bool`, optional): :obj:`True`, if the message can't be forwarded. .. versionadded:: 13.9 media_group_id (:obj:`str`, optional): The unique identifier of a media message group this message belongs to. text (str, optional): For text messages, the actual UTF-8 text of the message, 0-4096 characters. Also found as :attr:`telegram.constants.MAX_MESSAGE_LENGTH`. entities (List[: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. caption_entities (List[: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. 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 (List[: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. 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 (List[: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). caption (:obj:`str`, optional): Caption for the animation, audio, document, photo, video or voice, 0-1024 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 (List[:class:`telegram.PhotoSize`], optional): A chat photo was changed to this value. 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. 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. migrate_from_chat_id (:obj:`int`, optional): The supergroup has been migrated from a group with the specified identifier. 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. pinned_message (:class:`telegram.Message`, 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. 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. forward_signature (:obj:`str`, optional): For messages forwarded from channels, signature of the post author if present. 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 from 1 to 6. via_bot (:class:`telegram.User`, optional): Message was sent through an inline bot. proximity_alert_triggered (:class:`telegram.ProximityAlertTriggered`, optional): Service message. A user in the chat triggered another user's proximity alert while sharing Live Location. voice_chat_scheduled (:class:`telegram.VoiceChatScheduled`, optional): Service message: voice chat scheduled. .. versionadded:: 13.5 voice_chat_started (:class:`telegram.VoiceChatStarted`, optional): Service message: voice chat started. .. versionadded:: 13.4 voice_chat_ended (:class:`telegram.VoiceChatEnded`, optional): Service message: voice chat ended. .. versionadded:: 13.4 voice_chat_participants_invited (:class:`telegram.VoiceChatParticipantsInvited` optional): Service message: new participants invited to a voice chat. .. versionadded:: 13.4 reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. ``login_url`` buttons are represented as ordinary url buttons. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. 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 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. chat (:class:`telegram.Chat`): Conversation the message belongs to. forward_from (:class:`telegram.User`): Optional. Sender of the original message. forward_from_chat (:class:`telegram.Chat`): Optional. For messages forwarded from channels or from anonymous administrators, information about the original sender chat. forward_from_message_id (:obj:`int`): Optional. Identifier of the original message in the channel. forward_date (:class:`datetime.datetime`): Optional. Date the original message was sent. 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. has_protected_content (:obj:`bool`): Optional. :obj:`True`, if the message can't be forwarded. .. versionadded:: 13.9 media_group_id (:obj:`str`): Optional. The unique identifier of a media message group this message belongs to. text (:obj:`str`): Optional. The actual UTF-8 text of the message. entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like usernames, URLs, bot commands, etc. that appear in the text. See :attr:`Message.parse_entity` and :attr:`parse_entities` methods for how to use properly. caption_entities (List[:class:`telegram.MessageEntity`]): Optional. 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. audio (:class:`telegram.Audio`): Optional. Information about the file. document (:class:`telegram.Document`): Optional. Information about the file. animation (:class:`telegram.Animation`) Optional. Information about the file. For backward compatibility, when this field is set, the document field will also be set. game (:class:`telegram.Game`): Optional. Information about the game. photo (List[:class:`telegram.PhotoSize`]): Optional. Available sizes of the photo. sticker (:class:`telegram.Sticker`): Optional. Information about the sticker. video (:class:`telegram.Video`): Optional. Information about the video. voice (:class:`telegram.Voice`): Optional. Information about the file. video_note (:class:`telegram.VideoNote`): Optional. Information about the video message. new_chat_members (List[:class:`telegram.User`]): Optional. Information about new members to the chat. (the bot itself may be one of these members). caption (:obj:`str`): Optional. Caption for the document, photo or video, 0-1024 characters. contact (:class:`telegram.Contact`): Optional. Information about the contact. location (:class:`telegram.Location`): Optional. Information about the location. venue (:class:`telegram.Venue`): Optional. Information about the venue. left_chat_member (:class:`telegram.User`): Optional. Information about the user that left the group. (this member may be the bot itself). new_chat_title (:obj:`str`): Optional. A chat title was changed to this value. new_chat_photo (List[:class:`telegram.PhotoSize`]): Optional. A chat photo was changed to this value. delete_chat_photo (:obj:`bool`): Optional. The chat photo was deleted. group_chat_created (:obj:`bool`): Optional. The group has been created. supergroup_chat_created (:obj:`bool`): Optional. The supergroup has been created. channel_chat_created (:obj:`bool`): Optional. The channel has been created. 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.message`): Optional. Specified message was pinned. invoice (:class:`telegram.Invoice`): Optional. Information about the invoice. successful_payment (:class:`telegram.SuccessfulPayment`): Optional. Information about the payment. connected_website (:obj:`str`): Optional. The domain name of the website on which the user has logged in. forward_signature (:obj:`str`): Optional. Signature of the post author for messages forwarded from channels. forward_sender_name (:obj:`str`): Optional. Sender's name for messages forwarded from users who disallow adding a link to their account in forwarded messages. 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. via_bot (:class:`telegram.User`): Optional. Bot through which the 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. voice_chat_scheduled (:class:`telegram.VoiceChatScheduled`): Optional. Service message: voice chat scheduled. .. versionadded:: 13.5 voice_chat_started (:class:`telegram.VoiceChatStarted`): Optional. Service message: voice chat started. .. versionadded:: 13.4 voice_chat_ended (:class:`telegram.VoiceChatEnded`): Optional. Service message: voice chat ended. .. versionadded:: 13.4 voice_chat_participants_invited (:class:`telegram.VoiceChatParticipantsInvited`): Optional. Service message: new participants invited to a voice chat. .. versionadded:: 13.4 reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ # fmt: on __slots__ = ( 'reply_markup', 'audio', 'contact', 'migrate_to_chat_id', 'forward_signature', 'chat', 'successful_payment', 'game', 'text', 'forward_sender_name', 'document', 'new_chat_title', 'forward_date', 'group_chat_created', 'media_group_id', 'caption', 'video', 'bot', 'entities', 'via_bot', 'new_chat_members', 'connected_website', 'animation', 'migrate_from_chat_id', 'forward_from', 'sticker', 'location', 'venue', 'edit_date', 'reply_to_message', 'passport_data', 'pinned_message', 'forward_from_chat', 'new_chat_photo', 'message_id', 'delete_chat_photo', 'from_user', 'author_signature', 'proximity_alert_triggered', 'sender_chat', 'dice', 'forward_from_message_id', 'caption_entities', 'voice', 'date', 'supergroup_chat_created', 'poll', 'left_chat_member', 'photo', 'channel_chat_created', 'invoice', 'video_note', '_effective_attachment', 'message_auto_delete_timer_changed', 'voice_chat_ended', 'voice_chat_participants_invited', 'voice_chat_started', 'voice_chat_scheduled', 'is_automatic_forward', 'has_protected_content', '_id_attrs', ) ATTACHMENT_TYPES: ClassVar[List[str]] = [ 'audio', 'game', 'animation', 'document', 'photo', 'sticker', 'video', 'voice', 'video_note', 'contact', 'location', 'venue', 'invoice', 'successful_payment', ] MESSAGE_TYPES: ClassVar[List[str]] = [ 'text', 'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created', 'supergroup_chat_created', 'channel_chat_created', 'message_auto_delete_timer_changed', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message', 'poll', 'dice', 'passport_data', 'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended', 'voice_chat_participants_invited', ] + ATTACHMENT_TYPES def __init__( self, message_id: int, date: datetime.datetime, chat: Chat, from_user: User = None, forward_from: User = None, forward_from_chat: Chat = None, forward_from_message_id: int = None, forward_date: datetime.datetime = None, reply_to_message: 'Message' = None, edit_date: datetime.datetime = None, text: str = None, entities: List['MessageEntity'] = None, caption_entities: List['MessageEntity'] = None, audio: Audio = None, document: Document = None, game: Game = None, photo: List[PhotoSize] = None, sticker: Sticker = None, video: Video = None, voice: Voice = None, video_note: VideoNote = None, new_chat_members: List[User] = None, caption: str = None, contact: Contact = None, location: Location = None, venue: Venue = None, left_chat_member: User = None, new_chat_title: str = None, new_chat_photo: List[PhotoSize] = None, delete_chat_photo: bool = False, group_chat_created: bool = False, supergroup_chat_created: bool = False, channel_chat_created: bool = False, migrate_to_chat_id: int = None, migrate_from_chat_id: int = None, pinned_message: 'Message' = None, invoice: Invoice = None, successful_payment: SuccessfulPayment = None, forward_signature: str = None, author_signature: str = None, media_group_id: str = None, connected_website: str = None, animation: Animation = None, passport_data: PassportData = None, poll: Poll = None, forward_sender_name: str = None, reply_markup: InlineKeyboardMarkup = None, bot: 'Bot' = None, dice: Dice = None, via_bot: User = None, proximity_alert_triggered: ProximityAlertTriggered = None, sender_chat: Chat = None, voice_chat_started: VoiceChatStarted = None, voice_chat_ended: VoiceChatEnded = None, voice_chat_participants_invited: VoiceChatParticipantsInvited = None, message_auto_delete_timer_changed: MessageAutoDeleteTimerChanged = None, voice_chat_scheduled: VoiceChatScheduled = None, is_automatic_forward: bool = None, has_protected_content: bool = None, **_kwargs: Any, ): # Required self.message_id = int(message_id) # Optionals self.from_user = from_user self.sender_chat = sender_chat self.date = date self.chat = chat self.forward_from = forward_from self.forward_from_chat = forward_from_chat self.forward_date = forward_date self.is_automatic_forward = is_automatic_forward self.reply_to_message = reply_to_message self.edit_date = edit_date self.has_protected_content = has_protected_content self.text = text self.entities = entities or [] self.caption_entities = caption_entities or [] self.audio = audio self.game = game self.document = document self.photo = photo or [] self.sticker = sticker self.video = video self.voice = voice self.video_note = video_note self.caption = caption self.contact = contact self.location = location self.venue = venue self.new_chat_members = new_chat_members or [] self.left_chat_member = left_chat_member self.new_chat_title = new_chat_title self.new_chat_photo = new_chat_photo or [] self.delete_chat_photo = bool(delete_chat_photo) self.group_chat_created = bool(group_chat_created) self.supergroup_chat_created = bool(supergroup_chat_created) self.migrate_to_chat_id = migrate_to_chat_id self.migrate_from_chat_id = migrate_from_chat_id self.channel_chat_created = bool(channel_chat_created) self.message_auto_delete_timer_changed = message_auto_delete_timer_changed self.pinned_message = pinned_message self.forward_from_message_id = forward_from_message_id self.invoice = invoice self.successful_payment = successful_payment self.connected_website = connected_website self.forward_signature = forward_signature self.forward_sender_name = forward_sender_name self.author_signature = author_signature self.media_group_id = media_group_id self.animation = animation self.passport_data = passport_data self.poll = poll self.dice = dice self.via_bot = via_bot self.proximity_alert_triggered = proximity_alert_triggered self.voice_chat_scheduled = voice_chat_scheduled self.voice_chat_started = voice_chat_started self.voice_chat_ended = voice_chat_ended self.voice_chat_participants_invited = voice_chat_participants_invited self.reply_markup = reply_markup self.bot = bot 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 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. """ if self.chat.type not in [Chat.PRIVATE, Chat.GROUP]: if self.chat.username: to_link = self.chat.username else: # Get rid of leading -100 for supergroups to_link = f"c/{str(self.chat.id)[4:]}" return f"https://t.me/{to_link}/{self.message_id}" 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 data['from_user'] = User.de_json(data.get('from'), bot) data['sender_chat'] = Chat.de_json(data.get('sender_chat'), bot) data['date'] = from_timestamp(data['date']) data['chat'] = Chat.de_json(data.get('chat'), bot) data['entities'] = MessageEntity.de_list(data.get('entities'), bot) data['caption_entities'] = MessageEntity.de_list(data.get('caption_entities'), bot) data['forward_from'] = User.de_json(data.get('forward_from'), bot) data['forward_from_chat'] = Chat.de_json(data.get('forward_from_chat'), bot) data['forward_date'] = from_timestamp(data.get('forward_date')) data['reply_to_message'] = Message.de_json(data.get('reply_to_message'), bot) data['edit_date'] = from_timestamp(data.get('edit_date')) 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['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'] = Message.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['voice_chat_scheduled'] = VoiceChatScheduled.de_json( data.get('voice_chat_scheduled'), bot ) data['voice_chat_started'] = VoiceChatStarted.de_json(data.get('voice_chat_started'), bot) data['voice_chat_ended'] = VoiceChatEnded.de_json(data.get('voice_chat_ended'), bot) data['voice_chat_participants_invited'] = VoiceChatParticipantsInvited.de_json( data.get('voice_chat_participants_invited'), bot ) return cls(bot=bot, **data) @property def effective_attachment( self, ) -> Union[ Contact, Document, Animation, Game, Invoice, Location, List[PhotoSize], Sticker, SuccessfulPayment, Venue, Video, VideoNote, Voice, None, ]: """ :class:`telegram.Audio` or :class:`telegram.Contact` or :class:`telegram.Document` or :class:`telegram.Animation` or :class:`telegram.Game` or :class:`telegram.Invoice` or :class:`telegram.Location` or List[:class:`telegram.PhotoSize`] or :class:`telegram.Sticker` or :class:`telegram.SuccessfulPayment` or :class:`telegram.Venue` or :class:`telegram.Video` or :class:`telegram.VideoNote` or :class:`telegram.Voice`: The attachment that this message was sent with. May be :obj:`None` if no attachment was sent. """ if self._effective_attachment is not DEFAULT_NONE: return self._effective_attachment # type: ignore for i in Message.ATTACHMENT_TYPES: if getattr(self, i, None): self._effective_attachment = getattr(self, i) break else: self._effective_attachment = None return self._effective_attachment # type: ignore def __getitem__(self, item: str) -> Any: # pylint: disable=R1710 return self.chat.id if item == 'chat_id' else super().__getitem__(item) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() # Required data['date'] = to_timestamp(self.date) # Optionals if self.forward_date: data['forward_date'] = to_timestamp(self.forward_date) if self.edit_date: data['edit_date'] = to_timestamp(self.edit_date) if self.photo: data['photo'] = [p.to_dict() for p in self.photo] if self.entities: data['entities'] = [e.to_dict() for e in self.entities] if self.caption_entities: data['caption_entities'] = [e.to_dict() for e in self.caption_entities] if self.new_chat_photo: data['new_chat_photo'] = [p.to_dict() for p in self.new_chat_photo] if self.new_chat_members: data['new_chat_members'] = [u.to_dict() for u in self.new_chat_members] return data def _quote(self, quote: Optional[bool], reply_to_message_id: Optional[int]) -> Optional[int]: """Modify kwargs for replying with or without quoting.""" if reply_to_message_id is not None: return reply_to_message_id if quote is not None: if quote: return self.message_id else: if self.bot.defaults: default_quote = self.bot.defaults.quote else: default_quote = None if (default_quote is None and self.chat.type != Chat.PRIVATE) or default_quote: return self.message_id return None def reply_text( self, text: str, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_message(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_message( chat_id=self.chat_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, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, ) def reply_markdown( self, text: str, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_message( update.effective_message.chat_id, parse_mode=ParseMode.MARKDOWN, *args, **kwargs, ) Sends a message with Markdown version 1 formatting. For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. Note: :attr:`telegram.ParseMode.MARKDOWN` is a legacy mode, retained by Telegram for backward compatibility. You should use :meth:`reply_markdown_v2` instead. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_message( chat_id=self.chat_id, text=text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, ) def reply_markdown_v2( self, text: str, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_message( update.effective_message.chat_id, parse_mode=ParseMode.MARKDOWN_V2, *args, **kwargs, ) Sends a message with markdown version 2 formatting. For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_message( chat_id=self.chat_id, text=text, parse_mode=ParseMode.MARKDOWN_V2, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, ) def reply_html( self, text: str, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_message( update.effective_message.chat_id, parse_mode=ParseMode.HTML, *args, **kwargs, ) Sends a message with HTML formatting. For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the message is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_message( chat_id=self.chat_id, text=text, parse_mode=ParseMode.HTML, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, ) def reply_media_group( self, media: List[ Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo'] ], disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, ) -> List['Message']: """Shortcut for:: bot.send_media_group(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the media group is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: List[:class:`telegram.Message`]: An array of the sent Messages. Raises: :class:`telegram.error.TelegramError` """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_media_group( chat_id=self.chat_id, media=media, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def reply_photo( self, photo: Union[FileInput, 'PhotoSize'], caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_photo(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the photo is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_photo( chat_id=self.chat_id, photo=photo, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=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, ) def reply_audio( self, audio: Union[FileInput, 'Audio'], duration: int = None, performer: str = None, title: str = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_audio(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the audio is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_audio( chat_id=self.chat_id, audio=audio, duration=duration, performer=performer, title=title, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, parse_mode=parse_mode, thumb=thumb, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, filename=filename, protect_content=protect_content, ) def reply_document( self, document: Union[FileInput, 'Document'], filename: str = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb: FileInput = None, api_kwargs: JSONDict = None, disable_content_type_detection: bool = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_document(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the document is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_document( chat_id=self.chat_id, document=document, filename=filename, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, parse_mode=parse_mode, thumb=thumb, 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, ) def reply_animation( self, animation: Union[FileInput, 'Animation'], duration: int = None, width: int = None, height: int = None, thumb: FileInput = None, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_animation(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the animation is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_animation( chat_id=self.chat_id, animation=animation, duration=duration, width=width, height=height, thumb=thumb, caption=caption, parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, filename=filename, protect_content=protect_content, ) def reply_sticker( self, sticker: Union[FileInput, 'Sticker'], disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_sticker(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the sticker is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_sticker( chat_id=self.chat_id, sticker=sticker, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def reply_video( self, video: Union[FileInput, 'Video'], duration: int = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, width: int = None, height: int = None, parse_mode: ODVInput[str] = DEFAULT_NONE, supports_streaming: bool = None, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_video(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the video is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_video( chat_id=self.chat_id, video=video, duration=duration, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, width=width, height=height, parse_mode=parse_mode, supports_streaming=supports_streaming, thumb=thumb, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, filename=filename, protect_content=protect_content, ) def reply_video_note( self, video_note: Union[FileInput, 'VideoNote'], duration: int = None, length: int = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: str = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_video_note(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the video note is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_video_note( chat_id=self.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, timeout=timeout, thumb=thumb, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, ) def reply_voice( self, voice: Union[FileInput, 'Voice'], duration: int = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_voice(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the voice note is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_voice( chat_id=self.chat_id, voice=voice, duration=duration, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=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, ) def reply_location( self, latitude: float = None, longitude: float = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, location: Location = None, live_period: int = None, api_kwargs: JSONDict = None, horizontal_accuracy: float = None, heading: int = None, proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_location(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the location is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_location( chat_id=self.chat_id, latitude=latitude, longitude=longitude, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=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, ) def reply_venue( self, latitude: float = None, longitude: float = None, title: str = None, address: str = None, foursquare_id: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, venue: Venue = None, foursquare_type: str = None, api_kwargs: JSONDict = None, google_place_id: str = None, google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_venue(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the venue is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_venue( chat_id=self.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, timeout=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, ) def reply_contact( self, phone_number: str = None, first_name: str = None, last_name: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, contact: Contact = None, vcard: str = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_contact(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the contact is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_contact( chat_id=self.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, timeout=timeout, contact=contact, vcard=vcard, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def reply_poll( self, question: str, options: List[str], is_anonymous: bool = True, type: str = Poll.REGULAR, # pylint: disable=W0622 allows_multiple_answers: bool = False, correct_option_id: int = None, is_closed: bool = None, disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, explanation: str = None, explanation_parse_mode: ODVInput[str] = DEFAULT_NONE, open_period: int = None, close_date: Union[int, datetime.datetime] = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_poll(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the poll is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_poll( chat_id=self.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, timeout=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, ) def reply_dice( self, disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, emoji: str = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_dice(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the dice is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_dice( chat_id=self.chat_id, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, emoji=emoji, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def reply_chat_action( self, action: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: bot.send_chat_action(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`. .. versionadded:: 13.2 Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.bot.send_chat_action( chat_id=self.chat_id, action=action, timeout=timeout, api_kwargs=api_kwargs, ) def reply_game( self, game_short_name: str, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'InlineKeyboardMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_game(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the game is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. .. versionadded:: 13.2 Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_game( chat_id=self.chat_id, game_short_name=game_short_name, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def reply_invoice( self, title: str, description: str, payload: str, provider_token: str, currency: str, prices: List['LabeledPrice'], start_parameter: str = None, photo_url: str = None, photo_size: int = None, photo_width: int = None, photo_height: int = None, need_name: bool = None, need_phone_number: bool = None, need_email: bool = None, need_shipping_address: bool = None, is_flexible: bool = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'InlineKeyboardMarkup' = None, provider_data: Union[str, object] = None, send_phone_number_to_provider: bool = None, send_email_to_provider: bool = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_invoice(update.effective_message.chat_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`. Warning: As of API 5.2 :attr:`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 :attr:`start_parameter` is optional. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the invoice is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.send_invoice( chat_id=self.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, timeout=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, ) def forward( self, chat_id: Union[int, str], disable_notification: DVInput[bool] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.forward_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.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 self.bot.forward_message( chat_id=chat_id, from_chat_id=self.chat_id, message_id=self.message_id, disable_notification=disable_notification, timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) def copy( self, chat_id: Union[int, str], caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, ) -> 'MessageId': """Shortcut for:: 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 self.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, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) def reply_copy( self, from_chat_id: Union[str, int], message_id: int, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, quote: bool = None, protect_content: bool = None, ) -> 'MessageId': """Shortcut for:: bot.copy_message(chat_id=message.chat.id, from_chat_id=from_chat_id, message_id=message_id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. Args: quote (:obj:`bool`, optional): If set to :obj:`True`, the copy is sent as an actual reply to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats. .. versionadded:: 13.1 Returns: :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. """ reply_to_message_id = self._quote(quote, reply_to_message_id) return self.bot.copy_message( chat_id=self.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=reply_markup, timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) def edit_text( self, text: str, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, ) -> Union['Message', bool]: """Shortcut for:: 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 self.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, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, entities=entities, inline_message_id=None, ) def edit_caption( self, caption: str = None, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, parse_mode: ODVInput[str] = DEFAULT_NONE, api_kwargs: JSONDict = None, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, ) -> Union['Message', bool]: """Shortcut for:: 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 self.bot.edit_message_caption( chat_id=self.chat_id, message_id=self.message_id, caption=caption, reply_markup=reply_markup, timeout=timeout, parse_mode=parse_mode, api_kwargs=api_kwargs, caption_entities=caption_entities, inline_message_id=None, ) def edit_media( self, media: 'InputMedia' = None, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Union['Message', bool]: """Shortcut for:: 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 sent by the bot, the edited Message is returned, otherwise ``True`` is returned. """ return self.bot.edit_message_media( chat_id=self.chat_id, message_id=self.message_id, media=media, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, inline_message_id=None, ) def edit_reply_markup( self, reply_markup: Optional['InlineKeyboardMarkup'] = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Union['Message', bool]: """Shortcut for:: 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 self.bot.edit_message_reply_markup( chat_id=self.chat_id, message_id=self.message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, inline_message_id=None, ) def edit_live_location( self, latitude: float = None, longitude: float = None, location: Location = None, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, horizontal_accuracy: float = None, heading: int = None, proximity_alert_radius: int = None, ) -> Union['Message', bool]: """Shortcut for:: 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 self.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, timeout=timeout, api_kwargs=api_kwargs, horizontal_accuracy=horizontal_accuracy, heading=heading, proximity_alert_radius=proximity_alert_radius, inline_message_id=None, ) def stop_live_location( self, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Union['Message', bool]: """Shortcut for:: 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 self.bot.stop_message_live_location( chat_id=self.chat_id, message_id=self.message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, inline_message_id=None, ) def set_game_score( self, user_id: Union[int, str], score: int, force: bool = None, disable_edit_message: bool = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Union['Message', bool]: """Shortcut for:: 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 self.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, timeout=timeout, api_kwargs=api_kwargs, inline_message_id=None, ) def get_game_high_scores( self, user_id: Union[int, str], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> List['GameHighScore']: """Shortcut for:: 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: List[:class:`telegram.GameHighScore`] """ return self.bot.get_game_high_scores( chat_id=self.chat_id, message_id=self.message_id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs, inline_message_id=None, ) def delete( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.delete_message( chat_id=self.chat_id, message_id=self.message_id, timeout=timeout, api_kwargs=api_kwargs, ) def stop_poll( self, reply_markup: InlineKeyboardMarkup = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Poll: """Shortcut for:: 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 self.bot.stop_poll( chat_id=self.chat_id, message_id=self.message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, ) def pin( self, disable_notification: ODVInput[bool] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.pin_chat_message( chat_id=self.chat_id, message_id=self.message_id, disable_notification=disable_notification, timeout=timeout, api_kwargs=api_kwargs, ) def unpin( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.unpin_chat_message( chat_id=self.chat_id, message_id=self.message_id, timeout=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'.") # Is it a narrow build, if so we don't need to convert if sys.maxunicode == 0xFFFF: return self.text[entity.offset : entity.offset + entity.length] 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'.") # Is it a narrow build, if so we don't need to convert if sys.maxunicode == 0xFFFF: return self.caption[entity.offset : entity.offset + entity.length] 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: 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 or []) if entity.type in types } def parse_caption_entities(self, types: 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 or []) if entity.type in types } @staticmethod def _parse_html( message_text: Optional[str], entities: Dict[MessageEntity, str], urled: bool = False, offset: int = 0, ) -> Optional[str]: if message_text is None: return None if sys.maxunicode != 0xFFFF: message_text = message_text.encode('utf-16-le') # type: ignore 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 not in parsed_entities: 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())) orig_text = text text = escape(text) if nested_entities: text = Message._parse_html( orig_text, nested_entities, urled=urled, offset=entity.offset ) if entity.type == MessageEntity.TEXT_LINK: insert = f'{text}' elif entity.type == MessageEntity.TEXT_MENTION and entity.user: insert = f'{text}' elif entity.type == MessageEntity.URL and urled: insert = f'{text}' elif entity.type == MessageEntity.BOLD: insert = '' + text + '' elif entity.type == MessageEntity.ITALIC: insert = '' + text + '' elif entity.type == MessageEntity.CODE: insert = '' + text + '' elif entity.type == MessageEntity.PRE: if entity.language: insert = f'
{text}
' else: insert = '
' + text + '
' elif entity.type == MessageEntity.UNDERLINE: insert = '' + text + '' elif entity.type == MessageEntity.STRIKETHROUGH: insert = '' + text + '' elif entity.type == MessageEntity.SPOILER: insert = f'{text}' else: insert = text if offset == 0: if sys.maxunicode == 0xFFFF: html_text += ( escape(message_text[last_offset : entity.offset - offset]) + insert ) else: html_text += ( escape( message_text[ # type: ignore last_offset * 2 : (entity.offset - offset) * 2 ].decode('utf-16-le') ) + insert ) else: if sys.maxunicode == 0xFFFF: html_text += message_text[last_offset : entity.offset - offset] + insert else: html_text += ( message_text[ # type: ignore last_offset * 2 : (entity.offset - offset) * 2 ].decode('utf-16-le') + insert ) last_offset = entity.offset - offset + entity.length if offset == 0: if sys.maxunicode == 0xFFFF: html_text += escape(message_text[last_offset:]) else: html_text += escape( message_text[last_offset * 2 :].decode('utf-16-le') # type: ignore ) else: if sys.maxunicode == 0xFFFF: html_text += message_text[last_offset:] else: html_text += message_text[last_offset * 2 :].decode('utf-16-le') # type: ignore 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. .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. 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. .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. 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. .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. 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. .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. Returns: :obj:`str`: Message caption with caption entities formatted as HTML. """ return self._parse_html(self.caption, self.parse_caption_entities(), urled=True) @staticmethod def _parse_markdown( message_text: Optional[str], entities: Dict[MessageEntity, str], urled: bool = False, version: int = 1, offset: int = 0, ) -> Optional[str]: version = int(version) if message_text is None: return None if sys.maxunicode != 0xFFFF: message_text = message_text.encode('utf-16-le') # type: ignore 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 not in parsed_entities: 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())) orig_text = text text = escape_markdown(text, version=version) if nested_entities: if version < 2: raise ValueError( 'Nested entities are not supported for Markdown ' 'version 1' ) text = Message._parse_markdown( orig_text, nested_entities, urled=urled, offset=entity.offset, 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'[{text}]({url})' elif entity.type == MessageEntity.TEXT_MENTION and entity.user: insert = f'[{text}](tg://user?id={entity.user.id})' elif entity.type == MessageEntity.URL and urled: if version == 1: link = orig_text else: link = text insert = f'[{link}]({orig_text})' elif entity.type == MessageEntity.BOLD: insert = '*' + text + '*' elif entity.type == MessageEntity.ITALIC: insert = '_' + text + '_' elif entity.type == MessageEntity.CODE: # Monospace needs special escaping. Also can't have entities nested within insert = ( '`' + escape_markdown( orig_text, version=version, entity_type=MessageEntity.CODE ) + '`' ) elif entity.type == MessageEntity.PRE: # Monospace needs special escaping. Also can't have entities nested within code = escape_markdown( orig_text, version=version, entity_type=MessageEntity.PRE ) if entity.language: prefix = '```' + entity.language + '\n' else: if code.startswith('\\'): prefix = '```' else: prefix = '```\n' insert = prefix + code + '```' elif entity.type == MessageEntity.UNDERLINE: if version == 1: raise ValueError( 'Underline entities are not supported for Markdown ' 'version 1' ) insert = '__' + text + '__' elif entity.type == MessageEntity.STRIKETHROUGH: if version == 1: raise ValueError( 'Strikethrough entities are not supported for Markdown ' 'version 1' ) insert = '~' + text + '~' elif entity.type == MessageEntity.SPOILER: if version == 1: raise ValueError( "Spoiler entities are not supported for Markdown version 1" ) insert = f"||{text}||" else: insert = text if offset == 0: if sys.maxunicode == 0xFFFF: markdown_text += ( escape_markdown( message_text[last_offset : entity.offset - offset], version=version ) + insert ) else: markdown_text += ( escape_markdown( message_text[ # type: ignore last_offset * 2 : (entity.offset - offset) * 2 ].decode('utf-16-le'), version=version, ) + insert ) else: if sys.maxunicode == 0xFFFF: markdown_text += ( message_text[last_offset : entity.offset - offset] + insert ) else: markdown_text += ( message_text[ # type: ignore last_offset * 2 : (entity.offset - offset) * 2 ].decode('utf-16-le') + insert ) last_offset = entity.offset - offset + entity.length if offset == 0: if sys.maxunicode == 0xFFFF: markdown_text += escape_markdown(message_text[last_offset:], version=version) else: markdown_text += escape_markdown( message_text[last_offset * 2 :].decode('utf-16-le'), # type: ignore version=version, ) else: if sys.maxunicode == 0xFFFF: markdown_text += message_text[last_offset:] else: markdown_text += message_text[last_offset * 2 :].decode( # type: ignore 'utf-16-le' ) 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.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. Note: :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for backward compatibility. You should use :meth:`text_markdown_v2` instead. Returns: :obj:`str`: Message text with entities formatted as Markdown. Raises: :exc:`ValueError`: If the message contains underline, strikethrough, spoiler 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.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. .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. 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.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. Note: :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for backward compatibility. You should use :meth:`text_markdown_v2_urled` instead. Returns: :obj:`str`: Message text with entities formatted as Markdown. Raises: :exc:`ValueError`: If the message contains underline, strikethrough, spoiler 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.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. .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. 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.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. Note: :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for backward compatibility. You should use :meth:`caption_markdown_v2` instead. Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. Raises: :exc:`ValueError`: If the message contains underline, strikethrough, spoiler 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.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. .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. 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.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. Note: :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for backward compatibility. You should use :meth:`caption_markdown_v2_urled` instead. Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. Raises: :exc:`ValueError`: If the message contains underline, strikethrough, spoiler 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.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. .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. 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-13.11/telegram/messageautodeletetimerchanged.py000066400000000000000000000035761417656324400256740ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: message_auto_delete_time (:obj:`int`): New auto-delete time for messages in the chat. """ __slots__ = ('message_auto_delete_time', '_id_attrs') def __init__( self, message_auto_delete_time: int, **_kwargs: Any, ): self.message_auto_delete_time = int(message_auto_delete_time) self._id_attrs = (self.message_auto_delete_time,) python-telegram-bot-13.11/telegram/messageentity.py000066400000000000000000000134261417656324400224750ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, List, Optional, ClassVar from telegram import TelegramObject, User, constants 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. Currently, can be mention (@username), hashtag, bot_command, url, email, phone_number, bold (bold text), italic (italic text), strikethrough, spoiler (spoiler message), code (monowidth string), pre (monowidth block), text_link (for clickable text URLs), text_mention (for users without usernames). 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. Attributes: type (:obj:`str`): Type of the entity. 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. Url that will be opened after user taps on the text. user (:class:`telegram.User`): Optional. The mentioned user. language (:obj:`str`): Optional. Programming language of the entity text. """ __slots__ = ('length', 'url', 'user', 'type', 'language', 'offset', '_id_attrs') def __init__( self, type: str, # pylint: disable=W0622 offset: int, length: int, url: str = None, user: User = None, language: str = None, **_kwargs: Any, ): # Required self.type = type self.offset = offset self.length = length # Optionals self.url = url self.user = user self.language = language self._id_attrs = (self.type, self.offset, self.length) @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 cls(**data) MENTION: ClassVar[str] = constants.MESSAGEENTITY_MENTION """:const:`telegram.constants.MESSAGEENTITY_MENTION`""" HASHTAG: ClassVar[str] = constants.MESSAGEENTITY_HASHTAG """:const:`telegram.constants.MESSAGEENTITY_HASHTAG`""" CASHTAG: ClassVar[str] = constants.MESSAGEENTITY_CASHTAG """:const:`telegram.constants.MESSAGEENTITY_CASHTAG`""" PHONE_NUMBER: ClassVar[str] = constants.MESSAGEENTITY_PHONE_NUMBER """:const:`telegram.constants.MESSAGEENTITY_PHONE_NUMBER`""" BOT_COMMAND: ClassVar[str] = constants.MESSAGEENTITY_BOT_COMMAND """:const:`telegram.constants.MESSAGEENTITY_BOT_COMMAND`""" URL: ClassVar[str] = constants.MESSAGEENTITY_URL """:const:`telegram.constants.MESSAGEENTITY_URL`""" EMAIL: ClassVar[str] = constants.MESSAGEENTITY_EMAIL """:const:`telegram.constants.MESSAGEENTITY_EMAIL`""" BOLD: ClassVar[str] = constants.MESSAGEENTITY_BOLD """:const:`telegram.constants.MESSAGEENTITY_BOLD`""" ITALIC: ClassVar[str] = constants.MESSAGEENTITY_ITALIC """:const:`telegram.constants.MESSAGEENTITY_ITALIC`""" CODE: ClassVar[str] = constants.MESSAGEENTITY_CODE """:const:`telegram.constants.MESSAGEENTITY_CODE`""" PRE: ClassVar[str] = constants.MESSAGEENTITY_PRE """:const:`telegram.constants.MESSAGEENTITY_PRE`""" TEXT_LINK: ClassVar[str] = constants.MESSAGEENTITY_TEXT_LINK """:const:`telegram.constants.MESSAGEENTITY_TEXT_LINK`""" TEXT_MENTION: ClassVar[str] = constants.MESSAGEENTITY_TEXT_MENTION """:const:`telegram.constants.MESSAGEENTITY_TEXT_MENTION`""" UNDERLINE: ClassVar[str] = constants.MESSAGEENTITY_UNDERLINE """:const:`telegram.constants.MESSAGEENTITY_UNDERLINE`""" STRIKETHROUGH: ClassVar[str] = constants.MESSAGEENTITY_STRIKETHROUGH """:const:`telegram.constants.MESSAGEENTITY_STRIKETHROUGH`""" SPOILER: ClassVar[str] = constants.MESSAGEENTITY_SPOILER """:const:`telegram.constants.MESSAGEENTITY_SPOILER` .. versionadded:: 13.10 """ ALL_TYPES: ClassVar[List[str]] = constants.MESSAGEENTITY_ALL_TYPES """:const:`telegram.constants.MESSAGEENTITY_ALL_TYPES`\n List of all the types""" python-telegram-bot-13.11/telegram/messageid.py000066400000000000000000000027111417656324400215500ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject 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. Attributes: message_id (:obj:`int`): Unique message identifier """ __slots__ = ('message_id', '_id_attrs') def __init__(self, message_id: int, **_kwargs: Any): self.message_id = int(message_id) self._id_attrs = (self.message_id,) python-telegram-bot-13.11/telegram/parsemode.py000066400000000000000000000033671417656324400215760ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Parse Modes.""" from typing import ClassVar from telegram import constants from telegram.utils.deprecate import set_new_attribute_deprecated class ParseMode: """This object represents a Telegram Message Parse Modes.""" __slots__ = ('__dict__',) MARKDOWN: ClassVar[str] = constants.PARSEMODE_MARKDOWN """:const:`telegram.constants.PARSEMODE_MARKDOWN`\n Note: :attr:`MARKDOWN` is a legacy mode, retained by Telegram for backward compatibility. You should use :attr:`MARKDOWN_V2` instead. """ MARKDOWN_V2: ClassVar[str] = constants.PARSEMODE_MARKDOWN_V2 """:const:`telegram.constants.PARSEMODE_MARKDOWN_V2`""" HTML: ClassVar[str] = constants.PARSEMODE_HTML """:const:`telegram.constants.PARSEMODE_HTML`""" def __setattr__(self, key: str, value: object) -> None: set_new_attribute_deprecated(self, key, value) python-telegram-bot-13.11/telegram/passport/000077500000000000000000000000001417656324400211075ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/passport/__init__.py000066400000000000000000000000001417656324400232060ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/passport/credentials.py000066400000000000000000000456561417656324400237760ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=C0114, W0622 try: import ujson as json except ImportError: import json # type: ignore[no-redef] from base64 import b64decode from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, 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 MGF1, OAEP, Cipher, AES, CBC = (None, None, None, None, None) # type: ignore[misc] SHA1, SHA256, SHA512, Hash = (None, None, None, None) # type: ignore[misc] CRYPTO_INSTALLED = False from telegram import TelegramError, TelegramObject from telegram.utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot class TelegramDecryptionError(TelegramError): """Something went wrong with decryption.""" __slots__ = ('_msg',) def __init__(self, message: Union[str, Exception]): super().__init__(f"TelegramDecryptionError: {message}") self._msg = str(message) def __reduce__(self) -> Tuple[type, Tuple[str]]: return self.__class__, (self._msg,) @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:`TelegramDecryptionError`: 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 TelegramDecryptionError(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` or :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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: data (:class:`telegram.Credentials` or :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__ = ( 'hash', 'secret', 'bot', 'data', '_id_attrs', '_decrypted_secret', '_decrypted_data', ) def __init__(self, data: str, hash: str, secret: str, bot: 'Bot' = None, **_kwargs: Any): # Required self.data = data self.hash = hash self.secret = secret self._id_attrs = (self.data, self.hash, self.secret) self.bot = bot self._decrypted_secret = None self._decrypted_data: Optional['Credentials'] = None @property def decrypted_secret(self) -> str: """ :obj:`str`: Lazily decrypt and return secret. Raises: telegram.TelegramDecryptionError: 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.bot.private_key.decrypt( b64decode(self.secret), OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None), # skipcq ) except ValueError as exception: # If decryption fails raise exception raise TelegramDecryptionError(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.TelegramDecryptionError: 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.bot, ) return self._decrypted_data class Credentials(TelegramObject): """ Attributes: secure_data (:class:`telegram.SecureData`): Credentials for encrypted data nonce (:obj:`str`): Bot-specified nonce """ __slots__ = ('bot', 'nonce', 'secure_data') def __init__(self, secure_data: 'SecureData', nonce: str, bot: 'Bot' = None, **_kwargs: Any): # Required self.secure_data = secure_data self.nonce = nonce self.bot = bot @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 cls(bot=bot, **data) 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. 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__ = ( 'bot', 'utility_bill', 'personal_details', 'temporary_registration', 'address', 'driver_license', 'rental_agreement', 'internal_passport', 'identity_card', 'bank_statement', 'passport', 'passport_registration', ) def __init__( self, personal_details: 'SecureValue' = None, passport: 'SecureValue' = None, internal_passport: 'SecureValue' = None, driver_license: 'SecureValue' = None, identity_card: 'SecureValue' = None, address: 'SecureValue' = None, utility_bill: 'SecureValue' = None, bank_statement: 'SecureValue' = None, rental_agreement: 'SecureValue' = None, passport_registration: 'SecureValue' = None, temporary_registration: 'SecureValue' = None, bot: 'Bot' = None, **_kwargs: Any, ): # Optionals self.temporary_registration = temporary_registration self.passport_registration = passport_registration self.rental_agreement = rental_agreement self.bank_statement = bank_statement self.utility_bill = utility_bill self.address = address self.identity_card = identity_card self.driver_license = driver_license self.internal_passport = internal_passport self.passport = passport self.personal_details = personal_details self.bot = bot @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 cls(bot=bot, **data) 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. 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 (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. """ __slots__ = ('data', 'front_side', 'reverse_side', 'selfie', 'files', 'translation', 'bot') def __init__( self, data: 'DataCredentials' = None, front_side: 'FileCredentials' = None, reverse_side: 'FileCredentials' = None, selfie: 'FileCredentials' = None, files: List['FileCredentials'] = None, translation: List['FileCredentials'] = None, bot: 'Bot' = None, **_kwargs: Any, ): self.data = data self.front_side = front_side self.reverse_side = reverse_side self.selfie = selfie self.files = files self.translation = translation self.bot = bot @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 cls(bot=bot, **data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['files'] = [p.to_dict() for p in self.files] data['translation'] = [p.to_dict() for p in self.translation] return data class _CredentialsBase(TelegramObject): """Base class for DataCredentials and FileCredentials.""" __slots__ = ('hash', 'secret', 'file_hash', 'data_hash', 'bot') def __init__(self, hash: str, secret: str, bot: 'Bot' = None, **_kwargs: Any): self.hash = hash self.secret = secret # Aliases just be be sure self.file_hash = self.hash self.data_hash = self.hash self.bot = bot 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, **_kwargs: Any): super().__init__(data_hash, secret, **_kwargs) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() del data['file_hash'] del data['hash'] return data 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, **_kwargs: Any): super().__init__(file_hash, secret, **_kwargs) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() del data['data_hash'] del data['hash'] return data python-telegram-bot-13.11/telegram/passport/data.py000066400000000000000000000111241417656324400223710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=C0114 from typing import TYPE_CHECKING, Any from telegram import TelegramObject if TYPE_CHECKING: from telegram import Bot class PersonalDetails(TelegramObject): """ This object represents personal details. 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__ = ( 'middle_name', 'first_name_native', 'last_name_native', 'residence_country_code', 'first_name', 'last_name', 'country_code', 'gender', 'bot', 'middle_name_native', 'birth_date', ) def __init__( self, first_name: str, last_name: str, birth_date: str, gender: str, country_code: str, residence_country_code: str, first_name_native: str = None, last_name_native: str = None, middle_name: str = None, middle_name_native: str = None, bot: 'Bot' = None, **_kwargs: Any, ): # Required self.first_name = first_name self.last_name = last_name self.middle_name = middle_name self.birth_date = birth_date self.gender = gender self.country_code = country_code self.residence_country_code = residence_country_code self.first_name_native = first_name_native self.last_name_native = last_name_native self.middle_name_native = middle_name_native self.bot = bot class ResidentialAddress(TelegramObject): """ This object represents a residential address. 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__ = ( 'post_code', 'city', 'country_code', 'street_line2', 'street_line1', 'bot', 'state', ) def __init__( self, street_line1: str, street_line2: str, city: str, state: str, country_code: str, post_code: str, bot: 'Bot' = None, **_kwargs: Any, ): # Required self.street_line1 = street_line1 self.street_line2 = street_line2 self.city = city self.state = state self.country_code = country_code self.post_code = post_code self.bot = bot class IdDocumentData(TelegramObject): """ This object represents the data of an identity document. Attributes: document_no (:obj:`str`): Document number. expiry_date (:obj:`str`): Optional. Date of expiry, in DD.MM.YYYY format. """ __slots__ = ('document_no', 'bot', 'expiry_date') def __init__(self, document_no: str, expiry_date: str, bot: 'Bot' = None, **_kwargs: Any): self.document_no = document_no self.expiry_date = expiry_date self.bot = bot python-telegram-bot-13.11/telegram/passport/encryptedpassportelement.py000066400000000000000000000276141417656324400266360ustar00rootroot00000000000000#!/usr/bin/env python # flake8: noqa: E501 # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, List, Optional from telegram import ( IdDocumentData, PassportFile, PersonalDetails, ResidentialAddress, TelegramObject, ) from telegram.passport.credentials import decrypt_json 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". data (:class:`telegram.PersonalDetails` | :class:`telegram.IdDocument` | \ :class:`telegram.ResidentialAddress` | :obj:`str`, optional): Decrypted or encrypted data, available for "personal_details", "passport", "driver_license", "identity_card", "identity_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 (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted files with documents provided by the user, available for "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. front_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the front side of the document, provided by the user. Available 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 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 for "passport", "driver_license", "identity_card" and "internal_passport". translation (List[: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. hash (:obj:`str`): Base64-encoded element hash for using in :class:`telegram.PassportElementErrorUnspecified`. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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". data (:class:`telegram.PersonalDetails` | :class:`telegram.IdDocument` | \ :class:`telegram.ResidentialAddress` | :obj:`str`): Optional. Decrypted or encrypted data, available for "personal_details", "passport", "driver_license", "identity_card", "identity_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 (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted files with documents provided by the user, available for "utility_bill", "bank_statement", "rental_agreement", "passport_registration" and "temporary_registration" types. front_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the front side of the document, provided by the user. Available 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 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 for "passport", "driver_license", "identity_card" and "internal_passport". translation (List[: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. hash (:obj:`str`): Base64-encoded element hash for using in :class:`telegram.PassportElementErrorUnspecified`. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ( 'selfie', 'files', 'type', 'translation', 'email', 'hash', 'phone_number', 'bot', 'reverse_side', 'front_side', 'data', '_id_attrs', ) def __init__( self, type: str, # pylint: disable=W0622 data: PersonalDetails = None, phone_number: str = None, email: str = None, files: List[PassportFile] = None, front_side: PassportFile = None, reverse_side: PassportFile = None, selfie: PassportFile = None, translation: List[PassportFile] = None, hash: str = None, # pylint: disable=W0622 bot: 'Bot' = None, credentials: 'Credentials' = None, # pylint: disable=W0613 **_kwargs: Any, ): # Required self.type = type # Optionals self.data = data self.phone_number = phone_number self.email = email self.files = files self.front_side = front_side self.reverse_side = reverse_side self.selfie = selfie self.translation = translation self.hash = hash self._id_attrs = ( self.type, self.data, self.phone_number, self.email, self.files, self.front_side, self.reverse_side, self.selfie, ) self.bot = bot @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 cls(bot=bot, **data) @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 cls(bot=bot, **data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() if self.files: data['files'] = [p.to_dict() for p in self.files] if self.translation: data['translation'] = [p.to_dict() for p in self.translation] return data python-telegram-bot-13.11/telegram/passport/passportdata.py000066400000000000000000000115121417656324400241660ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, Any, List, Optional from telegram import EncryptedCredentials, EncryptedPassportElement, TelegramObject 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.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.payload`. Args: data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information about documents and other Telegram Passport elements that was shared with the bot. credentials (:class:`telegram.EncryptedCredentials`)): Encrypted credentials. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information about documents and other Telegram Passport elements that was shared with the bot. credentials (:class:`telegram.EncryptedCredentials`): Encrypted credentials. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. """ __slots__ = ('bot', 'credentials', 'data', '_decrypted_data', '_id_attrs') def __init__( self, data: List[EncryptedPassportElement], credentials: EncryptedCredentials, bot: 'Bot' = None, **_kwargs: Any, ): self.data = data self.credentials = credentials self.bot = bot self._decrypted_data: Optional[List[EncryptedPassportElement]] = None self._id_attrs = tuple([x.type for x in data] + [credentials.hash]) @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 cls(bot=bot, **data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['data'] = [e.to_dict() for e in self.data] return data @property def decrypted_data(self) -> List[EncryptedPassportElement]: """ List[:class:`telegram.EncryptedPassportElement`]: Lazily decrypt and return information about documents and other Telegram Passport elements which were shared with the bot. Raises: telegram.TelegramDecryptionError: 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 = [ EncryptedPassportElement.de_json_decrypted( element.to_dict(), self.bot, self.decrypted_credentials ) for element in self.data ] return self._decrypted_data @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.TelegramDecryptionError: Decryption failed. Usually due to bad private/public key but can also suggest malformed/tampered data. """ return self.credentials.decrypted_data python-telegram-bot-13.11/telegram/passport/passportelementerrors.py000066400000000000000000000376131417656324400261550ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=W0622 """This module contains the classes that represent Telegram PassportElementError.""" from typing import Any from telegram import TelegramObject 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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. """ # All subclasses of this class won't have _id_attrs in slots since it's added here. __slots__ = ('message', 'source', 'type', '_id_attrs') def __init__(self, source: str, type: str, message: str, **_kwargs: Any): # Required self.source = str(source) self.type = str(type) self.message = str(message) self._id_attrs = (self.source, self.type) 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:`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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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, **_kwargs: Any): # Required super().__init__('data', type, message) self.field_name = field_name self.data_hash = 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:`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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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, **_kwargs: Any): # Required super().__init__('file', type, message) self.file_hash = 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:`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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``. file_hashes (List[:obj:`str`]): List of base64-encoded file hashes. message (:obj:`str`): Error message. """ __slots__ = ('file_hashes',) def __init__(self, type: str, file_hashes: str, message: str, **_kwargs: Any): # Required super().__init__('files', type, message) self.file_hashes = file_hashes self._id_attrs = (self.source, self.type, self.message) + tuple(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:`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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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, **_kwargs: Any): # Required super().__init__('front_side', type, message) self.file_hash = 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:`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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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, **_kwargs: Any): # Required super().__init__('reverse_side', type, message) self.file_hash = 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:`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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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, **_kwargs: Any): # Required super().__init__('selfie', type, message) self.file_hash = 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:`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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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, **_kwargs: Any): # Required super().__init__('translation_file', type, message) self.file_hash = 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:`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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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_hashes (List[:obj:`str`]): List of base64-encoded file hashes. message (:obj:`str`): Error message. """ __slots__ = ('file_hashes',) def __init__(self, type: str, file_hashes: str, message: str, **_kwargs: Any): # Required super().__init__('translation_files', type, message) self.file_hashes = file_hashes self._id_attrs = (self.source, self.type, self.message) + tuple(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:`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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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, **_kwargs: Any): # Required super().__init__('unspecified', type, message) self.element_hash = element_hash self._id_attrs = (self.source, self.type, self.element_hash, self.message) python-telegram-bot-13.11/telegram/passport/passportfile.py000066400000000000000000000127411417656324400242010ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, List, Optional from telegram import TelegramObject from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput 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. file_date (:obj:`int`): Unix time when the file was uploaded. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: file_id (:obj:`str`): Identifier for this 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. file_date (:obj:`int`): Unix time when the file was uploaded. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ( 'file_date', 'bot', 'file_id', 'file_size', '_credentials', 'file_unique_id', '_id_attrs', ) def __init__( self, file_id: str, file_unique_id: str, file_date: int, file_size: int = None, bot: 'Bot' = None, credentials: 'FileCredentials' = None, **_kwargs: Any, ): # Required self.file_id = file_id self.file_unique_id = file_unique_id self.file_size = file_size self.file_date = file_date # Optionals self.bot = bot self._credentials = credentials self._id_attrs = (self.file_unique_id,) @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 cls(bot=bot, **data) @classmethod def de_list_decrypted( cls, data: Optional[List[JSONDict]], bot: 'Bot', credentials: List['FileCredentials'] ) -> List[Optional['PassportFile']]: """Variant of :meth:`telegram.TelegramObject.de_list` that also takes into account passport credentials. Args: data (Dict[:obj:`str`, ...]): The JSON data. bot (:class:`telegram.Bot`): The bot associated with these objects. credentials (:class:`telegram.FileCredentials`): The credentials Returns: List[:class:`telegram.PassportFile`]: """ if not data: return [] return [ cls.de_json_decrypted(passport_file, bot, credentials[i]) for i, passport_file in enumerate(data) ] def get_file( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> 'File': """ Wrapper over :attr:`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 = self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) file.set_credentials(self._credentials) return file python-telegram-bot-13.11/telegram/payment/000077500000000000000000000000001417656324400207115ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/payment/__init__.py000066400000000000000000000000001417656324400230100ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/payment/invoice.py000066400000000000000000000061361417656324400227250ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject 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`, :attr:`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 :obj:`exp` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: title (:obj:`str`): Product name. description (:obj:`str`): Product description. start_parameter (:obj:`str`): Unique bot deep-linking parameter. currency (:obj:`str`): Three-letter ISO 4217 currency code. total_amount (:obj:`int`): Total price in the smallest units of the currency. """ __slots__ = ( 'currency', 'start_parameter', 'title', 'description', 'total_amount', '_id_attrs', ) def __init__( self, title: str, description: str, start_parameter: str, currency: str, total_amount: int, **_kwargs: Any, ): self.title = title self.description = description self.start_parameter = start_parameter self.currency = currency self.total_amount = total_amount self._id_attrs = ( self.title, self.description, self.start_parameter, self.currency, self.total_amount, ) python-telegram-bot-13.11/telegram/payment/labeledprice.py000066400000000000000000000042061417656324400237000ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject 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. 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 :obj:`exp` parameter in `currencies.json `_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: label (:obj:`str`): Portion label. amount (:obj:`int`): Price of the product in the smallest units of the currency. """ __slots__ = ('label', '_id_attrs', 'amount') def __init__(self, label: str, amount: int, **_kwargs: Any): self.label = label self.amount = amount self._id_attrs = (self.label, self.amount) python-telegram-bot-13.11/telegram/payment/orderinfo.py000066400000000000000000000055011417656324400232530ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional from telegram import ShippingAddress, 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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', 'shipping_address', 'phone_number', 'name', '_id_attrs') def __init__( self, name: str = None, phone_number: str = None, email: str = None, shipping_address: str = None, **_kwargs: Any, ): self.name = name self.phone_number = phone_number self.email = email self.shipping_address = shipping_address self._id_attrs = (self.name, self.phone_number, self.email, self.shipping_address) @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 cls(**data) python-telegram-bot-13.11/telegram/payment/precheckoutquery.py000066400000000000000000000121551417656324400246710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional from telegram import OrderInfo, TelegramObject, User from telegram.utils.helpers 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 ``from`` is a reserved word, use ``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 :obj:`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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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. 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. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ( 'bot', 'invoice_payload', 'shipping_option_id', 'currency', 'order_info', 'total_amount', 'id', 'from_user', '_id_attrs', ) def __init__( self, id: str, # pylint: disable=W0622 from_user: User, currency: str, total_amount: int, invoice_payload: str, shipping_option_id: str = None, order_info: OrderInfo = None, bot: 'Bot' = None, **_kwargs: Any, ): self.id = id # pylint: disable=C0103 self.from_user = from_user self.currency = currency self.total_amount = total_amount self.invoice_payload = invoice_payload self.shipping_option_id = shipping_option_id self.order_info = order_info self.bot = bot self._id_attrs = (self.id,) @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'), bot) data['order_info'] = OrderInfo.de_json(data.get('order_info'), bot) return cls(bot=bot, **data) def answer( # pylint: disable=C0103 self, ok: bool, error_message: str = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.answer_pre_checkout_query( pre_checkout_query_id=self.id, ok=ok, error_message=error_message, timeout=timeout, api_kwargs=api_kwargs, ) python-telegram-bot-13.11/telegram/payment/shippingaddress.py000066400000000000000000000054521417656324400244600ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import TelegramObject 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_cod` 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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__ = ( 'post_code', 'city', '_id_attrs', 'country_code', 'street_line2', 'street_line1', 'state', ) def __init__( self, country_code: str, state: str, city: str, street_line1: str, street_line2: str, post_code: str, **_kwargs: Any, ): self.country_code = country_code self.state = state self.city = city self.street_line1 = street_line1 self.street_line2 = street_line2 self.post_code = post_code self._id_attrs = ( self.country_code, self.state, self.city, self.street_line1, self.street_line2, self.post_code, ) python-telegram-bot-13.11/telegram/payment/shippingoption.py000066400000000000000000000044321417656324400243400ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, List from telegram import TelegramObject from telegram.utils.types import JSONDict if TYPE_CHECKING: from telegram import LabeledPrice # noqa 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. Args: id (:obj:`str`): Shipping option identifier. title (:obj:`str`): Option title. prices (List[:class:`telegram.LabeledPrice`]): List of price portions. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: id (:obj:`str`): Shipping option identifier. title (:obj:`str`): Option title. prices (List[:class:`telegram.LabeledPrice`]): List of price portions. """ __slots__ = ('prices', 'title', 'id', '_id_attrs') def __init__( self, id: str, # pylint: disable=W0622 title: str, prices: List['LabeledPrice'], **_kwargs: Any, ): self.id = id # pylint: disable=C0103 self.title = title self.prices = prices self._id_attrs = (self.id,) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['prices'] = [p.to_dict() for p in self.prices] return data python-telegram-bot-13.11/telegram/payment/shippingquery.py000066400000000000000000000100261417656324400241710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional, List from telegram import ShippingAddress, TelegramObject, User, ShippingOption from telegram.utils.helpers import DEFAULT_NONE from telegram.utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot 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 ``from`` is a reserved word, use ``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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ('bot', 'invoice_payload', 'shipping_address', 'id', 'from_user', '_id_attrs') def __init__( self, id: str, # pylint: disable=W0622 from_user: User, invoice_payload: str, shipping_address: ShippingAddress, bot: 'Bot' = None, **_kwargs: Any, ): self.id = id # pylint: disable=C0103 self.from_user = from_user self.invoice_payload = invoice_payload self.shipping_address = shipping_address self.bot = bot self._id_attrs = (self.id,) @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'), bot) data['shipping_address'] = ShippingAddress.de_json(data.get('shipping_address'), bot) return cls(bot=bot, **data) def answer( # pylint: disable=C0103 self, ok: bool, shipping_options: List[ShippingOption] = None, error_message: str = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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 self.bot.answer_shipping_query( shipping_query_id=self.id, ok=ok, shipping_options=shipping_options, error_message=error_message, timeout=timeout, api_kwargs=api_kwargs, ) python-telegram-bot-13.11/telegram/payment/successfulpayment.py000066400000000000000000000104601417656324400250410ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional from telegram import OrderInfo, 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 :obj:`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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: currency (:obj:`str`): Three-letter ISO 4217 currency code. total_amount (:obj:`int`): Total price in the smallest units of the currency. 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__ = ( 'invoice_payload', 'shipping_option_id', 'currency', 'order_info', 'telegram_payment_charge_id', 'provider_payment_charge_id', 'total_amount', '_id_attrs', ) def __init__( self, currency: str, total_amount: int, invoice_payload: str, telegram_payment_charge_id: str, provider_payment_charge_id: str, shipping_option_id: str = None, order_info: OrderInfo = None, **_kwargs: Any, ): self.currency = currency self.total_amount = total_amount self.invoice_payload = invoice_payload self.shipping_option_id = shipping_option_id self.order_info = order_info self.telegram_payment_charge_id = telegram_payment_charge_id self.provider_payment_charge_id = provider_payment_charge_id self._id_attrs = (self.telegram_payment_charge_id, self.provider_payment_charge_id) @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 cls(**data) python-telegram-bot-13.11/telegram/poll.py000066400000000000000000000277541417656324400205730ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 import sys from typing import TYPE_CHECKING, Any, Dict, List, Optional, ClassVar from telegram import MessageEntity, TelegramObject, User, constants from telegram.utils.helpers import from_timestamp, to_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, 1-100 characters. voter_count (:obj:`int`): Number of users that voted for this option. Attributes: text (:obj:`str`): Option text, 1-100 characters. voter_count (:obj:`int`): Number of users that voted for this option. """ __slots__ = ('voter_count', 'text', '_id_attrs') def __init__(self, text: str, voter_count: int, **_kwargs: Any): self.text = text self.voter_count = voter_count self._id_attrs = (self.text, self.voter_count) MAX_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH """:const:`telegram.constants.MAX_POLL_OPTION_LENGTH`""" 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:`options_ids` are equal. Attributes: poll_id (:obj:`str`): Unique poll identifier. user (:class:`telegram.User`): The user, who changed the answer to the poll. option_ids (List[:obj:`int`]): Identifiers of answer options, chosen by the user. Args: poll_id (:obj:`str`): Unique poll identifier. user (:class:`telegram.User`): The user, who changed the answer to the poll. option_ids (List[:obj:`int`]): 0-based identifiers of answer options, chosen by the user. May be empty if the user retracted their vote. """ __slots__ = ('option_ids', 'user', 'poll_id', '_id_attrs') def __init__(self, poll_id: str, user: User, option_ids: List[int], **_kwargs: Any): self.poll_id = poll_id self.user = user self.option_ids = option_ids self._id_attrs = (self.poll_id, self.user, tuple(self.option_ids)) @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) return cls(**data) 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. Attributes: id (:obj:`str`): Unique poll identifier. question (:obj:`str`): Poll question, 1-300 characters. options (List[:class:`PollOption`]): List of poll options. 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. Identifier of the correct answer option. 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. explanation_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`. 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. Args: id (:obj:`str`): Unique poll identifier. question (:obj:`str`): Poll question, 1-300 characters. options (List[:class:`PollOption`]): List of poll options. 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): 0-based identifier of the correct answer option. Available only for polls in the quiz mode, which are closed, or was sent (not forwarded) by the bot or to the 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-200 characters. explanation_entities (List[:class:`telegram.MessageEntity`], optional): Special entities like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`. 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`. """ __slots__ = ( 'total_voter_count', 'allows_multiple_answers', 'open_period', 'options', 'type', 'explanation_entities', 'is_anonymous', 'close_date', 'is_closed', 'id', 'explanation', 'question', 'correct_option_id', '_id_attrs', ) def __init__( self, id: str, # pylint: disable=W0622 question: str, options: List[PollOption], total_voter_count: int, is_closed: bool, is_anonymous: bool, type: str, # pylint: disable=W0622 allows_multiple_answers: bool, correct_option_id: int = None, explanation: str = None, explanation_entities: List[MessageEntity] = None, open_period: int = None, close_date: datetime.datetime = None, **_kwargs: Any, ): self.id = id # pylint: disable=C0103 self.question = question self.options = options self.total_voter_count = total_voter_count self.is_closed = is_closed self.is_anonymous = is_anonymous self.type = type self.allows_multiple_answers = allows_multiple_answers self.correct_option_id = correct_option_id self.explanation = explanation self.explanation_entities = explanation_entities self.open_period = open_period self.close_date = close_date self._id_attrs = (self.id,) @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 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')) return cls(**data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['options'] = [x.to_dict() for x in self.options] if self.explanation_entities: data['explanation_entities'] = [e.to_dict() for e in self.explanation_entities] data['close_date'] = to_timestamp(data.get('close_date')) return data 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'.") # Is it a narrow build, if so we don't need to convert if sys.maxunicode == 0xFFFF: return self.explanation[entity.offset : entity.offset + entity.length] 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: 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 or []) if entity.type in types } REGULAR: ClassVar[str] = constants.POLL_REGULAR """:const:`telegram.constants.POLL_REGULAR`""" QUIZ: ClassVar[str] = constants.POLL_QUIZ """:const:`telegram.constants.POLL_QUIZ`""" MAX_QUESTION_LENGTH: ClassVar[int] = constants.MAX_POLL_QUESTION_LENGTH """:const:`telegram.constants.MAX_POLL_QUESTION_LENGTH`""" MAX_OPTION_LENGTH: ClassVar[int] = constants.MAX_POLL_OPTION_LENGTH """:const:`telegram.constants.MAX_POLL_OPTION_LENGTH`""" python-telegram-bot-13.11/telegram/proximityalerttriggered.py000066400000000000000000000051361417656324400246040ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any, Optional, TYPE_CHECKING from telegram import TelegramObject, 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__ = ('traveler', 'distance', 'watcher', '_id_attrs') def __init__(self, traveler: User, watcher: User, distance: int, **_kwargs: Any): self.traveler = traveler self.watcher = watcher self.distance = distance self._id_attrs = (self.traveler, self.watcher, self.distance) @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 cls(bot=bot, **data) python-telegram-bot-13.11/telegram/py.typed000066400000000000000000000000001417656324400207210ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/replykeyboardmarkup.py000066400000000000000000000303571417656324400237120ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any, List, Union, Sequence from telegram import KeyboardButton, ReplyMarkup from telegram.utils.types import JSONDict class ReplyKeyboardMarkup(ReplyMarkup): """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 the size of :attr:`keyboard` and all the buttons are equal. Example: 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. Args: keyboard (List[List[: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 (has ``reply_to_message_id``), 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; 1-64 characters. .. versionadded:: 13.7 **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: keyboard (List[List[:class:`telegram.KeyboardButton` | :obj:`str`]]): Array of button rows. resize_keyboard (:obj:`bool`): Optional. Requests clients to resize the keyboard. one_time_keyboard (:obj:`bool`): Optional. Requests clients to hide the keyboard as soon as it's been used. selective (:obj:`bool`): Optional. Show the keyboard to specific users only. input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input field when the reply is active. .. versionadded:: 13.7 """ __slots__ = ( 'selective', 'keyboard', 'resize_keyboard', 'one_time_keyboard', 'input_field_placeholder', '_id_attrs', ) def __init__( self, keyboard: Sequence[Sequence[Union[str, KeyboardButton]]], resize_keyboard: bool = False, one_time_keyboard: bool = False, selective: bool = False, input_field_placeholder: str = None, **_kwargs: Any, ): # Required self.keyboard = [] for row in keyboard: button_row = [] for button in row: if isinstance(button, KeyboardButton): button_row.append(button) # telegram.KeyboardButton else: button_row.append(KeyboardButton(button)) # str self.keyboard.append(button_row) # Optionals self.resize_keyboard = bool(resize_keyboard) self.one_time_keyboard = bool(one_time_keyboard) self.selective = bool(selective) self.input_field_placeholder = input_field_placeholder self._id_attrs = (self.keyboard,) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['keyboard'] = [] for row in self.keyboard: data['keyboard'].append([button.to_dict() for button in row]) return data @classmethod def from_button( cls, button: Union[KeyboardButton, str], resize_keyboard: bool = False, one_time_keyboard: bool = False, selective: bool = False, input_field_placeholder: str = 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 (has ``reply_to_message_id``), 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 **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ return cls( [[button]], resize_keyboard=resize_keyboard, one_time_keyboard=one_time_keyboard, selective=selective, input_field_placeholder=input_field_placeholder, **kwargs, ) @classmethod def from_row( cls, button_row: List[Union[str, KeyboardButton]], resize_keyboard: bool = False, one_time_keyboard: bool = False, selective: bool = False, input_field_placeholder: str = None, **kwargs: object, ) -> 'ReplyKeyboardMarkup': """Shortcut for:: ReplyKeyboardMarkup([button_row], **kwargs) Return a ReplyKeyboardMarkup from a single row of KeyboardButtons. Args: button_row (List[: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 (has ``reply_to_message_id``), 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 **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ return cls( [button_row], resize_keyboard=resize_keyboard, one_time_keyboard=one_time_keyboard, selective=selective, input_field_placeholder=input_field_placeholder, **kwargs, ) @classmethod def from_column( cls, button_column: List[Union[str, KeyboardButton]], resize_keyboard: bool = False, one_time_keyboard: bool = False, selective: bool = False, input_field_placeholder: str = 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 (List[: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 (has ``reply_to_message_id``), 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 **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ 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, **kwargs, ) def __hash__(self) -> int: return hash( ( tuple(tuple(button for button in row) for row in self.keyboard), self.resize_keyboard, self.one_time_keyboard, self.selective, ) ) python-telegram-bot-13.11/telegram/replykeyboardremove.py000066400000000000000000000052671417656324400237120ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Any from telegram import ReplyMarkup class ReplyKeyboardRemove(ReplyMarkup): """ 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`). Example: 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. 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`. 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 (has `reply_to_message_id`), sender of the original message. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: remove_keyboard (:obj:`True`): Requests clients to remove the custom keyboard. selective (:obj:`bool`): Optional. Use this parameter if you want to remove the keyboard for specific users only. """ __slots__ = ('selective', 'remove_keyboard') def __init__(self, selective: bool = False, **_kwargs: Any): # Required self.remove_keyboard = True # Optionals self.selective = bool(selective) python-telegram-bot-13.11/telegram/replymarkup.py000066400000000000000000000022601417656324400221610ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ReplyMarkup Objects.""" from telegram import TelegramObject class ReplyMarkup(TelegramObject): """Base class for Telegram ReplyMarkup Objects. See :class:`telegram.InlineKeyboardMarkup`, :class:`telegram.ReplyKeyboardMarkup`, :class:`telegram.ReplyKeyboardRemove` and :class:`telegram.ForceReply` for detailed use. """ __slots__ = () python-telegram-bot-13.11/telegram/update.py000066400000000000000000000410311417656324400210670ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, Optional from telegram import ( CallbackQuery, ChosenInlineResult, InlineQuery, Message, Poll, PreCheckoutQuery, ShippingQuery, TelegramObject, ChatMemberUpdated, constants, ChatJoinRequest, ) from telegram.poll import PollAnswer from telegram.utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot, Chat, User # noqa 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. 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. 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. 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 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 ``'chat_member'`` in the list of ``'allowed_updates'`` to receive these updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, :meth:`telegram.ext.Updater.start_polling` and :meth:`telegram.ext.Updater.start_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 **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: update_id (:obj:`int`): The update's unique identifier. message (:class:`telegram.Message`): Optional. New incoming message. edited_message (:class:`telegram.Message`): Optional. New version of a message. channel_post (:class:`telegram.Message`): Optional. New incoming channel post. edited_channel_post (:class:`telegram.Message`): Optional. New version of a channel post. 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. callback_query (:class:`telegram.CallbackQuery`): Optional. New incoming callback query. shipping_query (:class:`telegram.ShippingQuery`): Optional. New incoming shipping query. pre_checkout_query (:class:`telegram.PreCheckoutQuery`): Optional. New incoming pre-checkout query. poll (:class:`telegram.Poll`): Optional. New poll state. Bots receive only updates about 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 ``'chat_member'`` in the list of ``'allowed_updates'`` to receive these updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, :meth:`telegram.ext.Updater.start_polling` and :meth:`telegram.ext.Updater.start_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 ``'can_invite_users'`` administrator right in the chat to receive these updates. .. versionadded:: 13.8 """ __slots__ = ( 'callback_query', 'chosen_inline_result', 'pre_checkout_query', 'inline_query', 'update_id', 'message', 'shipping_query', 'poll', 'poll_answer', 'channel_post', 'edited_channel_post', 'edited_message', '_effective_user', '_effective_chat', '_effective_message', 'my_chat_member', 'chat_member', 'chat_join_request', '_id_attrs', ) MESSAGE = constants.UPDATE_MESSAGE """:const:`telegram.constants.UPDATE_MESSAGE` .. versionadded:: 13.5""" EDITED_MESSAGE = constants.UPDATE_EDITED_MESSAGE """:const:`telegram.constants.UPDATE_EDITED_MESSAGE` .. versionadded:: 13.5""" CHANNEL_POST = constants.UPDATE_CHANNEL_POST """:const:`telegram.constants.UPDATE_CHANNEL_POST` .. versionadded:: 13.5""" EDITED_CHANNEL_POST = constants.UPDATE_EDITED_CHANNEL_POST """:const:`telegram.constants.UPDATE_EDITED_CHANNEL_POST` .. versionadded:: 13.5""" INLINE_QUERY = constants.UPDATE_INLINE_QUERY """:const:`telegram.constants.UPDATE_INLINE_QUERY` .. versionadded:: 13.5""" CHOSEN_INLINE_RESULT = constants.UPDATE_CHOSEN_INLINE_RESULT """:const:`telegram.constants.UPDATE_CHOSEN_INLINE_RESULT` .. versionadded:: 13.5""" CALLBACK_QUERY = constants.UPDATE_CALLBACK_QUERY """:const:`telegram.constants.UPDATE_CALLBACK_QUERY` .. versionadded:: 13.5""" SHIPPING_QUERY = constants.UPDATE_SHIPPING_QUERY """:const:`telegram.constants.UPDATE_SHIPPING_QUERY` .. versionadded:: 13.5""" PRE_CHECKOUT_QUERY = constants.UPDATE_PRE_CHECKOUT_QUERY """:const:`telegram.constants.UPDATE_PRE_CHECKOUT_QUERY` .. versionadded:: 13.5""" POLL = constants.UPDATE_POLL """:const:`telegram.constants.UPDATE_POLL` .. versionadded:: 13.5""" POLL_ANSWER = constants.UPDATE_POLL_ANSWER """:const:`telegram.constants.UPDATE_POLL_ANSWER` .. versionadded:: 13.5""" MY_CHAT_MEMBER = constants.UPDATE_MY_CHAT_MEMBER """:const:`telegram.constants.UPDATE_MY_CHAT_MEMBER` .. versionadded:: 13.5""" CHAT_MEMBER = constants.UPDATE_CHAT_MEMBER """:const:`telegram.constants.UPDATE_CHAT_MEMBER` .. versionadded:: 13.5""" CHAT_JOIN_REQUEST = constants.UPDATE_CHAT_JOIN_REQUEST """:const:`telegram.constants.UPDATE_CHAT_JOIN_REQUEST` .. versionadded:: 13.8""" ALL_TYPES = constants.UPDATE_ALL_TYPES """:const:`telegram.constants.UPDATE_ALL_TYPES` .. versionadded:: 13.5""" def __init__( self, update_id: int, message: Message = None, edited_message: Message = None, channel_post: Message = None, edited_channel_post: Message = None, inline_query: InlineQuery = None, chosen_inline_result: ChosenInlineResult = None, callback_query: CallbackQuery = None, shipping_query: ShippingQuery = None, pre_checkout_query: PreCheckoutQuery = None, poll: Poll = None, poll_answer: PollAnswer = None, my_chat_member: ChatMemberUpdated = None, chat_member: ChatMemberUpdated = None, chat_join_request: ChatJoinRequest = None, **_kwargs: Any, ): # Required self.update_id = int(update_id) # Optionals self.message = message self.edited_message = edited_message self.inline_query = inline_query self.chosen_inline_result = chosen_inline_result self.callback_query = callback_query self.shipping_query = shipping_query self.pre_checkout_query = pre_checkout_query self.channel_post = channel_post self.edited_channel_post = edited_channel_post self.poll = poll self.poll_answer = poll_answer self.my_chat_member = my_chat_member self.chat_member = chat_member self.chat_join_request = chat_join_request self._effective_user: Optional['User'] = None self._effective_chat: Optional['Chat'] = None self._effective_message: Optional[Message] = None self._id_attrs = (self.update_id,) @property def effective_user(self) -> Optional['User']: """ :class:`telegram.User`: The user that sent this update, no matter what kind of update this is. Will be :obj:`None` for :attr:`channel_post` and :attr:`poll`. """ 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 self._effective_user = user return user @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. Will be :obj:`None` for :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll` and :attr:`poll_answer`. """ 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 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. Will be :obj:`None` for :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`, :attr:`my_chat_member`, :attr:`chat_member` as well as :attr:`chat_join_request` in case the bot is missing the :attr:`telegram.ChatPermissions.can_invite_users` administrator right in the chat. """ if self._effective_message: return self._effective_message message = None if self.message: message = self.message elif self.edited_message: message = self.edited_message elif self.callback_query: message = self.callback_query.message elif self.channel_post: message = self.channel_post elif self.edited_channel_post: message = self.edited_channel_post 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) return cls(**data) python-telegram-bot-13.11/telegram/user.py000066400000000000000000001265641417656324400206020ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=W0622 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, List, Optional, Union, Tuple from telegram import TelegramObject, constants from telegram.inline.inlinekeyboardbutton import InlineKeyboardButton from telegram.utils.helpers import ( mention_html as util_mention_html, DEFAULT_NONE, DEFAULT_20, ) from telegram.utils.helpers import mention_markdown as util_mention_markdown from telegram.utils.types import JSONDict, FileInput, ODVInput, DVInput if TYPE_CHECKING: from telegram import ( Bot, Message, UserProfilePhotos, MessageId, InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo, MessageEntity, ReplyMarkup, PhotoSize, Audio, Contact, Document, InlineKeyboardMarkup, LabeledPrice, Location, Animation, Sticker, Video, Venue, 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. 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 bots first name. last_name (:obj:`str`, optional): User's or bots last name. username (:obj:`str`, optional): User's or bots 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. 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. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ __slots__ = ( 'is_bot', 'can_read_all_group_messages', 'username', 'first_name', 'last_name', 'can_join_groups', 'supports_inline_queries', 'id', 'bot', 'language_code', '_id_attrs', ) def __init__( self, id: int, first_name: str, is_bot: bool, last_name: str = None, username: str = None, language_code: str = None, can_join_groups: bool = None, can_read_all_group_messages: bool = None, supports_inline_queries: bool = None, bot: 'Bot' = None, **_kwargs: Any, ): # Required self.id = int(id) # pylint: disable=C0103 self.first_name = first_name self.is_bot = is_bot # Optionals self.last_name = last_name self.username = username self.language_code = language_code self.can_join_groups = can_join_groups self.can_read_all_group_messages = can_read_all_group_messages self.supports_inline_queries = supports_inline_queries self.bot = bot self._id_attrs = (self.id,) @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 def get_profile_photos( self, offset: int = None, limit: int = 100, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> Optional['UserProfilePhotos']: """ Shortcut for:: 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 self.bot.get_user_profile_photos( user_id=self.id, offset=offset, limit=limit, timeout=timeout, api_kwargs=api_kwargs, ) def mention_markdown(self, name: str = None) -> str: """ Note: :attr:`telegram.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 util_mention_markdown(self.id, name) return util_mention_markdown(self.id, self.full_name) def mention_markdown_v2(self, name: 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 util_mention_markdown(self.id, name, version=2) return util_mention_markdown(self.id, self.full_name, version=2) def mention_html(self, name: 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 util_mention_html(self.id, name) return util_mention_html(self.id, self.full_name) def mention_button(self, name: 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}") def pin_message( self, message_id: int, disable_notification: ODVInput[bool] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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`. Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.bot.pin_chat_message( chat_id=self.id, message_id=message_id, disable_notification=disable_notification, timeout=timeout, api_kwargs=api_kwargs, ) def unpin_message( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, message_id: int = None, ) -> bool: """Shortcut for:: 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`. Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.bot.unpin_chat_message( chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs, message_id=message_id, ) def unpin_all_messages( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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`. Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.bot.unpin_all_chat_messages( chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs, ) def send_message( self, text: str, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_message(update.effective_user.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 self.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, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, ) def send_photo( self, photo: Union[FileInput, 'PhotoSize'], caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_photo(update.effective_user.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 self.bot.send_photo( chat_id=self.id, photo=photo, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=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, ) def send_media_group( self, media: List[ Union['InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo'] ], disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> List['Message']: """Shortcut for:: bot.send_media_group(update.effective_user.id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`. Returns: List[:class:`telegram.Message`:] On success, instance representing the message posted. """ return self.bot.send_media_group( chat_id=self.id, media=media, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def send_audio( self, audio: Union[FileInput, 'Audio'], duration: int = None, performer: str = None, title: str = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_audio(update.effective_user.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 self.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_markup=reply_markup, timeout=timeout, parse_mode=parse_mode, thumb=thumb, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, filename=filename, protect_content=protect_content, ) def send_chat_action( self, action: str, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: bot.send_chat_action(update.effective_user.id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`. Returns: :obj:`True`: On success. """ return self.bot.send_chat_action( chat_id=self.id, action=action, timeout=timeout, api_kwargs=api_kwargs, ) send_action = send_chat_action """Alias for :attr:`send_chat_action`""" def send_contact( self, phone_number: str = None, first_name: str = None, last_name: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, contact: 'Contact' = None, vcard: str = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_contact(update.effective_user.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 self.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_markup=reply_markup, timeout=timeout, contact=contact, vcard=vcard, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def send_dice( self, disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, emoji: str = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_dice(update.effective_user.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 self.bot.send_dice( chat_id=self.id, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, emoji=emoji, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def send_document( self, document: Union[FileInput, 'Document'], filename: str = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, thumb: FileInput = None, api_kwargs: JSONDict = None, disable_content_type_detection: bool = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_document(update.effective_user.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 self.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_markup=reply_markup, timeout=timeout, parse_mode=parse_mode, thumb=thumb, 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, ) def send_game( self, game_short_name: str, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'InlineKeyboardMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_game(update.effective_user.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 self.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_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def send_invoice( self, title: str, description: str, payload: str, provider_token: str, currency: str, prices: List['LabeledPrice'], start_parameter: str = None, photo_url: str = None, photo_size: int = None, photo_width: int = None, photo_height: int = None, need_name: bool = None, need_phone_number: bool = None, need_email: bool = None, need_shipping_address: bool = None, is_flexible: bool = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'InlineKeyboardMarkup' = None, provider_data: Union[str, object] = None, send_phone_number_to_provider: bool = None, send_email_to_provider: bool = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: 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 :attr:`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 :attr:`start_parameter` is optional. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.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, timeout=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, ) def send_location( self, latitude: float = None, longitude: float = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, location: 'Location' = None, live_period: int = None, api_kwargs: JSONDict = None, horizontal_accuracy: float = None, heading: int = None, proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_location(update.effective_user.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 self.bot.send_location( chat_id=self.id, latitude=latitude, longitude=longitude, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=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, ) def send_animation( self, animation: Union[FileInput, 'Animation'], duration: int = None, width: int = None, height: int = None, thumb: FileInput = None, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_animation(update.effective_user.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 self.bot.send_animation( chat_id=self.id, animation=animation, duration=duration, width=width, height=height, thumb=thumb, caption=caption, parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, filename=filename, protect_content=protect_content, ) def send_sticker( self, sticker: Union[FileInput, 'Sticker'], disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_sticker(update.effective_user.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 self.bot.send_sticker( chat_id=self.id, sticker=sticker, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, ) def send_video( self, video: Union[FileInput, 'Video'], duration: int = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, width: int = None, height: int = None, parse_mode: ODVInput[str] = DEFAULT_NONE, supports_streaming: bool = None, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_video(update.effective_user.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 self.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_markup=reply_markup, timeout=timeout, width=width, height=height, parse_mode=parse_mode, supports_streaming=supports_streaming, thumb=thumb, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, filename=filename, protect_content=protect_content, ) def send_venue( self, latitude: float = None, longitude: float = None, title: str = None, address: str = None, foursquare_id: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, venue: 'Venue' = None, foursquare_type: str = None, api_kwargs: JSONDict = None, google_place_id: str = None, google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_venue(update.effective_user.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 self.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_markup=reply_markup, timeout=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, ) def send_video_note( self, video_note: Union[FileInput, 'VideoNote'], duration: int = None, length: int = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, thumb: FileInput = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_video_note(update.effective_user.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 self.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_markup=reply_markup, timeout=timeout, thumb=thumb, api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, ) def send_voice( self, voice: Union[FileInput, 'Voice'], duration: int = None, caption: str = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: DVInput[float] = DEFAULT_20, parse_mode: ODVInput[str] = DEFAULT_NONE, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_voice(update.effective_user.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 self.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_markup=reply_markup, timeout=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, ) def send_poll( self, question: str, options: List[str], is_anonymous: bool = True, # We use constant.POLL_REGULAR instead of Poll.REGULAR here to avoid circular imports type: str = constants.POLL_REGULAR, # pylint: disable=W0622 allows_multiple_answers: bool = False, correct_option_id: int = None, is_closed: bool = None, disable_notification: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, explanation: str = None, explanation_parse_mode: ODVInput[str] = DEFAULT_NONE, open_period: int = None, close_date: Union[int, datetime] = None, api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, ) -> 'Message': """Shortcut for:: bot.send_poll(update.effective_user.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 self.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_markup=reply_markup, timeout=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, ) def send_copy( self, from_chat_id: Union[str, int], message_id: int, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, ) -> 'MessageId': """Shortcut for:: bot.copy_message(chat_id=update.effective_user.id, *args, **kwargs) For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.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, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) def copy_message( self, chat_id: Union[int, str], message_id: int, caption: str = None, parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None, disable_notification: DVInput[bool] = DEFAULT_NONE, reply_to_message_id: int = None, allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: 'ReplyMarkup' = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, ) -> 'MessageId': """Shortcut for:: 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`. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.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, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, ) def approve_join_request( self, chat_id: Union[int, str], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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`. .. versionadded:: 13.8 Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.bot.approve_chat_join_request( user_id=self.id, chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs ) def decline_join_request( self, chat_id: Union[int, str], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: """Shortcut for:: 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`. .. versionadded:: 13.8 Returns: :obj:`bool`: On success, :obj:`True` is returned. """ return self.bot.decline_chat_join_request( user_id=self.id, chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs ) python-telegram-bot-13.11/telegram/userprofilephotos.py000066400000000000000000000053701417656324400234070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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, Any, List, Optional from telegram import PhotoSize, TelegramObject from telegram.utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot class UserProfilePhotos(TelegramObject): """This object represent 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 (List[List[:class:`telegram.PhotoSize`]]): Requested profile pictures (in up to 4 sizes each). Attributes: total_count (:obj:`int`): Total number of profile pictures. photos (List[List[:class:`telegram.PhotoSize`]]): Requested profile pictures. """ __slots__ = ('photos', 'total_count', '_id_attrs') def __init__(self, total_count: int, photos: List[List[PhotoSize]], **_kwargs: Any): # Required self.total_count = int(total_count) self.photos = photos self._id_attrs = (self.total_count, self.photos) @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 cls(**data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data['photos'] = [] for photo in self.photos: data['photos'].append([x.to_dict() for x in photo]) return data def __hash__(self) -> int: return hash(tuple(tuple(p for p in photo) for photo in self.photos)) python-telegram-bot-13.11/telegram/utils/000077500000000000000000000000001417656324400203745ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/utils/__init__.py000066400000000000000000000000001417656324400224730ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/utils/deprecate.py000066400000000000000000000037551417656324400227140ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module facilitates the deprecation of functions.""" import warnings # We use our own DeprecationWarning since they are muted by default and "UserWarning" makes it # seem like it's the user that issued the warning # We name it something else so that you don't get confused when you attempt to suppress it class TelegramDeprecationWarning(Warning): """Custom warning class for deprecations in this library.""" __slots__ = () # Function to warn users that setting custom attributes is deprecated (Use only in __setattr__!) # Checks if a custom attribute is added by checking length of dictionary before & after # assigning attribute. This is the fastest way to do it (I hope!). def set_new_attribute_deprecated(self: object, key: str, value: object) -> None: """Warns the user if they set custom attributes on PTB objects.""" org = len(self.__dict__) object.__setattr__(self, key, value) new = len(self.__dict__) if new > org: warnings.warn( f"Setting custom attributes such as {key!r} on objects such as " f"{self.__class__.__name__!r} of the PTB library is deprecated.", TelegramDeprecationWarning, stacklevel=3, ) python-telegram-bot-13.11/telegram/utils/helpers.py000066400000000000000000000503021417656324400224100ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" import datetime as dtm # dtm = "DateTime Module" import re import signal import time from collections import defaultdict from html import escape from pathlib import Path from typing import ( TYPE_CHECKING, Any, DefaultDict, Dict, Optional, Tuple, Union, Type, cast, IO, TypeVar, Generic, overload, ) from telegram.utils.types import JSONDict, FileInput if TYPE_CHECKING: from telegram import Message, Update, TelegramObject, InputFile # in PTB-Raw we don't have pytz, 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] try: import ujson as json except ImportError: import json # type: ignore[no-redef] # From https://stackoverflow.com/questions/2549939/get-signal-names-from-numbers-in-python _signames = { v: k for k, v in reversed(sorted(vars(signal).items())) if k.startswith('SIG') and not k.startswith('SIG_') } def get_signal_name(signum: int) -> str: """Returns the signal name of the given signal number.""" return _signames[signum] def is_local_file(obj: Optional[Union[str, Path]]) -> 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( file_input: Union[FileInput, 'TelegramObject'], tg_type: Type['TelegramObject'] = None, attach: bool = None, filename: str = None, ) -> Union[str, 'InputFile', Any]: """ Parses input for sending files: * For string input, if the input is an absolute path of a local file, adds the ``file://`` prefix. If the input is a relative path of a local file, computes the absolute path and adds the ``file://`` prefix. 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` | `filelike 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`. attach (:obj:`bool`, optional): Whether this file should be send as one file or is part of a collection of files. Only relevant in case an :class:`telegram.InputFile` is returned. filename (:obj:`str`, optional): The filename. Only relevant in case an :class:`telegram.InputFile` is returned. 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=C0415 if isinstance(file_input, str) and file_input.startswith('file://'): return file_input if isinstance(file_input, (str, Path)): if is_local_file(file_input): out = Path(file_input).absolute().as_uri() else: out = file_input # type: ignore[assignment] return out if isinstance(file_input, bytes): return InputFile(file_input, attach=attach, filename=filename) if InputFile.is_file(file_input): file_input = cast(IO, file_input) return InputFile(file_input, attach=attach, filename=filename) if tg_type and isinstance(file_input, tg_type): return file_input.file_id # type: ignore[attr-defined] return file_input def escape_markdown(text: str, version: int = 1, entity_type: str = None) -> str: """ Helper function to escape telegram markup symbols. 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 ``PRE``, ``CODE`` and the link part of ``TEXT_LINKS``, only certain characters need to be escaped in ``MarkdownV2``. 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 == 'text_link': 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) # -------- date/time related helpers -------- 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() 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[int, float, dtm.timedelta, dtm.datetime, dtm.time], reference_timestamp: float = None, tzinfo: 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. object 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:`int` | :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:`int` or :obj:`float` will be interpreted as "seconds from ``reference_t``" * :obj:`datetime.timedelta` will be interpreted as "time increment from ``reference_t``" * :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 ``t`` is given as an :obj:`int`, indicating "seconds from ``reference_t``"). Defaults to now (the time at which this function is called). If ``t`` is given as an absolute representation of date & time (i.e. a :obj:`datetime.datetime` object), ``reference_timestamp`` is not relevant and so its value should be :obj:`None`. If this is not the case, a ``ValueError`` will be raised. tzinfo (:obj:`pytz.BaseTzInfo`, optional): If ``t`` is a naive object from the :class:`datetime` module, it will be interpreted as this timezone. Defaults to ``pytz.utc``. Note: Only to be used by ``telegram.ext``. Returns: :obj:`float` | :obj:`None`: The return value depends on the type of argument ``t``. If ``t`` is given as a time increment (i.e. as a :obj:`int`, :obj:`float` or :obj:`datetime.timedelta`), then the return value will be ``reference_t`` + ``t``. 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 ``t``'s type is not one of those described above. ValueError: If ``t`` is a :obj:`datetime.datetime` and :obj:`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[int, float, dtm.timedelta, dtm.datetime, dtm.time, None], reference_timestamp: float = None, tzinfo: 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: dtm.tzinfo = UTC) -> 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 UTC. Returns: Timezone aware equivalent :obj:`datetime.datetime` value if ``unixtime`` is not :obj:`None`; else :obj:`None`. """ if unixtime is None: return None if tzinfo is not None: return dtm.datetime.fromtimestamp(unixtime, tz=tzinfo) return dtm.datetime.utcfromtimestamp(unixtime) # -------- end -------- def mention_html(user_id: Union[int, str], name: str) -> str: """ 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: int = 1) -> str: """ 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. """ return f'[{escape_markdown(name, version=version)}](tg://user?id={user_id})' 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`: One of ``Message.MESSAGE_TYPES`` """ # Importing on file-level yields cyclic Import Errors from telegram import Message, Update # pylint: disable=C0415 if isinstance(entity, Message): message = entity elif isinstance(entity, Update): message = entity.effective_message # type: ignore[assignment] else: raise TypeError(f"entity is not Message or Update (got: {type(entity)})") for i in Message.MESSAGE_TYPES: if getattr(message, i, None): return i return None def create_deep_linked_url(bot_username: str, payload: str = None, group: bool = False) -> str: """ Creates a deep-linked URL for this ``bot_username`` with the specified ``payload``. See https://core.telegram.org/bots#deep-linking to learn more. The ``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")`` 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 """ 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 -" ) if group: key = 'startgroup' else: key = 'start' return f'{base_url}?{key}={payload}' def encode_conversations_to_json(conversations: Dict[str, Dict[Tuple, object]]) -> str: """Helper method to encode a conversations dict (that uses tuples as keys) to a JSON-serializable way. Use :meth:`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) def decode_conversations_from_json(json_string: str) -> Dict[str, Dict[Tuple, object]]: """Helper method to decode a conversations dict (that uses tuples as keys) from a JSON-string created with :meth:`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, Dict[Tuple, object]] = {} for handler, states in tmp.items(): conversations[handler] = {} for key, state in states.items(): conversations[handler][tuple(json.loads(key))] = state return conversations def decode_user_chat_data_from_json(data: str) -> DefaultDict[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: DefaultDict[int, Dict[object, object]] = defaultdict(dict) decoded_data = json.loads(data) for user, user_data in decoded_data.items(): user = int(user) tmp[user] = {} for key, value in user_data.items(): try: key = int(key) except ValueError: pass tmp[user][key] = value return tmp DVType = TypeVar('DVType', bound=object) 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:: DefaultOne = DefaultValue(1) def f(arg=DefaultOne): if arg is DefaultOne: 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 (:obj:`obj`): The value of the default argument Attributes: value (:obj:`obj`): The value of the default argument """ __slots__ = ('value', '__dict__') def __init__(self, value: DVType = None): self.value = value def __bool__(self) -> bool: return bool(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 # type: ignore[return-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) DEFAULT_NONE: DefaultValue = DefaultValue(None) """:class:`DefaultValue`: Default :obj:`None`""" DEFAULT_FALSE: DefaultValue = DefaultValue(False) """:class:`DefaultValue`: Default :obj:`False`""" DEFAULT_20: DefaultValue = DefaultValue(20) """:class:`DefaultValue`: Default :obj:`20`""" python-telegram-bot-13.11/telegram/utils/promise.py000066400000000000000000000025061417656324400224270ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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:`telegram.ext.utils.promise.Promise` class for backwards compatibility. """ import warnings import telegram.ext.utils.promise as promise from telegram.utils.deprecate import TelegramDeprecationWarning warnings.warn( 'telegram.utils.promise is deprecated. Please use telegram.ext.utils.promise instead.', TelegramDeprecationWarning, ) Promise = promise.Promise """ :class:`telegram.ext.utils.promise.Promise` .. deprecated:: v13.2 Use :class:`telegram.ext.utils.promise.Promise` instead. """ python-telegram-bot-13.11/telegram/utils/request.py000066400000000000000000000365461417656324400224540ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" import logging import os import socket import sys import warnings try: import ujson as json except ImportError: import json # type: ignore[no-redef] from typing import Any, Union import certifi try: import telegram.vendor.ptb_urllib3.urllib3 as urllib3 import telegram.vendor.ptb_urllib3.urllib3.contrib.appengine as appengine from telegram.vendor.ptb_urllib3.urllib3.connection import HTTPConnection from telegram.vendor.ptb_urllib3.urllib3.fields import RequestField from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout except ImportError: # pragma: no cover try: import urllib3 # type: ignore[no-redef] import urllib3.contrib.appengine as appengine # type: ignore[no-redef] from urllib3.connection import HTTPConnection # type: ignore[no-redef] from urllib3.fields import RequestField # type: ignore[no-redef] from urllib3.util.timeout import Timeout # type: ignore[no-redef] warnings.warn( 'python-telegram-bot is using upstream urllib3. This is allowed but not ' 'supported by python-telegram-bot maintainers.' ) except ImportError: warnings.warn( "python-telegram-bot wasn't properly installed. Please refer to README.rst on " "how to properly install." ) raise # pylint: disable=C0412 from telegram import InputFile, TelegramError from telegram.error import ( BadRequest, ChatMigrated, Conflict, InvalidToken, NetworkError, RetryAfter, TimedOut, Unauthorized, ) from telegram.utils.types import JSONDict from telegram.utils.deprecate import set_new_attribute_deprecated def _render_part(self: RequestField, name: str, value: str) -> str: # pylint: disable=W0613 r""" Monkey patch urllib3.urllib3.fields.RequestField to make it *not* support RFC2231 compliant Content-Disposition headers since telegram servers don't understand it. Instead just escape \\ and " and replace any \n and \r with a space. """ value = value.replace('\\', '\\\\').replace('"', '\\"') value = value.replace('\r', ' ').replace('\n', ' ') return f'{name}="{value}"' RequestField._render_part = _render_part # type: ignore # pylint: disable=W0212 logging.getLogger('telegram.vendor.ptb_urllib3.urllib3').setLevel(logging.WARNING) USER_AGENT = 'Python Telegram Bot (https://github.com/python-telegram-bot/python-telegram-bot)' class Request: """ Helper class for python-telegram-bot which provides methods to perform POST & GET towards Telegram servers. Args: con_pool_size (:obj:`int`): Number of connections to keep in the connection pool. proxy_url (:obj:`str`): The URL to the proxy server. For example: `http://127.0.0.1:3128`. urllib3_proxy_kwargs (:obj:`dict`): Arbitrary arguments passed as-is to :obj:`urllib3.ProxyManager`. This value will be ignored if :attr:`proxy_url` is not set. connect_timeout (:obj:`int` | :obj:`float`): The maximum amount of time (in seconds) to wait for a connection attempt to a server to succeed. :obj:`None` will set an infinite timeout for connection attempts. Defaults to ``5.0``. read_timeout (:obj:`int` | :obj:`float`): The maximum amount of time (in seconds) to wait between consecutive read operations for a response from the server. :obj:`None` will set an infinite timeout. This value is usually overridden by the various :class:`telegram.Bot` methods. Defaults to ``5.0``. """ __slots__ = ('_connect_timeout', '_con_pool_size', '_con_pool', '__dict__') def __init__( self, con_pool_size: int = 1, proxy_url: str = None, urllib3_proxy_kwargs: JSONDict = None, connect_timeout: float = 5.0, read_timeout: float = 5.0, ): if urllib3_proxy_kwargs is None: urllib3_proxy_kwargs = {} self._connect_timeout = connect_timeout sockopts = HTTPConnection.default_socket_options + [ (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) ] # TODO: Support other platforms like mac and windows. if 'linux' in sys.platform: sockopts.append( (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 120) # pylint: disable=no-member ) sockopts.append( (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30) # pylint: disable=no-member ) sockopts.append( (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 8) # pylint: disable=no-member ) self._con_pool_size = con_pool_size kwargs = dict( maxsize=con_pool_size, cert_reqs='CERT_REQUIRED', ca_certs=certifi.where(), socket_options=sockopts, timeout=urllib3.Timeout(connect=self._connect_timeout, read=read_timeout, total=None), ) # Set a proxy according to the following order: # * proxy defined in proxy_url (+ urllib3_proxy_kwargs) # * proxy set in `HTTPS_PROXY` env. var. # * proxy set in `https_proxy` env. var. # * None (if no proxy is configured) if not proxy_url: proxy_url = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy') self._con_pool: Union[ urllib3.PoolManager, appengine.AppEngineManager, 'SOCKSProxyManager', # noqa: F821 urllib3.ProxyManager, ] = None # type: ignore if not proxy_url: if appengine.is_appengine_sandbox(): # Use URLFetch service if running in App Engine self._con_pool = appengine.AppEngineManager() else: self._con_pool = urllib3.PoolManager(**kwargs) else: kwargs.update(urllib3_proxy_kwargs) if proxy_url.startswith('socks'): try: # pylint: disable=C0415 from telegram.vendor.ptb_urllib3.urllib3.contrib.socks import SOCKSProxyManager except ImportError as exc: raise RuntimeError('PySocks is missing') from exc self._con_pool = SOCKSProxyManager(proxy_url, **kwargs) else: mgr = urllib3.proxy_from_url(proxy_url, **kwargs) if mgr.proxy.auth: # TODO: what about other auth types? auth_hdrs = urllib3.make_headers(proxy_basic_auth=mgr.proxy.auth) mgr.proxy_headers.update(auth_hdrs) self._con_pool = mgr def __setattr__(self, key: str, value: object) -> None: set_new_attribute_deprecated(self, key, value) @property def con_pool_size(self) -> int: """The size of the connection pool used.""" return self._con_pool_size def stop(self) -> None: """Performs cleanup on shutdown.""" self._con_pool.clear() # type: ignore @staticmethod def _parse(json_data: bytes) -> Union[JSONDict, bool]: """Try and parse the JSON returned from Telegram. Returns: dict: A JSON parsed as Python dict with results - on error this dict will be empty. """ decoded_s = json_data.decode('utf-8', 'replace') try: data = json.loads(decoded_s) except ValueError as exc: raise TelegramError('Invalid server response') from exc if not data.get('ok'): # pragma: no cover description = data.get('description') parameters = 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) if description: return description return data['result'] def _request_wrapper(self, *args: object, **kwargs: Any) -> bytes: """Wraps urllib3 request for handling known exceptions. Args: args: unnamed arguments, passed to urllib3 request. kwargs: keyword arguments, passed to urllib3 request. Returns: bytes: A non-parsed JSON text. Raises: TelegramError """ # Make sure to hint Telegram servers that we reuse connections by sending # "Connection: keep-alive" in the HTTP headers. if 'headers' not in kwargs: kwargs['headers'] = {} kwargs['headers']['connection'] = 'keep-alive' # Also set our user agent kwargs['headers']['user-agent'] = USER_AGENT try: resp = self._con_pool.request(*args, **kwargs) except urllib3.exceptions.TimeoutError as error: raise TimedOut() from error except urllib3.exceptions.HTTPError as error: # HTTPError must come last as its the base urllib3 exception class # TODO: do something smart here; for now just raise NetworkError raise NetworkError(f'urllib3 HTTPError {error}') from error if 200 <= resp.status <= 299: # 200-299 range are HTTP success statuses return resp.data try: message = str(self._parse(resp.data)) except ValueError: message = 'Unknown HTTPError' if resp.status in (401, 403): raise Unauthorized(message) if resp.status == 400: raise BadRequest(message) if resp.status == 404: raise InvalidToken() if resp.status == 409: raise Conflict(message) if resp.status == 413: raise NetworkError( 'File too large. Check telegram api limits ' 'https://core.telegram.org/bots/api#senddocument' ) if resp.status == 502: raise NetworkError('Bad Gateway') raise NetworkError(f'{message} ({resp.status})') def post(self, url: str, data: JSONDict, timeout: float = None) -> Union[JSONDict, bool]: """Request an URL. Args: url (:obj:`str`): The web location we want to retrieve. data (Dict[:obj:`str`, :obj:`str` | :obj:`int`], optional): A dict of key/value pairs. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). Returns: A JSON object. """ urlopen_kwargs = {} if timeout is not None: urlopen_kwargs['timeout'] = Timeout(read=timeout, connect=self._connect_timeout) if data is None: data = {} # Are we uploading files? files = False # pylint: disable=R1702 for key, val in data.copy().items(): if isinstance(val, InputFile): # Convert the InputFile to urllib3 field format data[key] = val.field_tuple files = True elif isinstance(val, (float, int)): # Urllib3 doesn't like floats it seems data[key] = str(val) elif key == 'media': files = True # List of media if isinstance(val, list): # Attach and set val to attached name for all media = [] for med in val: media_dict = med.to_dict() media.append(media_dict) if isinstance(med.media, InputFile): data[med.media.attach] = med.media.field_tuple # if the file has a thumb, we also need to attach it to the data if "thumb" in media_dict: data[med.thumb.attach] = med.thumb.field_tuple data[key] = json.dumps(media) # Single media else: # Attach and set val to attached name media_dict = val.to_dict() if isinstance(val.media, InputFile): data[val.media.attach] = val.media.field_tuple # if the file has a thumb, we also need to attach it to the data if "thumb" in media_dict: data[val.thumb.attach] = val.thumb.field_tuple data[key] = json.dumps(media_dict) elif isinstance(val, list): # In case we're sending files, we need to json-dump lists manually # As we can't know if that's the case, we just json-dump here data[key] = json.dumps(val) # Use multipart upload if we're uploading files, otherwise use JSON if files: result = self._request_wrapper('POST', url, fields=data, **urlopen_kwargs) else: result = self._request_wrapper( 'POST', url, body=json.dumps(data).encode('utf-8'), headers={'Content-Type': 'application/json'}, **urlopen_kwargs, ) return self._parse(result) def retrieve(self, url: str, timeout: float = None) -> bytes: """Retrieve the contents of a file by its URL. Args: url (:obj:`str`): The web location we want to retrieve. timeout (:obj:`int` | :obj:`float`): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). """ urlopen_kwargs = {} if timeout is not None: urlopen_kwargs['timeout'] = Timeout(read=timeout, connect=self._connect_timeout) return self._request_wrapper('GET', url, **urlopen_kwargs) def download(self, url: str, filename: str, timeout: float = None) -> None: """Download a file by its URL. Args: url (:obj:`str`): The web location we want to retrieve. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). filename (:obj:`str`): The filename within the path to download the file. """ buf = self.retrieve(url, timeout=timeout) with open(filename, 'wb') as fobj: fobj.write(buf) python-telegram-bot-13.11/telegram/utils/types.py000066400000000000000000000041021417656324400221070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" from pathlib import Path from typing import ( IO, TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypeVar, Union, ) if TYPE_CHECKING: from telegram import InputFile # noqa: F401 from telegram.utils.helpers import DefaultValue # noqa: F401 FileLike = Union[IO, 'InputFile'] """Either an open file handler or a :class:`telegram.InputFile`.""" FileInput = Union[str, bytes, FileLike, Path] """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.""" DVType = TypeVar('DVType') ODVInput = Optional[Union['DefaultValue[DVType]', DVType]] """Generic type for bot method parameters which can have defaults. ``ODVInput[type]`` is the same as ``Optional[Union[DefaultValue, type]]``.""" DVInput = Union['DefaultValue[DVType]', DVType] """Generic type for bot method parameters which can have defaults. ``DVInput[type]`` is the same as ``Union[DefaultValue, type]``.""" RT = TypeVar("RT") SLT = Union[RT, List[RT], Tuple[RT, ...]] """Single instance or list/tuple of instances.""" python-telegram-bot-13.11/telegram/utils/webhookhandler.py000066400000000000000000000025471417656324400237520ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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:`telegram.ext.utils.webhookhandler.WebhookHandler` class for backwards compatibility. """ import warnings import telegram.ext.utils.webhookhandler as webhook_handler from telegram.utils.deprecate import TelegramDeprecationWarning warnings.warn( 'telegram.utils.webhookhandler is deprecated. Please use telegram.ext.utils.webhookhandler ' 'instead.', TelegramDeprecationWarning, ) WebhookHandler = webhook_handler.WebhookHandler WebhookServer = webhook_handler.WebhookServer WebhookAppClass = webhook_handler.WebhookAppClass python-telegram-bot-13.11/telegram/vendor/000077500000000000000000000000001417656324400205315ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/vendor/__init__.py000066400000000000000000000000001417656324400226300ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/vendor/ptb_urllib3/000077500000000000000000000000001417656324400227525ustar00rootroot00000000000000python-telegram-bot-13.11/telegram/version.py000066400000000000000000000016751417656324400213040ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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=C0114 from telegram import constants __version__ = '13.11' bot_api_version = constants.BOT_API_VERSION # pylint: disable=C0103 python-telegram-bot-13.11/telegram/voicechat.py000066400000000000000000000121311417656324400215510ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 voice chats.""" import datetime as dtm from typing import TYPE_CHECKING, Any, Optional, List from telegram import TelegramObject, User from telegram.utils.helpers import from_timestamp, to_timestamp from telegram.utils.types import JSONDict if TYPE_CHECKING: from telegram import Bot class VoiceChatStarted(TelegramObject): """ This object represents a service message about a voice chat started in the chat. Currently holds no information. .. versionadded:: 13.4 """ __slots__ = () def __init__(self, **_kwargs: Any): # skipcq: PTC-W0049 pass class VoiceChatEnded(TelegramObject): """ This object represents a service message about a voice 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 Args: duration (:obj:`int`): Voice chat duration in seconds. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: duration (:obj:`int`): Voice chat duration in seconds. """ __slots__ = ('duration', '_id_attrs') def __init__(self, duration: int, **_kwargs: Any) -> None: self.duration = int(duration) if duration is not None else None self._id_attrs = (self.duration,) class VoiceChatParticipantsInvited(TelegramObject): """ This object represents a service message about new members invited to a voice 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 Args: users (List[:class:`telegram.User`]): New members that were invited to the voice chat. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: users (List[:class:`telegram.User`]): New members that were invited to the voice chat. """ __slots__ = ('users', '_id_attrs') def __init__(self, users: List[User], **_kwargs: Any) -> None: self.users = users self._id_attrs = (self.users,) def __hash__(self) -> int: return hash(tuple(self.users)) @classmethod def de_json( cls, data: Optional[JSONDict], bot: 'Bot' ) -> Optional['VoiceChatParticipantsInvited']: """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 cls(**data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() data["users"] = [u.to_dict() for u in self.users] return data class VoiceChatScheduled(TelegramObject): """This object represents a service message about a voice 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. Args: start_date (:obj:`datetime.datetime`): Point in time (Unix timestamp) when the voice chat is supposed to be started by a chat administrator **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: start_date (:obj:`datetime.datetime`): Point in time (Unix timestamp) when the voice chat is supposed to be started by a chat administrator """ __slots__ = ('start_date', '_id_attrs') def __init__(self, start_date: dtm.datetime, **_kwargs: Any) -> None: self.start_date = start_date self._id_attrs = (self.start_date,) @classmethod def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['VoiceChatScheduled']: """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) if not data: return None data['start_date'] = from_timestamp(data['start_date']) return cls(**data, bot=bot) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data = super().to_dict() # Required data['start_date'] = to_timestamp(self.start_date) return data python-telegram-bot-13.11/telegram/webhookinfo.py000066400000000000000000000110711417656324400221200ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 typing import Any, List from telegram import TelegramObject 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` and :attr:`allowed_updates` are equal. 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 (:obj:`int`, optional): Unix time for the most recent error that happened when trying to deliver an update via webhook. 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 (List[:obj:`str`], optional): A list of update types the bot is subscribed to. Defaults to all update types, except :attr:`telegram.Update.chat_member`. Attributes: url (:obj:`str`): Webhook URL. has_custom_certificate (:obj:`bool`): If a custom certificate was provided for webhook. pending_update_count (:obj:`int`): Number of updates awaiting delivery. ip_address (:obj:`str`): Optional. Currently used webhook IP address. last_error_date (:obj:`int`): Optional. Unix time for the most recent error that happened. last_error_message (:obj:`str`): Optional. Error message in human-readable format. max_connections (:obj:`int`): Optional. Maximum allowed number of simultaneous HTTPS connections. allowed_updates (List[:obj:`str`]): Optional. A list of update types the bot is subscribed to. Defaults to all update types, except :attr:`telegram.Update.chat_member`. """ __slots__ = ( 'allowed_updates', 'url', 'max_connections', 'last_error_date', 'ip_address', 'last_error_message', 'pending_update_count', 'has_custom_certificate', '_id_attrs', ) def __init__( self, url: str, has_custom_certificate: bool, pending_update_count: int, last_error_date: int = None, last_error_message: str = None, max_connections: int = None, allowed_updates: List[str] = None, ip_address: str = None, **_kwargs: Any, ): # Required self.url = url self.has_custom_certificate = has_custom_certificate self.pending_update_count = pending_update_count # Optional self.ip_address = ip_address self.last_error_date = last_error_date self.last_error_message = last_error_message self.max_connections = max_connections self.allowed_updates = allowed_updates 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, ) python-telegram-bot-13.11/tests/000077500000000000000000000000001417656324400165765ustar00rootroot00000000000000python-telegram-bot-13.11/tests/__init__.py000066400000000000000000000000001417656324400206750ustar00rootroot00000000000000python-telegram-bot-13.11/tests/bots.py000066400000000000000000000067301417656324400201250ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 json import base64 import os import random import pytest from telegram.utils.request import Request from telegram.error import RetryAfter, TimedOut # 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' 'HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2lkIjogIjY3NTY2Nj' 'IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3J' 'hbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAxIiwgImJvdF91c2VybmFtZSI6ICJAcHRi' 'X2ZhbGxiYWNrXzFfYm90In0sIHsidG9rZW4iOiAiNTU4MTk0MDY2OkFBRndEUElGbHpHVWxDYVdIdFRPRVg0UkZyWDh1O' 'URNcWZvIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WWpFd09EUXdNVEZtTkRjeSIsIC' 'JjaGF0X2lkIjogIjY3NTY2NjIyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTIyMTIxNjgzMCIsICJjaGFubmVsX2l' 'kIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgImJv' 'dF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzJfYm90In1d' ) 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')) def get(name, fallback): # If we have TOKEN, PAYMENT_PROVIDER_TOKEN, CHAT_ID, SUPER_GROUP_ID, # CHANNEL_ID, BOT_NAME, or BOT_USERNAME in the environment, then use that val = os.getenv(name.upper()) if val: return val # 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][name] except KeyError: pass # Otherwise go with the fallback return fallback def get_bot(): return {k: get(k, v) for k, v in random.choice(FALLBACKS).items()} # Patch request to xfail on flood control errors and TimedOut errors original_request_wrapper = Request._request_wrapper def patient_request_wrapper(*args, **kwargs): try: return original_request_wrapper(*args, **kwargs) except RetryAfter as e: pytest.xfail(f'Not waiting for flood control: {e}') except TimedOut as e: pytest.xfail(f'Ignoring TimedOut error: {e}') Request._request_wrapper = patient_request_wrapper python-telegram-bot-13.11/tests/conftest.py000066400000000000000000000574361417656324400210140ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 functools import inspect import os import re from collections import defaultdict from queue import Queue from threading import Thread, Event from time import sleep from typing import Callable, List, Iterable, Any import pytest import pytz from telegram import ( Message, User, Chat, MessageEntity, Update, InlineQuery, CallbackQuery, ShippingQuery, PreCheckoutQuery, ChosenInlineResult, File, ChatPermissions, ) from telegram.ext import ( Dispatcher, JobQueue, Updater, MessageFilter, Defaults, UpdateFilter, ExtBot, ) from telegram.error import BadRequest from telegram.utils.helpers import DefaultValue, DEFAULT_NONE from tests.bots import get_bot # This is here instead of in setup.cfg due to https://github.com/pytest-dev/pytest/issues/8343 def pytest_runtestloop(session): session.add_marker( pytest.mark.filterwarnings('ignore::telegram.utils.deprecate.TelegramDeprecationWarning') ) GITHUB_ACTION = os.getenv('GITHUB_ACTION', False) if GITHUB_ACTION: pytest_plugins = ['tests.plugin_github_group'] # 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 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' @pytest.fixture(scope='session') def bot_info(): return get_bot() @pytest.fixture(scope='session') def bot(bot_info): class DictExtBot( ExtBot ): # Subclass Bot to allow monkey patching of attributes and functions, would pass # come into effect when we __dict__ is dropped from slots return DictExtBot(bot_info['token'], private_key=PRIVATE_KEY) DEFAULT_BOTS = {} @pytest.fixture(scope='function') def default_bot(request, bot_info): param = request.param if hasattr(request, 'param') else {} defaults = Defaults(**param) default_bot = DEFAULT_BOTS.get(defaults) if default_bot: return default_bot default_bot = make_bot(bot_info, **{'defaults': defaults}) DEFAULT_BOTS[defaults] = default_bot return default_bot @pytest.fixture(scope='function') def tz_bot(timezone, bot_info): defaults = Defaults(tzinfo=timezone) default_bot = DEFAULT_BOTS.get(defaults) if default_bot: return default_bot default_bot = make_bot(bot_info, **{'defaults': defaults}) 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 channel_id(bot_info): return bot_info['channel_id'] @pytest.fixture(scope='session') def provider_token(bot_info): return bot_info['payment_provider_token'] def create_dp(bot): # Dispatcher is heavy to init (due to many threads and such) so we have a single session # scoped one here, but before each test, reset it (dp fixture below) dispatcher = Dispatcher(bot, Queue(), job_queue=JobQueue(), workers=2, use_context=False) dispatcher.job_queue.set_dispatcher(dispatcher) thr = Thread(target=dispatcher.start) thr.start() sleep(2) yield dispatcher sleep(1) if dispatcher.running: dispatcher.stop() thr.join() @pytest.fixture(scope='session') def _dp(bot): yield from create_dp(bot) @pytest.fixture(scope='function') def dp(_dp): # Reset the dispatcher first while not _dp.update_queue.empty(): _dp.update_queue.get(False) _dp.chat_data = defaultdict(dict) _dp.user_data = defaultdict(dict) _dp.bot_data = {} _dp.persistence = None _dp.handlers = {} _dp.groups = [] _dp.error_handlers = {} # For some reason if we setattr with the name mangled, then some tests(like async) run forever, # due to threads not acquiring, (blocking). This adds these attributes to the __dict__. object.__setattr__(_dp, '__stop_event', Event()) object.__setattr__(_dp, '__exception_event', Event()) object.__setattr__(_dp, '__async_queue', Queue()) object.__setattr__(_dp, '__async_threads', set()) _dp.persistence = None _dp.use_context = False if _dp._Dispatcher__singleton_semaphore.acquire(blocking=0): Dispatcher._set_singleton(_dp) yield _dp Dispatcher._Dispatcher__singleton_semaphore.release() @pytest.fixture(scope='function') def cdp(dp): dp.use_context = True yield dp dp.use_context = False @pytest.fixture(scope='function') def updater(bot): up = Updater(bot=bot, workers=2, use_context=False) yield up if up.running: up.stop() @pytest.fixture(scope='function') def thumb_file(): f = open('tests/data/thumb.jpg', 'rb') yield f f.close() @pytest.fixture(scope='class') def class_thumb_file(): f = open('tests/data/thumb.jpg', 'rb') yield f f.close() def pytest_configure(config): config.addinivalue_line('filterwarnings', 'ignore::ResourceWarning') # TODO: Write so good code that we don't need to ignore ResourceWarnings anymore def make_bot(bot_info, **kwargs): """ Tests are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot """ return ExtBot(bot_info['token'], private_key=PRIVATE_KEY, **kwargs) 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`` """ return 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, bot=kwargs.pop('bot', make_bot(get_bot())), **kwargs, ) 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) @pytest.fixture( scope='class', params=[{'class': MessageFilter}, {'class': UpdateFilter}], ids=['MessageFilter', 'UpdateFilter'], ) def mock_filter(request): class MockFilter(request.param['class']): def __init__(self): 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(scope='function', **get_false_update_fixture_decorator_params()) def false_update(request): return Update(update_id=1, **request.param) @pytest.fixture(params=['Europe/Berlin', 'Asia/Singapore', 'UTC']) def tzinfo(request): return pytz.timezone(request.param) @pytest.fixture() def timezone(tzinfo): return tzinfo @pytest.fixture() def mro_slots(): def _mro_slots(_class): return [ attr for cls in _class.__class__.__mro__[:-1] if hasattr(cls, '__slots__') # ABC doesn't have slots in py 3.7 and below for attr in cls.__slots__ if attr != '__dict__' ] return _mro_slots 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 callable 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 func() except BadRequest as e: if message in str(e): pytest.xfail(f'{reason}. {e}') else: raise e def check_shortcut_signature( shortcut: Callable, bot_method: Callable, shortcut_kwargs: List[str], additional_kwargs: List[str], ) -> 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``. Returns: :obj:`bool`: Whether or not the signature matches. """ 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') args_check = expected_args == effective_shortcut_args if not args_check: raise Exception(f'Expected arguments {expected_args}, got {effective_shortcut_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: if bot_sig.parameters[kwarg].annotation != shortcut_sig.parameters[kwarg].annotation: if 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}, but ' f'got {shortcut_sig.parameters[kwarg].annotation}' ) bot_method_sig = inspect.signature(bot_method) shortcut_sig = inspect.signature(shortcut) for arg in expected_args: 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.' ) return True def check_shortcut_call( shortcut_method: Callable, bot: ExtBot, bot_method_name: str, skip_params: Iterable[str] = None, shortcut_kwargs: Iterable[str] = None, ) -> bool: """ Checks that a shortcut passes all the existing arguments to the underlying bot method. Use as:: assert 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']` shortcut_kwargs: The kwargs passed by the shortcut directly, e.g. ``chat_id`` Returns: :obj:`bool` """ if not skip_params: skip_params = set() if not shortcut_kwargs: shortcut_kwargs = set() 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 kwargs = {name: name for name in shortcut_signature.parameters if name != 'auto_pagination'} 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 } 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: shortcut_method(**kwargs) except Exception as exc: raise exc finally: setattr(bot, bot_method_name, orig_bot_method) return True def check_defaults_handling( method: Callable, bot: ExtBot, return_value=None, ) -> bool: """ Checks that tg.ext.Defaults are handled correctly. Args: method: The shortcut/bot_method bot: The bot return_value: Optional. The return value of Bot._post that the method expects. Defaults to None. get_file is automatically handled. """ def build_kwargs(signature: inspect.Signature, default_kwargs, dfv: Any = DEFAULT_NONE): kws = {} for name, param in signature.parameters.items(): # For required params we need to pass something if param.default == param.empty: # Some special casing if name == 'permissions': kws[name] = ChatPermissions() elif name in ['prices', 'media', 'results', 'commands', 'errors']: kws[name] = [] 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 dfv != DEFAULT_NONE: kws[name] = dfv # 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 return kws shortcut_signature = inspect.signature(method) kwargs_need_default = [ kwarg for kwarg, value in shortcut_signature.parameters.items() if isinstance(value.default, DefaultValue) ] # shortcut_signature.parameters['timeout'] is of type DefaultValue method_timeout = shortcut_signature.parameters['timeout'].default.value default_kwarg_names = kwargs_need_default # special case explanation_parse_mode of Bot.send_poll: if 'explanation_parse_mode' in default_kwarg_names: default_kwarg_names.remove('explanation_parse_mode') defaults_no_custom_defaults = Defaults() defaults_custom_defaults = Defaults( **{kwarg: 'custom_default' for kwarg in default_kwarg_names} ) expected_return_values = [None, []] if return_value is None else [return_value] def make_assertion(_, data, timeout=DEFAULT_NONE, df_value=DEFAULT_NONE): expected_timeout = method_timeout if df_value == DEFAULT_NONE else df_value if timeout != expected_timeout: pytest.fail(f'Got value {timeout} for "timeout", expected {expected_timeout}') for arg in (dkw for dkw in kwargs_need_default if dkw != 'timeout'): # 'None' should not be passed along to Telegram if df_value in [None, DEFAULT_NONE]: if arg in data: pytest.fail( f'Got value {data[arg]} for argument {arg}, expected it to be absent' ) else: value = data.get(arg, '`not passed at all`') if value != df_value: pytest.fail(f'Got value {value} for argument {arg} instead of {df_value}') 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') nonlocal expected_return_values expected_return_values = [out] 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 return return_value orig_post = bot.request.post try: for default_value, defaults in [ (DEFAULT_NONE, defaults_no_custom_defaults), ('custom_default', defaults_custom_defaults), ]: 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, df_value=default_value) setattr(bot.request, 'post', assertion_callback) assert 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, dfv='non-None-value') assertion_callback = functools.partial(make_assertion, df_value='non-None-value') setattr(bot.request, 'post', assertion_callback) assert method(**kwargs) in expected_return_values # 3: test that we get the manually passed None value kwargs = build_kwargs( shortcut_signature, kwargs_need_default, dfv=None, ) assertion_callback = functools.partial(make_assertion, df_value=None) setattr(bot.request, 'post', assertion_callback) assert method(**kwargs) in expected_return_values except Exception as exc: raise exc finally: setattr(bot.request, 'post', orig_post) bot.defaults = None return True python-telegram-bot-13.11/tests/data/000077500000000000000000000000001417656324400175075ustar00rootroot00000000000000python-telegram-bot-13.11/tests/data/game.gif000066400000000000000000001072671417656324400211240ustar00rootroot00000000000000GIF89ahH4n: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-13.11/tests/data/game.png000066400000000000000000001200411417656324400211240ustar00rootroot00000000000000PNG  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-13.11/tests/data/local_file.txt000066400000000000000000000000141417656324400223340ustar00rootroot00000000000000Saint-Saënspython-telegram-bot-13.11/tests/data/sticker_set_thumb.png000066400000000000000000000033751417656324400237430ustar00rootroot00000000000000PNG  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-13.11/tests/data/telegram000066400000000000000000000000001417656324400212200ustar00rootroot00000000000000python-telegram-bot-13.11/tests/data/telegram.gif000066400000000000000000000074461417656324400220110ustar00rootroot00000000000000GIF87aX___???߿,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-13.11/tests/data/telegram.jpg000066400000000000000000001004361417656324400220150ustar00rootroot00000000000000JFIFHHC       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-13.11/tests/data/telegram.mp3000066400000000000000000003600501417656324400217340ustar00rootroot00000000000000ID3TXXXSoftwareLavf56.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-13.11/tests/data/telegram.png000066400000000000000000000312241417656324400220170ustar00rootroot00000000000000PNG  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-13.11/tests/data/telegram2.mp4000066400000000000000000004017641417656324400220270ustar00rootroot00000000000000 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-13.11/tests/data/telegram_sticker.png000066400000000000000000001220211417656324400235370ustar00rootroot00000000000000PNG  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-13.11/tests/data/telegram_video_sticker.webm000066400000000000000000003726021417656324400251070ustar00rootroot00000000000000Eߣ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-13.11/tests/data/text_file.txt000066400000000000000000000000121417656324400222240ustar00rootroot00000000000000PTB Rocks!python-telegram-bot-13.11/tests/data/thumb.jpg000066400000000000000000000052701417656324400213340ustar00rootroot00000000000000JFIFC       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-13.11/tests/plugin_github_group.py000066400000000000000000000045301417656324400232260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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.config import pytest fold_plugins = {'_cov': 'Coverage report', 'flaky': 'Flaky report'} def terminal_summary_wrapper(original, plugin_name): text = fold_plugins[plugin_name] def pytest_terminal_summary(terminalreporter): terminalreporter.write(f'##[group] {text}\n') original(terminalreporter) terminalreporter.write('##[endgroup]') return pytest_terminal_summary @pytest.mark.trylast def pytest_configure(config): for hookimpl in config.pluginmanager.hook.pytest_terminal_summary._nonwrappers: if hookimpl.plugin_name in fold_plugins: hookimpl.function = terminal_summary_wrapper(hookimpl.function, hookimpl.plugin_name) terminal = None previous_name = None def _get_name(location): if location[0].startswith('tests/'): return location[0][6:] return location[0] @pytest.mark.trylast def pytest_itemcollected(item): item._nodeid = item._nodeid.split('::', 1)[1] @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_runtest_protocol(item, nextitem): # This is naughty but pytests' own plugins does something similar too, so who cares global terminal if terminal is None: terminal = _pytest.config.create_terminal_writer(item.config) global previous_name name = _get_name(item.location) if previous_name is None or previous_name != name: previous_name = name terminal.write(f'\n##[group] {name}') yield if nextitem is None or _get_name(nextitem.location) != name: terminal.write('\n##[endgroup]') python-telegram-bot-13.11/tests/test_animation.py000066400000000000000000000327441417656324400222000ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 import pytest from flaky import flaky from telegram import PhotoSize, Animation, Voice, TelegramError, MessageEntity, Bot from telegram.error import BadRequest from telegram.utils.helpers import escape_markdown from tests.conftest import check_shortcut_call, check_shortcut_signature, check_defaults_handling @pytest.fixture(scope='function') def animation_file(): f = open('tests/data/game.gif', 'rb') yield f f.close() @pytest.fixture(scope='class') def animation(bot, chat_id): with open('tests/data/game.gif', 'rb') as f: return bot.send_animation( chat_id, animation=f, timeout=50, thumb=open('tests/data/thumb.jpg', 'rb') ).animation class TestAnimation: 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.mp4' mime_type = 'video/mp4' file_size = 4127 caption = "Test *animation*" def test_slot_behaviour(self, animation, recwarn, mro_slots): for attr in animation.__slots__: assert getattr(animation, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not animation.__dict__, f"got missing slot(s): {animation.__dict__}" assert len(mro_slots(animation)) == len(set(mro_slots(animation))), "duplicate slot" animation.custom, animation.file_name = 'should give warning', self.file_name assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.file_size == self.file_size assert animation.mime_type == self.mime_type assert animation.file_name == self.file_name assert isinstance(animation.thumb, PhotoSize) @flaky(3, 1) def test_send_all_args(self, bot, chat_id, animation_file, animation, thumb_file): message = 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, thumb=thumb_file, ) 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.thumb.width == self.width assert message.animation.thumb.height == self.height assert message.has_protected_content @flaky(3, 1) def test_send_animation_custom_filename(self, bot, chat_id, animation_file, monkeypatch): def make_assertion(url, data, **kwargs): return data['animation'].filename == 'custom_filename' monkeypatch.setattr(bot.request, 'post', make_assertion) assert bot.send_animation(chat_id, animation_file, filename='custom_filename') monkeypatch.delattr(bot.request, 'post') @flaky(3, 1) def test_get_and_download(self, bot, animation): new_file = bot.get_file(animation.file_id) assert new_file.file_size == self.file_size assert new_file.file_id == animation.file_id assert new_file.file_path.startswith('https://') new_file.download('game.gif') assert os.path.isfile('game.gif') @flaky(3, 1) def test_send_animation_url_file(self, bot, chat_id, animation): message = 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 == animation.file_name assert message.animation.mime_type == animation.mime_type assert message.animation.file_size == animation.file_size @flaky(3, 1) 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 = bot.send_animation( chat_id, animation, caption=test_string, caption_entities=entities ) assert message.caption == test_string assert message.caption_entities == entities @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) 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 = default_bot.send_animation(chat_id, animation_file, caption=test_markdown_string) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_animation_default_parse_mode_2(self, default_bot, chat_id, animation_file): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_animation_default_parse_mode_3(self, default_bot, chat_id, animation_file): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) def test_send_animation_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('animation') == expected and data.get('thumb') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.send_animation(chat_id, file, thumb=file) assert test_flag monkeypatch.delattr(bot, '_post') @flaky(3, 1) @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'], ) def test_send_animation_default_allow_sending_without_reply( self, default_bot, chat_id, animation, custom ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_animation( chat_id, animation, reply_to_message_id=reply_to_message.message_id ) @flaky(3, 1) def test_resend(self, bot, chat_id, animation): message = bot.send_animation(chat_id, animation.file_id) assert message.animation == animation def test_send_with_animation(self, monkeypatch, bot, chat_id, animation): def test(url, data, **kwargs): return data['animation'] == animation.file_id monkeypatch.setattr(bot.request, 'post', test) message = bot.send_animation(animation=animation, chat_id=chat_id) assert message 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, 'thumb': animation.thumb.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.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['thumb'] == animation.thumb.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 @flaky(3, 1) def test_error_send_empty_file(self, bot, chat_id): animation_file = open(os.devnull, 'rb') with pytest.raises(TelegramError): bot.send_animation(chat_id=chat_id, animation=animation_file) @flaky(3, 1) def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): bot.send_animation(chat_id=chat_id, animation='') def test_error_send_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): bot.send_animation(chat_id=chat_id) def test_get_file_instance_method(self, monkeypatch, animation): def make_assertion(*_, **kwargs): return kwargs['file_id'] == animation.file_id assert check_shortcut_signature(Animation.get_file, Bot.get_file, ['file_id'], []) assert check_shortcut_call(animation.get_file, animation.bot, 'get_file') assert check_defaults_handling(animation.get_file, animation.bot) monkeypatch.setattr(animation.bot, 'get_file', make_assertion) assert animation.get_file() 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) python-telegram-bot-13.11/tests/test_audio.py000066400000000000000000000302601417656324400213110ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 import pytest from flaky import flaky from telegram import Audio, TelegramError, Voice, MessageEntity, Bot from telegram.utils.helpers import escape_markdown from tests.conftest import check_shortcut_call, check_shortcut_signature, check_defaults_handling @pytest.fixture(scope='function') def audio_file(): f = open('tests/data/telegram.mp3', 'rb') yield f f.close() @pytest.fixture(scope='class') def audio(bot, chat_id): with open('tests/data/telegram.mp3', 'rb') as f: return bot.send_audio( chat_id, audio=f, timeout=50, thumb=open('tests/data/thumb.jpg', 'rb') ).audio class TestAudio: 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' def test_slot_behaviour(self, audio, recwarn, mro_slots): for attr in audio.__slots__: assert getattr(audio, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not audio.__dict__, f"got missing slot(s): {audio.__dict__}" assert len(mro_slots(audio)) == len(set(mro_slots(audio))), "duplicate slot" audio.custom, audio.file_name = 'should give warning', self.file_name assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb.file_size == self.thumb_file_size assert audio.thumb.width == self.thumb_width assert audio.thumb.height == self.thumb_height @flaky(3, 1) def test_send_all_args(self, bot, chat_id, audio_file, thumb_file): message = 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', thumb=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.thumb.file_size == self.thumb_file_size assert message.audio.thumb.width == self.thumb_width assert message.audio.thumb.height == self.thumb_height assert message.has_protected_content @flaky(3, 1) def test_send_audio_custom_filename(self, bot, chat_id, audio_file, monkeypatch): def make_assertion(url, data, **kwargs): return data['audio'].filename == 'custom_filename' monkeypatch.setattr(bot.request, 'post', make_assertion) assert bot.send_audio(chat_id, audio_file, filename='custom_filename') @flaky(3, 1) def test_get_and_download(self, bot, audio): new_file = bot.get_file(audio.file_id) assert new_file.file_size == self.file_size assert new_file.file_id == audio.file_id assert new_file.file_unique_id == audio.file_unique_id assert new_file.file_path.startswith('https://') new_file.download('telegram.mp3') assert os.path.isfile('telegram.mp3') @flaky(3, 1) def test_send_mp3_url_file(self, bot, chat_id, audio): message = 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 @flaky(3, 1) def test_resend(self, bot, chat_id, audio): message = bot.send_audio(chat_id=chat_id, audio=audio.file_id) assert message.audio == audio def test_send_with_audio(self, monkeypatch, bot, chat_id, audio): def test(url, data, **kwargs): return data['audio'] == audio.file_id monkeypatch.setattr(bot.request, 'post', test) message = bot.send_audio(audio=audio, chat_id=chat_id) assert message @flaky(3, 1) 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 = bot.send_audio(chat_id, audio, caption=test_string, caption_entities=entities) assert message.caption == test_string assert message.caption_entities == entities @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_audio_default_parse_mode_1(self, default_bot, chat_id, audio_file, thumb_file): test_string = 'Italic Bold Code' test_markdown_string = '_Italic_ *Bold* `Code`' message = default_bot.send_audio(chat_id, audio_file, caption=test_markdown_string) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_audio_default_parse_mode_2(self, default_bot, chat_id, audio_file, thumb_file): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_audio_default_parse_mode_3(self, default_bot, chat_id, audio_file, thumb_file): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) def test_send_audio_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('audio') == expected and data.get('thumb') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.send_audio(chat_id, file, thumb=file) assert test_flag monkeypatch.delattr(bot, '_post') 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, 'caption': self.caption, 'mime_type': self.mime_type, 'file_size': self.file_size, 'thumb': audio.thumb.to_dict(), } json_audio = Audio.de_json(json_dict, bot) 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.thumb == audio.thumb 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 @flaky(3, 1) def test_error_send_empty_file(self, bot, chat_id): audio_file = open(os.devnull, 'rb') with pytest.raises(TelegramError): bot.send_audio(chat_id=chat_id, audio=audio_file) @flaky(3, 1) def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): bot.send_audio(chat_id=chat_id, audio='') def test_error_send_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): bot.send_audio(chat_id=chat_id) def test_get_file_instance_method(self, monkeypatch, audio): def make_assertion(*_, **kwargs): return kwargs['file_id'] == audio.file_id assert check_shortcut_signature(Audio.get_file, Bot.get_file, ['file_id'], []) assert check_shortcut_call(audio.get_file, audio.bot, 'get_file') assert check_defaults_handling(audio.get_file, audio.bot) monkeypatch.setattr(audio.bot, 'get_file', make_assertion) assert audio.get_file() 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) python-telegram-bot-13.11/tests/test_bot.py000066400000000000000000003137071417656324400210060ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 time import datetime as dtm from collections import defaultdict from pathlib import Path from platform import python_implementation import pytest import pytz from flaky import flaky from telegram import ( Bot, Update, ChatAction, TelegramError, User, InlineKeyboardMarkup, InlineKeyboardButton, InlineQueryResultArticle, InputTextMessageContent, ShippingOption, LabeledPrice, ChatPermissions, Poll, BotCommand, InlineQueryResultDocument, Dice, MessageEntity, ParseMode, CallbackQuery, Message, Chat, InlineQueryResultVoice, PollOption, BotCommandScopeChat, ) from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.ext import ExtBot, Defaults from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter from telegram.ext.callbackdatacache import InvalidCallbackData from telegram.utils.helpers import ( from_timestamp, escape_markdown, to_timestamp, ) from tests.conftest import expect_bad_request, check_defaults_handling, GITHUB_ACTION from tests.bots import FALLBACKS 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:]) class ExtBotSubClass(ExtBot): # used for test_defaults_warning below pass class BotSubClass(Bot): # used for test_defaults_warning below pass @pytest.fixture(scope='class') def message(bot, chat_id): to_reply_to = bot.send_message( chat_id, 'Text', disable_web_page_preview=True, disable_notification=True ) return bot.send_message( chat_id, 'Text', reply_to_message_id=to_reply_to.message_id, disable_web_page_preview=True, disable_notification=True, ) @pytest.fixture(scope='class') def media_message(bot, chat_id): with open('tests/data/telegram.ogg', 'rb') as f: return bot.send_voice(chat_id, voice=f, caption='my caption', timeout=10) @pytest.fixture(scope='class') 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='class') 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', ) @pytest.fixture(scope='function') def inst(request, bot_info, default_bot): return Bot(bot_info['token']) if request.param == 'bot' else default_bot class TestBot: """ Most are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot """ @pytest.mark.parametrize('inst', ['bot', "default_bot"], indirect=True) def test_slot_behaviour(self, inst, recwarn, mro_slots): for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slots: {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.base_url = 'should give warning', inst.base_url assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list class CustomBot(Bot): pass # Tests that setting custom attributes of Bot subclass doesn't raise warning a = CustomBot(inst.token) a.my_custom = 'no error!' assert len(recwarn) == 1 @pytest.mark.parametrize( 'token', argvalues=[ '123', '12a:abcd1234', '12:abcd1234', '1234:abcd1234\n', ' 1234:abcd1234', ' 1234:abcd1234\r', '1234:abcd 1234', ], ) def test_invalid_token(self, token): with pytest.raises(InvalidToken, match='Invalid token'): Bot(token) @pytest.mark.parametrize( 'acd_in,maxsize,acd', [(True, 1024, True), (False, 1024, False), (0, 0, True), (None, None, True)], ) def test_callback_data_maxsize(self, bot, acd_in, maxsize, acd): bot = ExtBot(bot.token, arbitrary_callback_data=acd_in) assert bot.arbitrary_callback_data == acd assert bot.callback_data_cache.maxsize == maxsize @flaky(3, 1) def test_invalid_token_server_response(self, monkeypatch): monkeypatch.setattr('telegram.Bot._validate_token', lambda x, y: True) bot = Bot('12') with pytest.raises(InvalidToken): bot.get_me() def test_unknown_kwargs(self, bot, monkeypatch): def post(url, data, timeout): assert data['unknown_kwarg_1'] == 7 assert data['unknown_kwarg_2'] == 5 monkeypatch.setattr(bot.request, 'post', post) bot.send_message(123, 'text', api_kwargs={'unknown_kwarg_1': 7, 'unknown_kwarg_2': 5}) @flaky(3, 1) def test_get_me_and_properties(self, bot): get_me_bot = bot.get_me() commands = bot.get_my_commands() 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 assert commands == bot.commands bot._commands = None assert commands == bot.commands def test_equality(self): a = Bot(FALLBACKS[0]["token"]) b = Bot(FALLBACKS[0]["token"]) c = Bot(FALLBACKS[1]["token"]) d = Update(123456789) 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) @flaky(3, 1) 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 @pytest.mark.parametrize( 'bot_method_name', argvalues=[ name for name, _ in inspect.getmembers(Bot, predicate=inspect.isfunction) if not name.startswith('_') and name not in [ 'de_json', 'de_list', 'to_dict', 'to_json', 'parse_data', 'get_updates', 'getUpdates', ] ], ) def test_defaults_handling(self, bot_method_name, bot): """ Here we check that the bot methods handle tg.ext.Defaults correctly. 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. """ bot_method = getattr(bot, bot_method_name) assert check_defaults_handling(bot_method, bot) def test_ext_bot_signature(self): """ 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 = set() extra_args_per_method = defaultdict(set, {'__init__': {'arbitrary_callback_data'}}) different_hints_per_method = defaultdict(set, {'__setattr__': {'ext_bot'}}) for name, method in inspect.getmembers(Bot, predicate=inspect.isfunction): 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}' @flaky(3, 1) def test_forward_message(self, bot, chat_id, message): forward_message = 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_from.username == message.from_user.username assert isinstance(forward_message.forward_date, dtm.datetime) def test_forward_protected_message(self, bot, message, chat_id): to_forward_protected = bot.send_message(chat_id, 'cant forward me', protect_content=True) assert to_forward_protected.has_protected_content with pytest.raises(BadRequest, match="can't be forwarded"): to_forward_protected.forward(chat_id) to_forward_unprotected = bot.send_message(chat_id, 'forward me', protect_content=False) assert not to_forward_unprotected.has_protected_content forwarded_but_now_protected = to_forward_unprotected.forward(chat_id, protect_content=True) assert forwarded_but_now_protected.has_protected_content with pytest.raises(BadRequest, match="can't be forwarded"): forwarded_but_now_protected.forward(chat_id) @flaky(3, 1) def test_delete_message(self, bot, chat_id): message = bot.send_message(chat_id, text='will be deleted') time.sleep(2) assert bot.delete_message(chat_id=chat_id, message_id=message.message_id) is True @flaky(3, 1) def test_delete_message_old_message(self, bot, chat_id): with pytest.raises(BadRequest): # Considering that the first message is old enough 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 and send_animation are tested in their respective test modules. No need to # duplicate here. @flaky(3, 1) 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' message = bot.send_venue( chat_id=chat_id, title=title, address=address, latitude=latitude, longitude=longitude, foursquare_id=foursquare_id, foursquare_type=foursquare_type, protect_content=True, ) 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 message = bot.send_venue( chat_id=chat_id, title=title, address=address, latitude=latitude, longitude=longitude, google_place_id=google_place_id, google_place_type=google_place_type, protect_content=True, ) 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.google_place_id == google_place_id assert message.venue.google_place_type == google_place_type assert message.venue.foursquare_id is None assert message.venue.foursquare_type is None assert message.has_protected_content @flaky(3, 1) @pytest.mark.xfail(raises=RetryAfter) @pytest.mark.skipif( python_implementation() == 'PyPy', reason='Unstable on pypy for some reason' ) def test_send_contact(self, bot, chat_id): phone_number = '+11234567890' first_name = 'Leandro' last_name = 'Toledo' message = 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 # TODO: Add bot to group to test polls too @flaky(3, 1) @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(), ], ) def test_send_and_stop_poll(self, bot, super_group_id, reply_markup): question = 'Is this a test?' answers = ['Yes', 'No', 'Maybe'] message = bot.send_poll( chat_id=super_group_id, question=question, options=answers, is_anonymous=False, allows_multiple_answers=True, timeout=60, protect_content=True, ) 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 = bot.stop_poll( chat_id=super_group_id, message_id=message.message_id, reply_markup=reply_markup, 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 explanation = '[Here is a link](https://google.com)' explanation_entities = [ MessageEntity(MessageEntity.TEXT_LINK, 0, 14, url='https://google.com') ] message_quiz = 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, ) 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 == explanation_entities @flaky(3, 1) @pytest.mark.parametrize(['open_period', 'close_date'], [(5, None), (None, True)]) 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) message = bot.send_poll( chat_id=super_group_id, question=question, options=answers, is_anonymous=False, allows_multiple_answers=True, timeout=60, open_period=open_period, close_date=close_date, ) time.sleep(5.1) new_message = bot.edit_message_reply_markup( chat_id=super_group_id, message_id=message.message_id, reply_markup=reply_markup, timeout=60, ) assert new_message.poll.id == message.poll.id assert new_message.poll.is_closed @flaky(5, 1) 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) message = tz_bot.send_poll( chat_id=super_group_id, question=question, options=answers, close_date=close_date, timeout=60, ) assert message.poll.close_date == aware_close_date.replace(microsecond=0) time.sleep(5.1) new_message = tz_bot.edit_message_reply_markup( chat_id=super_group_id, message_id=message.message_id, reply_markup=reply_markup, timeout=60, ) assert new_message.poll.id == message.poll.id assert new_message.poll.is_closed @flaky(3, 1) 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 = 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 == entities @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) 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'] message = 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, ) assert message.poll.explanation == explanation assert message.poll.explanation_entities == [ MessageEntity(MessageEntity.ITALIC, 0, 6), MessageEntity(MessageEntity.BOLD, 7, 4), MessageEntity(MessageEntity.CODE, 12, 4), ] message = 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, explanation_parse_mode=None, ) assert message.poll.explanation == explanation_markdown assert message.poll.explanation_entities == [] message = 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, explanation_parse_mode='HTML', ) assert message.poll.explanation == explanation_markdown assert message.poll.explanation_entities == [] @flaky(3, 1) @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'], ) 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 = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_poll( chat_id, question=question, options=answers, reply_to_message_id=reply_to_message.message_id, ) @flaky(3, 1) @pytest.mark.parametrize('emoji', Dice.ALL_EMOJI + [None]) def test_send_dice(self, bot, chat_id, emoji): message = 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 @flaky(3, 1) @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'], ) def test_send_dice_default_allow_sending_without_reply(self, default_bot, chat_id, custom): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_dice(chat_id, reply_to_message_id=reply_to_message.message_id) @flaky(3, 1) @pytest.mark.parametrize( 'chat_action', [ ChatAction.FIND_LOCATION, ChatAction.RECORD_AUDIO, ChatAction.RECORD_VIDEO, ChatAction.RECORD_VIDEO_NOTE, ChatAction.RECORD_VOICE, ChatAction.TYPING, ChatAction.UPLOAD_AUDIO, ChatAction.UPLOAD_DOCUMENT, ChatAction.UPLOAD_PHOTO, ChatAction.UPLOAD_VIDEO, ChatAction.UPLOAD_VIDEO_NOTE, ChatAction.UPLOAD_VOICE, ChatAction.CHOOSE_STICKER, ], ) def test_send_chat_action(self, bot, chat_id, chat_action): assert bot.send_chat_action(chat_id, chat_action) with pytest.raises(BadRequest, match='Wrong parameter action'): bot.send_chat_action(chat_id, 'unknown action') # TODO: Needs improvement. We need incoming inline query to test answer. def test_answer_inline_query(self, monkeypatch, bot): # For now just test that our internals pass the correct data def test(url, data, *args, **kwargs): return data == { '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'}, }, ], 'next_offset': '42', 'switch_pm_parameter': 'start_pm', 'inline_query_id': 1234, 'is_personal': True, 'switch_pm_text': 'switch pm', } monkeypatch.setattr(bot.request, 'post', test) results = [ InlineQueryResultArticle('11', 'first', InputTextMessageContent('first')), InlineQueryResultArticle('12', 'second', InputTextMessageContent('second')), ] assert bot.answer_inline_query( 1234, results=results, cache_time=300, is_personal=True, next_offset='42', switch_pm_text='switch pm', switch_pm_parameter='start_pm', ) monkeypatch.delattr(bot.request, 'post') def test_answer_inline_query_no_default_parse_mode(self, monkeypatch, bot): def test(url, data, *args, **kwargs): return data == { 'cache_time': 300, 'results': [ { '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', } ], 'next_offset': '42', 'switch_pm_parameter': 'start_pm', 'inline_query_id': 1234, 'is_personal': True, 'switch_pm_text': 'switch pm', } monkeypatch.setattr(bot.request, 'post', test) results = [ 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', ) ] assert bot.answer_inline_query( 1234, results=results, cache_time=300, is_personal=True, next_offset='42', switch_pm_text='switch pm', switch_pm_parameter='start_pm', ) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_answer_inline_query_default_parse_mode(self, monkeypatch, default_bot): def test(url, data, *args, **kwargs): return data == { 'cache_time': 300, 'results': [ { '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', 'parse_mode': 'Markdown', } ], 'next_offset': '42', 'switch_pm_parameter': 'start_pm', 'inline_query_id': 1234, 'is_personal': True, 'switch_pm_text': 'switch pm', } monkeypatch.setattr(default_bot.request, 'post', test) results = [ 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', ) ] assert default_bot.answer_inline_query( 1234, results=results, cache_time=300, is_personal=True, next_offset='42', switch_pm_text='switch pm', switch_pm_parameter='start_pm', ) def test_answer_inline_query_current_offset_error(self, bot, inline_results): with pytest.raises(ValueError, match=('`current_offset` and `next_offset`')): bot.answer_inline_query( 1234, results=inline_results, next_offset=42, current_offset=51 ) @pytest.mark.parametrize( 'current_offset,num_results,id_offset,expected_next_offset', [ ('', MAX_INLINE_QUERY_RESULTS, 1, 1), (1, MAX_INLINE_QUERY_RESULTS, 51, 2), (5, 3, 251, ''), ], ) 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 def make_assertion(url, data, *args, **kwargs): 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 bot.answer_inline_query(1234, results=inline_results, current_offset=current_offset) def test_answer_inline_query_current_offset_2(self, monkeypatch, bot, inline_results): # For now just test that our internals pass the correct data def make_assertion(url, data, *args, **kwargs): results = data['results'] length_matches = len(results) == MAX_INLINE_QUERY_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 bot.answer_inline_query(1234, results=inline_results, current_offset=0) inline_results = inline_results[:30] def make_assertion(url, data, *args, **kwargs): 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 = data['next_offset'] == '' return length_matches and ids_match and next_offset_matches monkeypatch.setattr(bot.request, 'post', make_assertion) assert bot.answer_inline_query(1234, results=inline_results, current_offset=0) def test_answer_inline_query_current_offset_callback(self, monkeypatch, bot, caplog): # For now just test that our internals pass the correct data def make_assertion(url, data, *args, **kwargs): 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 bot.answer_inline_query(1234, results=inline_results_callback, current_offset=1) def make_assertion(url, data, *args, **kwargs): results = data['results'] length = results == [] next_offset = data['next_offset'] == '' return length and next_offset monkeypatch.setattr(bot.request, 'post', make_assertion) assert bot.answer_inline_query(1234, results=inline_results_callback, current_offset=6) @flaky(3, 1) def test_get_user_profile_photos(self, bot, chat_id): user_profile_photos = bot.get_user_profile_photos(chat_id) assert user_profile_photos.photos[0][0].file_size == 5403 @flaky(3, 1) def test_get_one_user_profile_photo(self, bot, chat_id): user_profile_photos = bot.get_user_profile_photos(chat_id, offset=0, limit=1) assert user_profile_photos.photos[0][0].file_size == 5403 # get_file is tested multiple times in the test_*media* modules. # Here we only test the behaviour for bot apis in local mode def test_get_file_local_mode(self, bot, monkeypatch): path = str(Path.cwd() / 'tests' / 'data' / 'game.gif') def _post(*args, **kwargs): return { 'file_id': None, 'file_unique_id': None, 'file_size': None, 'file_path': path, } monkeypatch.setattr(bot, '_post', _post) resulting_path = bot.get_file('file_id').file_path assert bot.token not in resulting_path assert resulting_path == path monkeypatch.delattr(bot, '_post') # TODO: Needs improvement. No feasible way to test until bots can add members. def test_ban_chat_member(self, monkeypatch, bot): def test(url, data, *args, **kwargs): 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) is True return chat_id and user_id and until_date and revoke_msgs monkeypatch.setattr(bot.request, 'post', test) until = from_timestamp(1577887200) assert bot.ban_chat_member(2, 32) assert bot.ban_chat_member(2, 32, until_date=until) assert bot.ban_chat_member(2, 32, until_date=1577887200) assert bot.ban_chat_member(2, 32, revoke_messages=True) monkeypatch.delattr(bot.request, 'post') 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) def test(url, data, *args, **kwargs): 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', test) assert tz_bot.ban_chat_member(2, 32) assert tz_bot.ban_chat_member(2, 32, until_date=until) assert tz_bot.ban_chat_member(2, 32, until_date=until_timestamp) def test_ban_chat_sender_chat(self, monkeypatch, bot): # For now, we just test that we pass the correct data to TG def make_assertion(url, data, *args, **kwargs): 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 bot.ban_chat_sender_chat(2, 32) monkeypatch.delattr(bot.request, 'post') def test_kick_chat_member_warning(self, monkeypatch, bot, recwarn): def test(url, data, *args, **kwargs): chat_id = data['chat_id'] == 2 user_id = data['user_id'] == 32 return chat_id and user_id monkeypatch.setattr(bot.request, 'post', test) bot.kick_chat_member(2, 32) assert len(recwarn) == 1 assert '`bot.kick_chat_member` is deprecated' in str(recwarn[0].message) monkeypatch.delattr(bot.request, 'post') # TODO: Needs improvement. @pytest.mark.parametrize('only_if_banned', [True, False, None]) def test_unban_chat_member(self, monkeypatch, bot, only_if_banned): def make_assertion(url, data, *args, **kwargs): 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 bot.unban_chat_member(2, 32, only_if_banned=only_if_banned) def test_unban_chat_sender_chat(self, monkeypatch, bot): def make_assertion(url, data, *args, **kwargs): 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 bot.unbanChatSenderChat(2, 32) def test_set_chat_permissions(self, monkeypatch, bot, chat_permissions): def test(url, data, *args, **kwargs): chat_id = data['chat_id'] == 2 permissions = data['permissions'] == chat_permissions.to_dict() return chat_id and permissions monkeypatch.setattr(bot.request, 'post', test) assert bot.set_chat_permissions(2, chat_permissions) def test_set_chat_administrator_custom_title(self, monkeypatch, bot): def test(url, data, *args, **kwargs): 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', test) assert bot.set_chat_administrator_custom_title(2, 32, 'custom_title') # TODO: Needs improvement. Need an incoming callbackquery to test def test_answer_callback_query(self, monkeypatch, bot): # For now just test that our internals pass the correct data def test(url, data, *args, **kwargs): return data == { 'callback_query_id': 23, 'show_alert': True, 'url': 'no_url', 'cache_time': 1, 'text': 'answer', } monkeypatch.setattr(bot.request, 'post', test) assert bot.answer_callback_query( 23, text='answer', show_alert=True, url='no_url', cache_time=1 ) @flaky(3, 1) def test_edit_message_text(self, bot, message): message = 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' @flaky(3, 1) 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 = 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 == entities @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_edit_message_text_default_parse_mode(self, default_bot, message): test_string = 'Italic Bold Code' test_markdown_string = '_Italic_ *Bold* `Code`' message = 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 = 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 = 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 = 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') def test_edit_message_text_inline(self): pass @flaky(3, 1) def test_edit_message_caption(self, bot, media_message): message = bot.edit_message_caption( caption='new_caption', chat_id=media_message.chat_id, message_id=media_message.message_id, ) assert message.caption == 'new_caption' @flaky(3, 1) 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 = 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 == entities # edit_message_media is tested in test_inputmedia @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) 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 = 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 = 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 = default_bot.edit_message_caption( caption=test_markdown_string, chat_id=media_message.chat_id, message_id=media_message.message_id, ) message = 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) @flaky(3, 1) def test_edit_message_caption_with_parse_mode(self, bot, media_message): message = 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' def test_edit_message_caption_without_required(self, bot): with pytest.raises(ValueError, match='Both chat_id and message_id are required when'): bot.edit_message_caption(caption='new_caption') @pytest.mark.skip(reason='need reference to an inline message') def test_edit_message_caption_inline(self): pass @flaky(3, 1) def test_edit_reply_markup(self, bot, message): new_markup = InlineKeyboardMarkup([[InlineKeyboardButton(text='test', callback_data='1')]]) message = bot.edit_message_reply_markup( chat_id=message.chat_id, message_id=message.message_id, reply_markup=new_markup ) assert message is not True def test_edit_message_reply_markup_without_required(self, bot): new_markup = InlineKeyboardMarkup([[InlineKeyboardButton(text='test', callback_data='1')]]) with pytest.raises(ValueError, match='Both chat_id and message_id are required when'): bot.edit_message_reply_markup(reply_markup=new_markup) @pytest.mark.skip(reason='need reference to an inline message') def test_edit_reply_markup_inline(self): pass # TODO: Actually send updates to the test bot so this can be tested properly @flaky(3, 1) def test_get_updates(self, bot): bot.delete_webhook() # make sure there is no webhook set if webhook tests failed updates = bot.get_updates(timeout=1) assert isinstance(updates, list) if updates: assert isinstance(updates[0], Update) def test_get_updates_invalid_callback_data(self, bot, monkeypatch): 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=None, chat=Chat(1, ''), text='Webhook', ), ), ).to_dict() ] bot.arbitrary_callback_data = True try: monkeypatch.setattr(bot.request, 'post', post) bot.delete_webhook() # make sure there is no webhook set if webhook tests failed updates = bot.get_updates(timeout=1) assert isinstance(updates, list) assert len(updates) == 1 assert isinstance(updates[0].callback_query.data, InvalidCallbackData) finally: # Reset b/c bots scope is session bot.arbitrary_callback_data = False @flaky(3, 1) @pytest.mark.xfail def test_set_webhook_get_webhook_info_and_delete_webhook(self, bot): url = 'https://python-telegram-bot.org/test/webhook' max_connections = 7 allowed_updates = ['message'] bot.set_webhook( url, max_connections=max_connections, allowed_updates=allowed_updates, ip_address='127.0.0.1', ) time.sleep(2) live_info = bot.get_webhook_info() time.sleep(6) bot.delete_webhook() time.sleep(2) info = bot.get_webhook_info() assert info.url == '' assert live_info.url == url assert live_info.max_connections == max_connections assert live_info.allowed_updates == allowed_updates assert live_info.ip_address == '127.0.0.1' @pytest.mark.parametrize('drop_pending_updates', [True, False]) def test_set_webhook_delete_webhook_drop_pending_updates( self, bot, drop_pending_updates, monkeypatch ): def assertion(url, data, *args, **kwargs): return bool(data.get('drop_pending_updates')) == drop_pending_updates monkeypatch.setattr(bot.request, 'post', assertion) assert bot.set_webhook(drop_pending_updates=drop_pending_updates) assert bot.delete_webhook(drop_pending_updates=drop_pending_updates) @flaky(3, 1) def test_leave_chat(self, bot): with pytest.raises(BadRequest, match='Chat not found'): bot.leave_chat(-123456) with pytest.raises(NetworkError, match='Chat not found'): bot.leave_chat(-123456) @flaky(3, 1) def test_get_chat(self, bot, super_group_id): chat = 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) @flaky(3, 1) def test_get_chat_administrators(self, bot, channel_id): admins = bot.get_chat_administrators(channel_id) assert isinstance(admins, list) for a in admins: assert a.status in ('administrator', 'creator') @flaky(3, 1) def test_get_chat_member_count(self, bot, channel_id): count = bot.get_chat_member_count(channel_id) assert isinstance(count, int) assert count > 3 def test_get_chat_members_count_warning(self, bot, channel_id, recwarn): bot.get_chat_members_count(channel_id) assert len(recwarn) == 1 assert '`bot.get_chat_members_count` is deprecated' in str(recwarn[0].message) def test_bot_command_property_warning(self, bot, recwarn): _ = bot.commands assert len(recwarn) == 1 assert 'Bot.commands has been deprecated since there can' in str(recwarn[0].message) @flaky(3, 1) def test_get_chat_member(self, bot, channel_id, chat_id): chat_member = 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") def test_set_chat_sticker_set(self): pass @pytest.mark.skip(reason="Not implemented since we need a supergroup with many members") def test_delete_chat_sticker_set(self): pass @flaky(3, 1) def test_send_game(self, bot, chat_id): game_short_name = 'test_game' message = 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 @flaky(3, 1) @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'], ) def test_send_game_default_allow_sending_without_reply(self, default_bot, chat_id, custom): game_short_name = 'test_game' reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_game( chat_id, game_short_name, reply_to_message_id=reply_to_message.message_id ) @xfail 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 = bot.send_game(chat_id, game_short_name) message = 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 @xfail 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 = bot.send_game(chat_id, game_short_name) score = BASE_GAME_SCORE + 1 message = 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 @xfail 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 = 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'): bot.set_game_score( user_id=chat_id, score=score, chat_id=game.chat_id, message_id=game.message_id ) @xfail 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 = bot.send_game(chat_id, game_short_name) time.sleep(2) score = BASE_GAME_SCORE - 10 message = 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 = bot.send_game(chat_id, game_short_name) assert str(score) in game2.game.text @xfail 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 = bot.send_game(chat_id, game_short_name) high_scores = 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 is tested in test_invoice # TODO: Needs improvement. Need incoming shipping queries to test def test_answer_shipping_query_ok(self, monkeypatch, bot): # For now just test that our internals pass the correct data def test(url, data, *args, **kwargs): return data == { 'shipping_query_id': 1, 'ok': True, 'shipping_options': [ {'title': 'option1', 'prices': [{'label': 'price', 'amount': 100}], 'id': 1} ], } monkeypatch.setattr(bot.request, 'post', test) shipping_options = ShippingOption(1, 'option1', [LabeledPrice('price', 100)]) assert bot.answer_shipping_query(1, True, shipping_options=[shipping_options]) def test_answer_shipping_query_error_message(self, monkeypatch, bot): # For now just test that our internals pass the correct data def test(url, data, *args, **kwargs): return data == { 'shipping_query_id': 1, 'error_message': 'Not enough fish', 'ok': False, } monkeypatch.setattr(bot.request, 'post', test) assert bot.answer_shipping_query(1, False, error_message='Not enough fish') def test_answer_shipping_query_errors(self, monkeypatch, bot): shipping_options = ShippingOption(1, 'option1', [LabeledPrice('price', 100)]) with pytest.raises(TelegramError, match='should not be empty and there should not be'): bot.answer_shipping_query(1, True, error_message='Not enough fish') with pytest.raises(TelegramError, match='should not be empty and there should not be'): bot.answer_shipping_query(1, False) with pytest.raises(TelegramError, match='should not be empty and there should not be'): bot.answer_shipping_query(1, False, shipping_options=shipping_options) with pytest.raises(TelegramError, match='should not be empty and there should not be'): bot.answer_shipping_query(1, True) with pytest.raises(AssertionError): bot.answer_shipping_query(1, True, shipping_options=[]) # TODO: Needs improvement. Need incoming pre checkout queries to test def test_answer_pre_checkout_query_ok(self, monkeypatch, bot): # For now just test that our internals pass the correct data def test(url, data, *args, **kwargs): return data == {'pre_checkout_query_id': 1, 'ok': True} monkeypatch.setattr(bot.request, 'post', test) assert bot.answer_pre_checkout_query(1, True) def test_answer_pre_checkout_query_error_message(self, monkeypatch, bot): # For now just test that our internals pass the correct data def test(url, data, *args, **kwargs): return data == { 'pre_checkout_query_id': 1, 'error_message': 'Not enough fish', 'ok': False, } monkeypatch.setattr(bot.request, 'post', test) assert bot.answer_pre_checkout_query(1, False, error_message='Not enough fish') def test_answer_pre_checkout_query_errors(self, monkeypatch, bot): with pytest.raises(TelegramError, match='should not be'): bot.answer_pre_checkout_query(1, True, error_message='Not enough fish') with pytest.raises(TelegramError, match='should not be empty'): bot.answer_pre_checkout_query(1, False) @flaky(3, 1) def test_restrict_chat_member(self, bot, channel_id, chat_permissions): # TODO: Add bot to supergroup so this can be tested properly with pytest.raises(BadRequest, match='Method is available only for supergroups'): assert bot.restrict_chat_member( channel_id, 95205500, chat_permissions, until_date=dtm.datetime.utcnow() ) 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) def test(url, data, *args, **kwargs): return data.get('until_date', until_timestamp) == until_timestamp monkeypatch.setattr(tz_bot.request, 'post', test) assert tz_bot.restrict_chat_member(channel_id, 95205500, chat_permissions) assert tz_bot.restrict_chat_member( channel_id, 95205500, chat_permissions, until_date=until ) assert tz_bot.restrict_chat_member( channel_id, 95205500, chat_permissions, until_date=until_timestamp ) @flaky(3, 1) 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 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_voice_chats=True, ) # Test that we pass the correct params to TG 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_voice_chats') == 11 ) monkeypatch.setattr(bot, '_post', make_assertion) assert 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_voice_chats=11, ) @flaky(3, 1) def test_export_chat_invite_link(self, bot, channel_id): # Each link is unique apparently invite_link = bot.export_chat_invite_link(channel_id) assert isinstance(invite_link, str) assert invite_link != '' def test_create_edit_invite_link_mutually_exclusive_arguments(self, bot, channel_id): data = {'chat_id': channel_id, 'member_limit': 17, 'creates_join_request': True} with pytest.raises(ValueError, match="`member_limit` can't be specified"): bot.create_chat_invite_link(**data) data.update({'invite_link': 'https://invite.link'}) with pytest.raises(ValueError, match="`member_limit` can't be specified"): bot.edit_chat_invite_link(**data) @flaky(3, 1) @pytest.mark.parametrize('creates_join_request', [True, False]) @pytest.mark.parametrize('name', [None, 'name']) 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 = 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 = bot.revoke_chat_invite_link( chat_id=channel_id, invite_link=invite_link.invite_link ) assert revoked_link.is_revoked @flaky(3, 1) @pytest.mark.parametrize('datetime', argvalues=[True, False], ids=['datetime', 'integer']) 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 = pytz.UTC.localize(time_in_future) invite_link = 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 pytest.approx(invite_link.expire_date == aware_time_in_future) 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 = pytz.UTC.localize(time_in_future) edited_invite_link = 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 pytest.approx(edited_invite_link.expire_date == aware_time_in_future) assert edited_invite_link.name == 'NewName' assert edited_invite_link.member_limit == 20 edited_invite_link = 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 pytest.approx(edited_invite_link.expire_date == aware_time_in_future) assert edited_invite_link.name == 'EvenNewerName' assert edited_invite_link.creates_join_request is True assert edited_invite_link.member_limit is None revoked_invite_link = 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 is True @flaky(3, 1) 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 = 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 pytest.approx(invite_link.expire_date == aware_expire_date) 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 = 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 pytest.approx(edited_invite_link.expire_date == aware_expire_date) assert edited_invite_link.name == 'NewName' assert edited_invite_link.member_limit == 20 edited_invite_link = 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 pytest.approx(edited_invite_link.expire_date == aware_expire_date) assert edited_invite_link.name == 'EvenNewerName' assert edited_invite_link.creates_join_request is True assert edited_invite_link.member_limit is None revoked_invite_link = 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 is True @flaky(3, 1) 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'): bot.approve_chat_join_request(chat_id=channel_id, user_id=chat_id) @flaky(3, 1) 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 with pytest.raises(BadRequest, match='User_already_participant'): bot.decline_chat_join_request(chat_id=channel_id, user_id=chat_id) @flaky(3, 1) def test_set_chat_photo(self, bot, channel_id): def func(): assert bot.set_chat_photo(channel_id, f) with open('tests/data/telegram_test_channel.jpg', 'rb') as f: expect_bad_request(func, 'Type of file mismatch', 'Telegram did not accept the file.') def test_set_chat_photo_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('photo') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.set_chat_photo(chat_id, file) assert test_flag @flaky(3, 1) def test_delete_chat_photo(self, bot, channel_id): def func(): assert bot.delete_chat_photo(channel_id) expect_bad_request(func, 'Chat_not_modified', 'Chat photo was not set.') @flaky(3, 1) def test_set_chat_title(self, bot, channel_id): assert bot.set_chat_title(channel_id, '>>> telegram.Bot() - Tests') @flaky(3, 1) def test_set_chat_description(self, bot, channel_id): assert bot.set_chat_description(channel_id, 'Time: ' + str(time.time())) # TODO: Add bot to group to test there too @flaky(3, 1) def test_pin_and_unpin_message(self, bot, super_group_id): message1 = bot.send_message(super_group_id, text="test_pin_message_1") message2 = bot.send_message(super_group_id, text="test_pin_message_2") message3 = bot.send_message(super_group_id, text="test_pin_message_3") assert bot.pin_chat_message( chat_id=super_group_id, message_id=message1.message_id, disable_notification=True ) time.sleep(1) bot.pin_chat_message( chat_id=super_group_id, message_id=message2.message_id, disable_notification=True ) time.sleep(1) bot.pin_chat_message( chat_id=super_group_id, message_id=message3.message_id, disable_notification=True ) time.sleep(1) chat = bot.get_chat(super_group_id) assert chat.pinned_message == message3 assert bot.unpin_chat_message(super_group_id, message_id=message2.message_id) assert bot.unpin_chat_message(super_group_id) assert bot.unpin_all_chat_messages(super_group_id) # get_sticker_set, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, # set_sticker_position_in_set and delete_sticker_from_set are tested in the # test_sticker module. def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id): from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout class OkException(Exception): pass TIMEOUT = 500 def request_wrapper(*args, **kwargs): obj = kwargs.get('timeout') if isinstance(obj, Timeout) and obj._read == TIMEOUT: raise OkException return b'{"ok": true, "result": []}' monkeypatch.setattr('telegram.utils.request.Request._request_wrapper', request_wrapper) # Test file uploading with pytest.raises(OkException): bot.send_photo(chat_id, open('tests/data/telegram.jpg', 'rb'), timeout=TIMEOUT) # Test JSON submission with pytest.raises(OkException): bot.get_chat_administrators(chat_id, timeout=TIMEOUT) def test_timeout_propagation_implicit(self, monkeypatch, bot, chat_id): from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout class OkException(Exception): pass def request_wrapper(*args, **kwargs): obj = kwargs.get('timeout') if isinstance(obj, Timeout) and obj._read == 20: raise OkException return b'{"ok": true, "result": []}' monkeypatch.setattr('telegram.utils.request.Request._request_wrapper', request_wrapper) # Test file uploading with pytest.raises(OkException): bot.send_photo(chat_id, open('tests/data/telegram.jpg', 'rb')) @flaky(3, 1) 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 = bot.send_message(chat_id=chat_id, text=test_string, entities=entities) assert message.text == test_string assert message.entities == entities @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_message_default_parse_mode(self, default_bot, chat_id): test_string = 'Italic Bold Code' test_markdown_string = '_Italic_ *Bold* `Code`' message = default_bot.send_message(chat_id, test_markdown_string) assert message.text_markdown == test_markdown_string assert message.text == test_string message = default_bot.send_message(chat_id, test_markdown_string, parse_mode=None) assert message.text == test_markdown_string assert message.text_markdown == escape_markdown(test_markdown_string) message = default_bot.send_message(chat_id, test_markdown_string, parse_mode='HTML') assert message.text == test_markdown_string assert message.text_markdown == escape_markdown(test_markdown_string) @flaky(3, 1) @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'], ) def test_send_message_default_allow_sending_without_reply(self, default_bot, chat_id, custom): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_message( chat_id, 'test', reply_to_message_id=reply_to_message.message_id ) @flaky(3, 1) def test_set_and_get_my_commands(self, bot): commands = [ BotCommand('cmd1', 'descr1'), BotCommand('cmd2', 'descr2'), ] bot.set_my_commands([]) assert bot.get_my_commands() == [] assert bot.commands == [] assert bot.set_my_commands(commands) for bc in [bot.get_my_commands(), bot.commands]: assert len(bc) == 2 assert bc[0].command == 'cmd1' assert bc[0].description == 'descr1' assert bc[1].command == 'cmd2' assert bc[1].description == 'descr2' @flaky(3, 1) def test_set_and_get_my_commands_strings(self, bot): commands = [ ['cmd1', 'descr1'], ['cmd2', 'descr2'], ] bot.set_my_commands([]) assert bot.get_my_commands() == [] assert bot.commands == [] assert bot.set_my_commands(commands) for bc in [bot.get_my_commands(), bot.commands]: assert len(bc) == 2 assert bc[0].command == 'cmd1' assert bc[0].description == 'descr1' assert bc[1].command == 'cmd2' assert bc[1].description == 'descr2' @flaky(3, 1) 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 bot.set_my_commands(group_cmds, scope=group_scope, language_code='en') gotten_group_cmds = 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 bot.set_my_commands(private_cmds, scope=private_scope) gotten_private_cmd = 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 assert len(bot.commands) == 2 # set from previous test. Makes sure this hasn't changed. assert bot.commands[0].command == 'cmd1' # Delete command list from that supergroup and private chat- bot.delete_my_commands(private_scope) bot.delete_my_commands(group_scope, 'en') # Check if its been deleted- deleted_priv_cmds = bot.get_my_commands(scope=private_scope) deleted_grp_cmds = bot.get_my_commands(scope=group_scope, language_code='en') assert len(deleted_grp_cmds) == 0 == len(group_cmds) - 1 assert len(deleted_priv_cmds) == 0 == len(private_cmds) - 1 bot.delete_my_commands() # Delete commands from default scope assert not bot.commands # Check if this has been updated to reflect the deletion. def test_log_out(self, monkeypatch, bot): # We don't actually make a request as to not break the test setup def assertion(url, data, *args, **kwargs): return data == {} and url.split('/')[-1] == 'logOut' monkeypatch.setattr(bot.request, 'post', assertion) assert bot.log_out() def test_close(self, monkeypatch, bot): # We don't actually make a request as to not break the test setup def assertion(url, data, *args, **kwargs): return data == {} and url.split('/')[-1] == 'close' monkeypatch.setattr(bot.request, 'post', assertion) assert bot.close() @flaky(3, 1) @pytest.mark.parametrize('json_keyboard', [True, False]) @pytest.mark.parametrize('caption', ["Test", '', None]) def test_copy_message(self, monkeypatch, bot, chat_id, media_message, json_keyboard, caption): keyboard = InlineKeyboardMarkup( [[InlineKeyboardButton(text="test", callback_data="test2")]] ) def post(url, data, timeout): assert data["chat_id"] == chat_id assert data["from_chat_id"] == chat_id assert data["message_id"] == media_message.message_id assert data.get("caption") == caption assert data["parse_mode"] == ParseMode.HTML assert data["reply_to_message_id"] == media_message.message_id assert data["reply_markup"] == keyboard.to_json() assert data["disable_notification"] is True assert data["caption_entities"] == [MessageEntity(MessageEntity.BOLD, 0, 4)] assert data['protect_content'] is True return data monkeypatch.setattr(bot.request, 'post', post) 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, ) @flaky(3, 1) def test_copy_message_without_reply(self, bot, chat_id, media_message): keyboard = InlineKeyboardMarkup( [[InlineKeyboardButton(text="test", callback_data="test2")]] ) returned = 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 = 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 @flaky(3, 1) @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'], ) def test_copy_message_with_default(self, default_bot, chat_id, media_message): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if not default_bot.defaults.allow_sending_without_reply: with pytest.raises(BadRequest, match='not found'): 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 = 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 = 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 def test_replace_callback_data_send_message(self, bot, chat_id): try: bot.arbitrary_callback_data = True 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 = 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 = list(bot.callback_data_cache._keyboard_data)[0] data = list(bot.callback_data_cache._keyboard_data[keyboard].button_data.values())[0] assert data == 'replace_test' finally: bot.arbitrary_callback_data = False bot.callback_data_cache.clear_callback_data() bot.callback_data_cache.clear_callback_queries() def test_replace_callback_data_stop_poll_and_repl_to_message(self, bot, chat_id): poll_message = bot.send_poll(chat_id=chat_id, question='test', options=['1', '2']) try: bot.arbitrary_callback_data = True 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, ] ) poll_message.stop_poll(reply_markup=reply_markup) helper_message = 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 = list(bot.callback_data_cache._keyboard_data)[0] data = list(bot.callback_data_cache._keyboard_data[keyboard].button_data.values())[0] assert data == 'replace_test' finally: bot.arbitrary_callback_data = False bot.callback_data_cache.clear_callback_data() bot.callback_data_cache.clear_callback_queries() def test_replace_callback_data_copy_message(self, 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""" original_message = bot.send_message(chat_id=chat_id, text='original') try: bot.arbitrary_callback_data = True 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 = original_message.copy(chat_id=chat_id, reply_markup=reply_markup) helper_message = 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 = list(bot.callback_data_cache._keyboard_data)[0] data = list(bot.callback_data_cache._keyboard_data[keyboard].button_data.values())[0] assert data == 'replace_test' finally: bot.arbitrary_callback_data = False 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. def test_replace_callback_data_answer_inline_query(self, monkeypatch, bot, chat_id): # For now just test that our internals pass the correct data def make_assertion( endpoint, data=None, timeout=None, api_kwargs=None, ): inline_keyboard = InlineKeyboardMarkup.de_json( data['results'][0]['reply_markup'], bot ).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 = 'reply_markup' not in data['results'][1] return assertion_1 and assertion_2 and assertion_3 and assertion_4 try: bot.arbitrary_callback_data = True 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 bot.answer_inline_query(chat_id, results=results) finally: bot.arbitrary_callback_data = False bot.callback_data_cache.clear_callback_data() bot.callback_data_cache.clear_callback_queries() def test_get_chat_arbitrary_callback_data(self, super_group_id, bot): try: bot.arbitrary_callback_data = True reply_markup = InlineKeyboardMarkup.from_button( InlineKeyboardButton(text='text', callback_data='callback_data') ) message = bot.send_message( super_group_id, text='get_chat_arbitrary_callback_data', reply_markup=reply_markup ) message.pin() keyboard = list(bot.callback_data_cache._keyboard_data)[0] data = list(bot.callback_data_cache._keyboard_data[keyboard].button_data.values())[0] assert data == 'callback_data' chat = bot.get_chat(super_group_id) assert chat.pinned_message == message assert chat.pinned_message.reply_markup == reply_markup finally: bot.arbitrary_callback_data = False bot.callback_data_cache.clear_callback_data() bot.callback_data_cache.clear_callback_queries() bot.unpin_all_chat_messages(super_group_id) # 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. def test_arbitrary_callback_data_no_insert(self, monkeypatch, bot): """Updates that don't need insertion shouldn.t fail obviously""" 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: bot.arbitrary_callback_data = True monkeypatch.setattr(bot.request, 'post', post) bot.delete_webhook() # make sure there is no webhook set if webhook tests failed updates = bot.get_updates(timeout=1) assert len(updates) == 1 assert updates[0].update_id == 17 assert updates[0].poll.id == '42' finally: bot.arbitrary_callback_data = False @pytest.mark.parametrize( 'message_type', ['channel_post', 'edited_channel_post', 'message', 'edited_message'] ) def test_arbitrary_callback_data_pinned_message_reply_to_message( self, super_group_id, bot, monkeypatch, message_type ): bot.arbitrary_callback_data = True reply_markup = InlineKeyboardMarkup.from_button( InlineKeyboardButton(text='text', callback_data='callback_data') ) message = Message( 1, None, None, reply_markup=bot.callback_data_cache.process_keyboard(reply_markup) ) # 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) def post(*args, **kwargs): update = Update( 17, **{ message_type: Message( 1, None, None, pinned_message=message, reply_to_message=Message.de_json(message.to_dict(), bot), ) }, ) return [update.to_dict()] try: monkeypatch.setattr(bot.request, 'post', post) bot.delete_webhook() # make sure there is no webhook set if webhook tests failed updates = bot.get_updates(timeout=1) assert isinstance(updates, list) 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.arbitrary_callback_data = False bot.callback_data_cache.clear_callback_data() bot.callback_data_cache.clear_callback_queries() def test_arbitrary_callback_data_get_chat_no_pinned_message(self, super_group_id, bot): bot.arbitrary_callback_data = True bot.unpin_all_chat_messages(super_group_id) try: chat = 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.arbitrary_callback_data = False @pytest.mark.parametrize( 'message_type', ['channel_post', 'edited_channel_post', 'message', 'edited_message'] ) @pytest.mark.parametrize('self_sender', [True, False]) def test_arbitrary_callback_data_via_bot( self, super_group_id, bot, monkeypatch, self_sender, message_type ): bot.arbitrary_callback_data = True 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, None, None, reply_markup=reply_markup, via_bot=bot.bot if self_sender else User(1, 'first', False), ) def post(*args, **kwargs): return [Update(17, **{message_type: message}).to_dict()] try: monkeypatch.setattr(bot.request, 'post', post) bot.delete_webhook() # make sure there is no webhook set if webhook tests failed updates = bot.get_updates(timeout=1) assert isinstance(updates, list) 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.arbitrary_callback_data = False bot.callback_data_cache.clear_callback_data() bot.callback_data_cache.clear_callback_queries() @pytest.mark.parametrize( 'cls,warn', [(Bot, True), (BotSubClass, True), (ExtBot, False), (ExtBotSubClass, False)] ) def test_defaults_warning(self, bot, recwarn, cls, warn): defaults = Defaults() cls(bot.token, defaults=defaults) if warn: assert len(recwarn) == 1 assert 'Passing Defaults to telegram.Bot is deprecated.' in str(recwarn[-1].message) else: assert len(recwarn) == 0 def test_camel_case_redefinition_extbot(self): invalid_camel_case_functions = [] for function_name, function in ExtBot.__dict__.items(): camel_case_function = getattr(ExtBot, to_camel_case(function_name), False) if callable(function) and camel_case_function and camel_case_function is not function: invalid_camel_case_functions.append(function_name) assert invalid_camel_case_functions == [] def test_camel_case_bot(self): not_available_camelcase_functions = [] for function_name, function in Bot.__dict__.items(): if ( function_name.startswith("_") or not callable(function) or function_name == "to_dict" ): continue camel_case_function = getattr(Bot, to_camel_case(function_name), False) if not camel_case_function: not_available_camelcase_functions.append(function_name) assert not_available_camelcase_functions == [] python-telegram-bot-13.11/tests/test_botcommand.py000066400000000000000000000052541417656324400223400ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope="class") def bot_command(): return BotCommand(command='start', description='A command') class TestBotCommand: command = 'start' description = 'A command' def test_slot_behaviour(self, bot_command, recwarn, mro_slots): for attr in bot_command.__slots__: assert getattr(bot_command, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not bot_command.__dict__, f"got missing slot(s): {bot_command.__dict__}" assert len(mro_slots(bot_command)) == len(set(mro_slots(bot_command))), "duplicate slot" bot_command.custom, bot_command.command = 'should give warning', self.command assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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-13.11/tests/test_botcommandscope.py000066400000000000000000000155521417656324400233740ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, BotCommandScope, BotCommandScopeDefault, BotCommandScopeAllPrivateChats, BotCommandScopeAllGroupChats, BotCommandScopeAllChatAdministrators, BotCommandScopeChat, BotCommandScopeChatAdministrators, BotCommandScopeChatMember, ) @pytest.fixture(scope="class", 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="class", 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="class", 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='class') def bot_command_scope(scope_class_and_type, chat_id): return scope_class_and_type[0](type=scope_class_and_type[1], chat_id=chat_id, user_id=42) # All the scope types are very similar, so we test everything via parametrization class TestBotCommandScope: def test_slot_behaviour(self, bot_command_scope, mro_slots, recwarn): for attr in bot_command_scope.__slots__: assert getattr(bot_command_scope, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not bot_command_scope.__dict__, f"got missing slot(s): {bot_command_scope.__dict__}" assert len(mro_slots(bot_command_scope)) == len( set(mro_slots(bot_command_scope)) ), "duplicate slot" bot_command_scope.custom, bot_command_scope.type = 'warning!', bot_command_scope.type assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 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_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-13.11/tests/test_callbackcontext.py000066400000000000000000000212201417656324400233450ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ( Update, Message, Chat, User, TelegramError, Bot, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery, ) from telegram.ext import CallbackContext """ CallbackContext.refresh_data is tested in TestBasePersistence """ class TestCallbackContext: def test_slot_behaviour(self, cdp, recwarn, mro_slots): c = CallbackContext(cdp) 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" c.args = c.args assert len(recwarn) == 0, recwarn.list def test_non_context_dp(self, dp): with pytest.raises(ValueError): CallbackContext(dp) def test_from_job(self, cdp): job = cdp.job_queue.run_once(lambda x: x, 10) callback_context = CallbackContext.from_job(job, cdp) 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 cdp.bot_data assert callback_context.bot is cdp.bot assert callback_context.job_queue is cdp.job_queue assert callback_context.update_queue is cdp.update_queue def test_from_update(self, cdp): update = Update( 0, message=Message(0, None, Chat(1, 'chat'), from_user=User(1, 'user', False)) ) callback_context = CallbackContext.from_update(update, cdp) assert callback_context.chat_data == {} assert callback_context.user_data == {} assert callback_context.bot_data is cdp.bot_data assert callback_context.bot is cdp.bot assert callback_context.job_queue is cdp.job_queue assert callback_context.update_queue is cdp.update_queue callback_context_same_user_chat = CallbackContext.from_update(update, cdp) 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, cdp) 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, cdp): callback_context = CallbackContext.from_update(None, cdp) assert callback_context.chat_data is None assert callback_context.user_data is None assert callback_context.bot_data is cdp.bot_data assert callback_context.bot is cdp.bot assert callback_context.job_queue is cdp.job_queue assert callback_context.update_queue is cdp.update_queue callback_context = CallbackContext.from_update('', cdp) assert callback_context.chat_data is None assert callback_context.user_data is None assert callback_context.bot_data is cdp.bot_data assert callback_context.bot is cdp.bot assert callback_context.job_queue is cdp.job_queue assert callback_context.update_queue is cdp.update_queue def test_from_error(self, cdp): error = TelegramError('test') update = Update( 0, message=Message(0, None, Chat(1, 'chat'), from_user=User(1, 'user', False)) ) callback_context = CallbackContext.from_error(update, error, cdp) assert callback_context.error is error assert callback_context.chat_data == {} assert callback_context.user_data == {} assert callback_context.bot_data is cdp.bot_data assert callback_context.bot is cdp.bot assert callback_context.job_queue is cdp.job_queue assert callback_context.update_queue is cdp.update_queue assert callback_context.async_args is None assert callback_context.async_kwargs is None def test_from_error_async_params(self, cdp): error = TelegramError('test') args = [1, '2'] kwargs = {'one': 1, 2: 'two'} callback_context = CallbackContext.from_error( None, error, cdp, async_args=args, async_kwargs=kwargs ) assert callback_context.error is error assert callback_context.async_args is args assert callback_context.async_kwargs is kwargs def test_match(self, cdp): callback_context = CallbackContext(cdp) assert callback_context.match is None callback_context.matches = ['test', 'blah'] assert callback_context.match == 'test' def test_data_assignment(self, cdp): update = Update( 0, message=Message(0, None, Chat(1, 'chat'), from_user=User(1, 'user', False)) ) callback_context = CallbackContext.from_update(update, cdp) 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_dispatcher_attribute(self, cdp): callback_context = CallbackContext(cdp) assert callback_context.dispatcher == cdp def test_drop_callback_data_exception(self, bot, cdp): 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, cdp) with pytest.raises(RuntimeError, match='This telegram.ext.ExtBot instance does not'): callback_context.drop_callback_data(None) try: cdp.bot = non_ext_bot with pytest.raises(RuntimeError, match='telegram.Bot does not allow for'): callback_context.drop_callback_data(None) finally: cdp.bot = bot def test_drop_callback_data(self, cdp, monkeypatch, chat_id): monkeypatch.setattr(cdp.bot, 'arbitrary_callback_data', True) update = Update( 0, message=Message(0, None, Chat(1, 'chat'), from_user=User(1, 'user', False)) ) callback_context = CallbackContext.from_update(update, cdp) cdp.bot.send_message( chat_id=chat_id, text='test', reply_markup=InlineKeyboardMarkup.from_button( InlineKeyboardButton('test', callback_data='callback_data') ), ) keyboard_uuid = cdp.bot.callback_data_cache.persistence_data[0][0][0] button_uuid = list(cdp.bot.callback_data_cache.persistence_data[0][0][2])[0] callback_data = keyboard_uuid + button_uuid callback_query = CallbackQuery( id='1', from_user=None, chat_instance=None, data=callback_data, ) cdp.bot.callback_data_cache.process_callback_query(callback_query) try: assert len(cdp.bot.callback_data_cache.persistence_data[0]) == 1 assert list(cdp.bot.callback_data_cache.persistence_data[1]) == ['1'] callback_context.drop_callback_data(callback_query) assert cdp.bot.callback_data_cache.persistence_data == ([], {}) finally: cdp.bot.callback_data_cache.clear_callback_data() cdp.bot.callback_data_cache.clear_callback_queries() python-telegram-bot-13.11/tests/test_callbackdatacache.py000066400000000000000000000416341417656324400235710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 import pytz from telegram import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, Message, User from telegram.ext.callbackdatacache import ( CallbackDataCache, _KeyboardData, InvalidCallbackData, ) @pytest.fixture(scope='function') def callback_data_cache(bot): return CallbackDataCache(bot) class TestInvalidCallbackData: def test_slot_behaviour(self, mro_slots, recwarn): 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" with pytest.raises(AttributeError): invalid_callback_data.custom class TestKeyboardData: def test_slot_behaviour(self, mro_slots): 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" with pytest.raises(AttributeError): keyboard_data.custom = 42 class TestCallbackDataCache: def test_slot_behaviour(self, callback_data_cache, mro_slots): for attr in callback_data_cache.__slots__: attr = ( f"_CallbackDataCache{attr}" if attr.startswith('__') and not attr.endswith('__') else attr ) assert getattr(callback_data_cache, attr, 'err') != 'err', f"got extra slot '{attr}'" assert len(mro_slots(callback_data_cache)) == len( set(mro_slots(callback_data_cache)) ), "duplicate slot" with pytest.raises(AttributeError): callback_data_cache.custom = 42 @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): 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, button_1 = cdc.extract_uuids(out1.inline_keyboard[0][1].callback_data) keyboard_2, button_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() effective_message = Message(message_id=1, date=None, chat=None, reply_markup=out) 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] == list(callback_data_cache._keyboard_data.keys())[0] ) 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(pytz.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 = [ list(data[2].values())[0] for data in callback_data_cache.persistence_data[0] ] assert callback_data == list(str(i) for i in range(50, 100)) python-telegram-bot-13.11/tests/test_callbackquery.py000066400000000000000000000442161417656324400230400ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 CallbackQuery, User, Message, Chat, Audio, Bot from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling @pytest.fixture(scope='function', params=['message', 'inline']) def callback_query(bot, request): cbq = CallbackQuery( TestCallbackQuery.id_, TestCallbackQuery.from_user, TestCallbackQuery.chat_instance, data=TestCallbackQuery.data, game_short_name=TestCallbackQuery.game_short_name, bot=bot, ) if request.param == 'message': cbq.message = TestCallbackQuery.message cbq.message.bot = bot else: cbq.inline_message_id = TestCallbackQuery.inline_message_id return cbq class TestCallbackQuery: id_ = 'id' from_user = User(1, 'test_user', False) chat_instance = 'chat_instance' message = Message(3, None, Chat(4, 'private'), from_user=User(5, 'bot', False)) data = 'data' inline_message_id = 'inline_message_id' game_short_name = 'the_game' def test_slot_behaviour(self, callback_query, recwarn, mro_slots): for attr in callback_query.__slots__: assert getattr(callback_query, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not callback_query.__dict__, f"got missing slot(s): {callback_query.__dict__}" assert len(mro_slots(callback_query)) == len(set(mro_slots(callback_query))), "same slot" callback_query.custom, callback_query.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @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_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.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: assert callback_query_dict['message'] == callback_query.message.to_dict() else: 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_answer(self, monkeypatch, callback_query): 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 check_shortcut_call( callback_query.answer, callback_query.bot, 'answer_callback_query' ) assert check_defaults_handling(callback_query.answer, callback_query.bot) monkeypatch.setattr(callback_query.bot, 'answer_callback_query', make_assertion) # TODO: PEP8 assert callback_query.answer() def test_edit_message_text(self, monkeypatch, callback_query): 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 check_shortcut_call( callback_query.edit_message_text, callback_query.bot, 'edit_message_text', skip_params=self.skip_params(callback_query), shortcut_kwargs=self.shortcut_kwargs(callback_query), ) assert check_defaults_handling(callback_query.edit_message_text, callback_query.bot) monkeypatch.setattr(callback_query.bot, 'edit_message_text', make_assertion) assert callback_query.edit_message_text(text='test') assert callback_query.edit_message_text('test') def test_edit_message_caption(self, monkeypatch, callback_query): 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 check_shortcut_call( callback_query.edit_message_caption, callback_query.bot, 'edit_message_caption', skip_params=self.skip_params(callback_query), shortcut_kwargs=self.shortcut_kwargs(callback_query), ) assert check_defaults_handling(callback_query.edit_message_caption, callback_query.bot) monkeypatch.setattr(callback_query.bot, 'edit_message_caption', make_assertion) assert callback_query.edit_message_caption(caption='new caption') assert callback_query.edit_message_caption('new caption') def test_edit_message_reply_markup(self, monkeypatch, callback_query): 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 check_shortcut_call( callback_query.edit_message_reply_markup, callback_query.bot, 'edit_message_reply_markup', skip_params=self.skip_params(callback_query), shortcut_kwargs=self.shortcut_kwargs(callback_query), ) assert check_defaults_handling( callback_query.edit_message_reply_markup, callback_query.bot ) monkeypatch.setattr(callback_query.bot, 'edit_message_reply_markup', make_assertion) assert callback_query.edit_message_reply_markup(reply_markup=[['1', '2']]) assert callback_query.edit_message_reply_markup([['1', '2']]) def test_edit_message_media(self, monkeypatch, callback_query): 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 check_shortcut_call( callback_query.edit_message_media, callback_query.bot, 'edit_message_media', skip_params=self.skip_params(callback_query), shortcut_kwargs=self.shortcut_kwargs(callback_query), ) assert check_defaults_handling(callback_query.edit_message_media, callback_query.bot) monkeypatch.setattr(callback_query.bot, 'edit_message_media', make_assertion) assert callback_query.edit_message_media(media=[['1', '2']]) assert callback_query.edit_message_media([['1', '2']]) def test_edit_message_live_location(self, monkeypatch, callback_query): 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 check_shortcut_call( callback_query.edit_message_live_location, callback_query.bot, 'edit_message_live_location', skip_params=self.skip_params(callback_query), shortcut_kwargs=self.shortcut_kwargs(callback_query), ) assert check_defaults_handling( callback_query.edit_message_live_location, callback_query.bot ) monkeypatch.setattr(callback_query.bot, 'edit_message_live_location', make_assertion) assert callback_query.edit_message_live_location(latitude=1, longitude=2) assert callback_query.edit_message_live_location(1, 2) def test_stop_message_live_location(self, monkeypatch, callback_query): def make_assertion(*_, **kwargs): ids = self.check_passed_ids(callback_query, kwargs) return ids assert check_shortcut_signature( CallbackQuery.stop_message_live_location, Bot.stop_message_live_location, ['inline_message_id', 'message_id', 'chat_id'], [], ) assert check_shortcut_call( callback_query.stop_message_live_location, callback_query.bot, 'stop_message_live_location', skip_params=self.skip_params(callback_query), shortcut_kwargs=self.shortcut_kwargs(callback_query), ) assert check_defaults_handling( callback_query.stop_message_live_location, callback_query.bot ) monkeypatch.setattr(callback_query.bot, 'stop_message_live_location', make_assertion) assert callback_query.stop_message_live_location() def test_set_game_score(self, monkeypatch, callback_query): 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 check_shortcut_call( callback_query.set_game_score, callback_query.bot, 'set_game_score', skip_params=self.skip_params(callback_query), shortcut_kwargs=self.shortcut_kwargs(callback_query), ) assert check_defaults_handling(callback_query.set_game_score, callback_query.bot) monkeypatch.setattr(callback_query.bot, 'set_game_score', make_assertion) assert callback_query.set_game_score(user_id=1, score=2) assert callback_query.set_game_score(1, 2) def test_get_game_high_scores(self, monkeypatch, callback_query): 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 check_shortcut_call( callback_query.get_game_high_scores, callback_query.bot, 'get_game_high_scores', skip_params=self.skip_params(callback_query), shortcut_kwargs=self.shortcut_kwargs(callback_query), ) assert check_defaults_handling(callback_query.get_game_high_scores, callback_query.bot) monkeypatch.setattr(callback_query.bot, 'get_game_high_scores', make_assertion) assert callback_query.get_game_high_scores(user_id=1) assert callback_query.get_game_high_scores(1) def test_delete_message(self, monkeypatch, callback_query): if callback_query.inline_message_id: pytest.skip("Can't delete inline messages") 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 check_shortcut_call( callback_query.delete_message, callback_query.bot, 'delete_message' ) assert check_defaults_handling(callback_query.delete_message, callback_query.bot) monkeypatch.setattr(callback_query.bot, 'delete_message', make_assertion) assert callback_query.delete_message() def test_pin_message(self, monkeypatch, callback_query): if callback_query.inline_message_id: pytest.skip("Can't pin inline messages") 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 check_shortcut_call( callback_query.pin_message, callback_query.bot, 'pin_chat_message' ) assert check_defaults_handling(callback_query.pin_message, callback_query.bot) monkeypatch.setattr(callback_query.bot, 'pin_chat_message', make_assertion) assert callback_query.pin_message() def test_unpin_message(self, monkeypatch, callback_query): if callback_query.inline_message_id: pytest.skip("Can't unpin inline messages") 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 check_shortcut_call( callback_query.unpin_message, callback_query.bot, 'unpin_chat_message', shortcut_kwargs=['message_id', 'chat_id'], ) assert check_defaults_handling(callback_query.unpin_message, callback_query.bot) monkeypatch.setattr(callback_query.bot, 'unpin_chat_message', make_assertion) assert callback_query.unpin_message() def test_copy_message(self, monkeypatch, callback_query): if callback_query.inline_message_id: pytest.skip("Can't copy inline messages") 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 check_shortcut_call(callback_query.copy_message, callback_query.bot, 'copy_message') assert check_defaults_handling(callback_query.copy_message, callback_query.bot) monkeypatch.setattr(callback_query.bot, 'copy_message', make_assertion) assert callback_query.copy_message(1) 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) python-telegram-bot-13.11/tests/test_callbackqueryhandler.py000066400000000000000000000241701417656324400243730ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram import ( Update, CallbackQuery, Bot, Message, User, Chat, InlineQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, ) from telegram.ext import CallbackQueryHandler, CallbackContext, JobQueue 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(scope='function') def callback_query(bot): return Update(0, callback_query=CallbackQuery(2, User(1, '', False), None, data='test data')) class TestCallbackQueryHandler: test_flag = False def test_slot_behaviour(self, recwarn, mro_slots): handler = CallbackQueryHandler(self.callback_data_1, pass_user_data=True) for attr in handler.__slots__: assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" handler.custom, handler.callback = 'should give warning', self.callback_basic assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(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'} def callback_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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_context_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_basic(self, dp, callback_query): handler = CallbackQueryHandler(self.callback_basic) dp.add_handler(handler) assert handler.check_update(callback_query) dp.process_update(callback_query) assert self.test_flag 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) 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_with_passing_group_dict(self, dp, callback_query): handler = CallbackQueryHandler( self.callback_group, pattern='(?P.*)est(?P.*)', pass_groups=True ) dp.add_handler(handler) dp.process_update(callback_query) assert self.test_flag dp.remove_handler(handler) handler = CallbackQueryHandler( self.callback_group, pattern='(?P.*)est(?P.*)', pass_groupdict=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(callback_query) assert self.test_flag def test_pass_user_or_chat_data(self, dp, callback_query): handler = CallbackQueryHandler(self.callback_data_1, pass_user_data=True) dp.add_handler(handler) dp.process_update(callback_query) assert self.test_flag dp.remove_handler(handler) handler = CallbackQueryHandler(self.callback_data_1, pass_chat_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(callback_query) assert self.test_flag dp.remove_handler(handler) handler = CallbackQueryHandler( self.callback_data_2, pass_chat_data=True, pass_user_data=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(callback_query) assert self.test_flag def test_pass_job_or_update_queue(self, dp, callback_query): handler = CallbackQueryHandler(self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update(callback_query) assert self.test_flag dp.remove_handler(handler) handler = CallbackQueryHandler(self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(callback_query) assert self.test_flag dp.remove_handler(handler) handler = CallbackQueryHandler( self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(callback_query) assert self.test_flag def test_other_update_types(self, false_update): handler = CallbackQueryHandler(self.callback_basic) assert not handler.check_update(false_update) def test_context(self, cdp, callback_query): handler = CallbackQueryHandler(self.callback_context) cdp.add_handler(handler) cdp.process_update(callback_query) assert self.test_flag def test_context_pattern(self, cdp, callback_query): handler = CallbackQueryHandler( self.callback_context_pattern, pattern=r'(?P.*)est(?P.*)' ) cdp.add_handler(handler) cdp.process_update(callback_query) assert self.test_flag cdp.remove_handler(handler) handler = CallbackQueryHandler(self.callback_context_pattern, pattern=r'(t)est(.*)') cdp.add_handler(handler) cdp.process_update(callback_query) assert self.test_flag def test_context_callable_pattern(self, cdp, 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) cdp.add_handler(handler) cdp.process_update(callback_query) python-telegram-bot-13.11/tests/test_chat.py000066400000000000000000001034171417656324400211340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, ChatAction, ChatPermissions, ChatLocation, Location, Bot from telegram import User from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling @pytest.fixture(scope='class') def chat(bot): return Chat( TestChat.id_, TestChat.title, TestChat.type_, username=TestChat.username, all_members_are_administrators=TestChat.all_members_are_administrators, bot=bot, sticker_set_name=TestChat.sticker_set_name, can_set_sticker_set=TestChat.can_set_sticker_set, permissions=TestChat.permissions, slow_mode_delay=TestChat.slow_mode_delay, message_auto_delete_time=TestChat.message_auto_delete_time, bio=TestChat.bio, linked_chat_id=TestChat.linked_chat_id, location=TestChat.location, has_private_forwards=True, has_protected_content=True, ) class TestChat: 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 message_auto_delete_time = 42 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_private_forwards = True def test_slot_behaviour(self, chat, recwarn, mro_slots): for attr in chat.__slots__: assert getattr(chat, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not chat.__dict__, f"got missing slot(s): {chat.__dict__}" assert len(mro_slots(chat)) == len(set(mro_slots(chat))), "duplicate slot" chat.custom, chat.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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, 'message_auto_delete_time': self.message_auto_delete_time, 'bio': self.bio, 'has_protected_content': self.has_protected_content, 'has_private_forwards': self.has_private_forwards, 'linked_chat_id': self.linked_chat_id, 'location': self.location.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.all_members_are_administrators == self.all_members_are_administrators 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.message_auto_delete_time == self.message_auto_delete_time assert chat.bio == self.bio assert chat.has_protected_content == self.has_protected_content 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 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['all_members_are_administrators'] == chat.all_members_are_administrators assert chat_dict['permissions'] == chat.permissions.to_dict() assert chat_dict['slow_mode_delay'] == chat.slow_mode_delay assert chat_dict['message_auto_delete_time'] == chat.message_auto_delete_time assert chat_dict['bio'] == chat.bio assert chat_dict['has_private_forwards'] == chat.has_private_forwards assert chat_dict['has_protected_content'] == chat.has_protected_content assert chat_dict['linked_chat_id'] == chat.linked_chat_id assert chat_dict['location'] == chat.location.to_dict() 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_send_action(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_action, chat.bot, 'send_chat_action') assert check_defaults_handling(chat.send_action, chat.bot) monkeypatch.setattr(chat.bot, 'send_chat_action', make_assertion) assert chat.send_action(action=ChatAction.TYPING) assert chat.send_action(action=ChatAction.TYPING) def test_leave(self, monkeypatch, chat): def make_assertion(*_, **kwargs): return kwargs['chat_id'] == chat.id assert check_shortcut_signature(Chat.leave, Bot.leave_chat, ['chat_id'], []) assert check_shortcut_call(chat.leave, chat.bot, 'leave_chat') assert check_defaults_handling(chat.leave, chat.bot) monkeypatch.setattr(chat.bot, 'leave_chat', make_assertion) assert chat.leave() def test_get_administrators(self, monkeypatch, chat): def make_assertion(*_, **kwargs): return kwargs['chat_id'] == chat.id assert check_shortcut_signature( Chat.get_administrators, Bot.get_chat_administrators, ['chat_id'], [] ) assert check_shortcut_call(chat.get_administrators, chat.bot, 'get_chat_administrators') assert check_defaults_handling(chat.get_administrators, chat.bot) monkeypatch.setattr(chat.bot, 'get_chat_administrators', make_assertion) assert chat.get_administrators() def test_get_member_count(self, monkeypatch, chat): 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 check_shortcut_call(chat.get_member_count, chat.bot, 'get_chat_member_count') assert check_defaults_handling(chat.get_member_count, chat.bot) monkeypatch.setattr(chat.bot, 'get_chat_member_count', make_assertion) assert chat.get_member_count() def test_get_members_count_warning(self, chat, monkeypatch, recwarn): def make_assertion(*_, **kwargs): return kwargs['chat_id'] == chat.id monkeypatch.setattr(chat.bot, 'get_chat_member_count', make_assertion) assert chat.get_members_count() assert len(recwarn) == 1 assert '`Chat.get_members_count` is deprecated' in str(recwarn[0].message) def test_get_member(self, monkeypatch, chat): 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 check_shortcut_call(chat.get_member, chat.bot, 'get_chat_member') assert check_defaults_handling(chat.get_member, chat.bot) monkeypatch.setattr(chat.bot, 'get_chat_member', make_assertion) assert chat.get_member(user_id=42) def test_ban_member(self, monkeypatch, chat): 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 check_shortcut_call(chat.ban_member, chat.bot, 'ban_chat_member') assert check_defaults_handling(chat.ban_member, chat.bot) monkeypatch.setattr(chat.bot, 'ban_chat_member', make_assertion) assert chat.ban_member(user_id=42, until_date=43) def test_ban_sender_chat(self, monkeypatch, chat): 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 check_shortcut_call(chat.ban_sender_chat, chat.bot, 'ban_chat_sender_chat') assert check_defaults_handling(chat.ban_sender_chat, chat.bot) monkeypatch.setattr(chat.bot, 'ban_chat_sender_chat', make_assertion) assert chat.ban_sender_chat(42) def test_ban_chat(self, monkeypatch, chat): 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 check_shortcut_call(chat.ban_chat, chat.bot, 'ban_chat_sender_chat') assert check_defaults_handling(chat.ban_chat, chat.bot) monkeypatch.setattr(chat.bot, 'ban_chat_sender_chat', make_assertion) assert chat.ban_chat(42) def test_kick_member_warning(self, chat, monkeypatch, recwarn): 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 monkeypatch.setattr(chat.bot, 'ban_chat_member', make_assertion) assert chat.kick_member(user_id=42, until_date=43) assert len(recwarn) == 1 assert '`Chat.kick_member` is deprecated' in str(recwarn[0].message) @pytest.mark.parametrize('only_if_banned', [True, False, None]) def test_unban_member(self, monkeypatch, chat, only_if_banned): 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') == 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 check_shortcut_call(chat.unban_member, chat.bot, 'unban_chat_member') assert check_defaults_handling(chat.unban_member, chat.bot) monkeypatch.setattr(chat.bot, 'unban_chat_member', make_assertion) assert chat.unban_member(user_id=42, only_if_banned=only_if_banned) def test_unban_sender_chat(self, monkeypatch, chat): 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 check_shortcut_call(chat.unban_sender_chat, chat.bot, 'unban_chat_sender_chat') assert check_defaults_handling(chat.unban_sender_chat, chat.bot) monkeypatch.setattr(chat.bot, 'unban_chat_sender_chat', make_assertion) assert chat.unban_sender_chat(42) def test_unban_chat(self, monkeypatch, chat): 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 check_shortcut_call(chat.unban_chat, chat.bot, 'unban_chat_sender_chat') assert check_defaults_handling(chat.unban_chat, chat.bot) monkeypatch.setattr(chat.bot, 'unban_chat_sender_chat', make_assertion) assert chat.unban_chat(42) @pytest.mark.parametrize('is_anonymous', [True, False, None]) def test_promote_member(self, monkeypatch, chat, is_anonymous): def make_assertion(*_, **kwargs): chat_id = kwargs['chat_id'] == chat.id user_id = kwargs['user_id'] == 42 o_i_b = kwargs.get('is_anonymous') == 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 check_shortcut_call(chat.promote_member, chat.bot, 'promote_chat_member') assert check_defaults_handling(chat.promote_member, chat.bot) monkeypatch.setattr(chat.bot, 'promote_chat_member', make_assertion) assert chat.promote_member(user_id=42, is_anonymous=is_anonymous) def test_restrict_member(self, monkeypatch, chat): permissions = ChatPermissions(True, False, True, False, True, False, True, False) def make_assertion(*_, **kwargs): chat_id = kwargs['chat_id'] == chat.id user_id = kwargs['user_id'] == 42 o_i_b = kwargs.get('permissions') == 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 check_shortcut_call(chat.restrict_member, chat.bot, 'restrict_chat_member') assert check_defaults_handling(chat.restrict_member, chat.bot) monkeypatch.setattr(chat.bot, 'restrict_chat_member', make_assertion) assert chat.restrict_member(user_id=42, permissions=permissions) def test_set_permissions(self, monkeypatch, chat): 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 check_shortcut_call(chat.set_permissions, chat.bot, 'set_chat_permissions') assert check_defaults_handling(chat.set_permissions, chat.bot) monkeypatch.setattr(chat.bot, 'set_chat_permissions', make_assertion) assert chat.set_permissions(permissions=self.permissions) def test_set_administrator_custom_title(self, monkeypatch, chat): 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 monkeypatch.setattr('telegram.Bot.set_chat_administrator_custom_title', make_assertion) assert chat.set_administrator_custom_title(user_id=42, custom_title='custom_title') def test_pin_message(self, monkeypatch, chat): 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 check_shortcut_call(chat.pin_message, chat.bot, 'pin_chat_message') assert check_defaults_handling(chat.pin_message, chat.bot) monkeypatch.setattr(chat.bot, 'pin_chat_message', make_assertion) assert chat.pin_message(message_id=42) def test_unpin_message(self, monkeypatch, chat): def make_assertion(*_, **kwargs): return kwargs['chat_id'] == chat.id assert check_shortcut_signature( Chat.unpin_message, Bot.unpin_chat_message, ['chat_id'], [] ) assert check_shortcut_call(chat.unpin_message, chat.bot, 'unpin_chat_message') assert check_defaults_handling(chat.unpin_message, chat.bot) monkeypatch.setattr(chat.bot, 'unpin_chat_message', make_assertion) assert chat.unpin_message() def test_unpin_all_messages(self, monkeypatch, chat): 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 check_shortcut_call(chat.unpin_all_messages, chat.bot, 'unpin_all_chat_messages') assert check_defaults_handling(chat.unpin_all_messages, chat.bot) monkeypatch.setattr(chat.bot, 'unpin_all_chat_messages', make_assertion) assert chat.unpin_all_messages() def test_instance_method_send_message(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_message, chat.bot, 'send_message') assert check_defaults_handling(chat.send_message, chat.bot) monkeypatch.setattr(chat.bot, 'send_message', make_assertion) assert chat.send_message(text='test') def test_instance_method_send_media_group(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_media_group, chat.bot, 'send_media_group') assert check_defaults_handling(chat.send_media_group, chat.bot) monkeypatch.setattr(chat.bot, 'send_media_group', make_assertion) assert chat.send_media_group(media='test_media_group') def test_instance_method_send_photo(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_photo, chat.bot, 'send_photo') assert check_defaults_handling(chat.send_photo, chat.bot) monkeypatch.setattr(chat.bot, 'send_photo', make_assertion) assert chat.send_photo(photo='test_photo') def test_instance_method_send_contact(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_contact, chat.bot, 'send_contact') assert check_defaults_handling(chat.send_contact, chat.bot) monkeypatch.setattr(chat.bot, 'send_contact', make_assertion) assert chat.send_contact(phone_number='test_contact') def test_instance_method_send_audio(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_audio, chat.bot, 'send_audio') assert check_defaults_handling(chat.send_audio, chat.bot) monkeypatch.setattr(chat.bot, 'send_audio', make_assertion) assert chat.send_audio(audio='test_audio') def test_instance_method_send_document(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_document, chat.bot, 'send_document') assert check_defaults_handling(chat.send_document, chat.bot) monkeypatch.setattr(chat.bot, 'send_document', make_assertion) assert chat.send_document(document='test_document') def test_instance_method_send_dice(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_dice, chat.bot, 'send_dice') assert check_defaults_handling(chat.send_dice, chat.bot) monkeypatch.setattr(chat.bot, 'send_dice', make_assertion) assert chat.send_dice(emoji='test_dice') def test_instance_method_send_game(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_game, chat.bot, 'send_game') assert check_defaults_handling(chat.send_game, chat.bot) monkeypatch.setattr(chat.bot, 'send_game', make_assertion) assert chat.send_game(game_short_name='test_game') def test_instance_method_send_invoice(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_invoice, chat.bot, 'send_invoice') assert check_defaults_handling(chat.send_invoice, chat.bot) monkeypatch.setattr(chat.bot, 'send_invoice', make_assertion) assert chat.send_invoice( 'title', 'description', 'payload', 'provider_token', 'currency', 'prices', ) def test_instance_method_send_location(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_location, chat.bot, 'send_location') assert check_defaults_handling(chat.send_location, chat.bot) monkeypatch.setattr(chat.bot, 'send_location', make_assertion) assert chat.send_location(latitude='test_location') def test_instance_method_send_sticker(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_sticker, chat.bot, 'send_sticker') assert check_defaults_handling(chat.send_sticker, chat.bot) monkeypatch.setattr(chat.bot, 'send_sticker', make_assertion) assert chat.send_sticker(sticker='test_sticker') def test_instance_method_send_venue(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_venue, chat.bot, 'send_venue') assert check_defaults_handling(chat.send_venue, chat.bot) monkeypatch.setattr(chat.bot, 'send_venue', make_assertion) assert chat.send_venue(title='test_venue') def test_instance_method_send_video(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_video, chat.bot, 'send_video') assert check_defaults_handling(chat.send_video, chat.bot) monkeypatch.setattr(chat.bot, 'send_video', make_assertion) assert chat.send_video(video='test_video') def test_instance_method_send_video_note(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_video_note, chat.bot, 'send_video_note') assert check_defaults_handling(chat.send_video_note, chat.bot) monkeypatch.setattr(chat.bot, 'send_video_note', make_assertion) assert chat.send_video_note(video_note='test_video_note') def test_instance_method_send_voice(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_voice, chat.bot, 'send_voice') assert check_defaults_handling(chat.send_voice, chat.bot) monkeypatch.setattr(chat.bot, 'send_voice', make_assertion) assert chat.send_voice(voice='test_voice') def test_instance_method_send_animation(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_animation, chat.bot, 'send_animation') assert check_defaults_handling(chat.send_animation, chat.bot) monkeypatch.setattr(chat.bot, 'send_animation', make_assertion) assert chat.send_animation(animation='test_animation') def test_instance_method_send_poll(self, monkeypatch, chat): 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 check_shortcut_call(chat.send_poll, chat.bot, 'send_poll') assert check_defaults_handling(chat.send_poll, chat.bot) monkeypatch.setattr(chat.bot, 'send_poll', make_assertion) assert chat.send_poll(question='test_poll', options=[1, 2]) def test_instance_method_send_copy(self, monkeypatch, chat): 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 check_shortcut_call(chat.copy_message, chat.bot, 'copy_message') assert check_defaults_handling(chat.copy_message, chat.bot) monkeypatch.setattr(chat.bot, 'copy_message', make_assertion) assert chat.send_copy(from_chat_id='test_copy', message_id=42) def test_instance_method_copy_message(self, monkeypatch, chat): 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 check_shortcut_call(chat.copy_message, chat.bot, 'copy_message') assert check_defaults_handling(chat.copy_message, chat.bot) monkeypatch.setattr(chat.bot, 'copy_message', make_assertion) assert chat.copy_message(chat_id='test_copy', message_id=42) def test_export_invite_link(self, monkeypatch, chat): 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 check_shortcut_call(chat.export_invite_link, chat.bot, 'export_chat_invite_link') assert check_defaults_handling(chat.export_invite_link, chat.bot) monkeypatch.setattr(chat.bot, 'export_chat_invite_link', make_assertion) assert chat.export_invite_link() def test_create_invite_link(self, monkeypatch, chat): 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 check_shortcut_call(chat.create_invite_link, chat.bot, 'create_chat_invite_link') assert check_defaults_handling(chat.create_invite_link, chat.bot) monkeypatch.setattr(chat.bot, 'create_chat_invite_link', make_assertion) assert chat.create_invite_link() def test_edit_invite_link(self, monkeypatch, chat): link = "ThisIsALink" 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 check_shortcut_call(chat.edit_invite_link, chat.bot, 'edit_chat_invite_link') assert check_defaults_handling(chat.edit_invite_link, chat.bot) monkeypatch.setattr(chat.bot, 'edit_chat_invite_link', make_assertion) assert chat.edit_invite_link(invite_link=link) def test_revoke_invite_link(self, monkeypatch, chat): link = "ThisIsALink" 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 check_shortcut_call(chat.revoke_invite_link, chat.bot, 'revoke_chat_invite_link') assert check_defaults_handling(chat.revoke_invite_link, chat.bot) monkeypatch.setattr(chat.bot, 'revoke_chat_invite_link', make_assertion) assert chat.revoke_invite_link(invite_link=link) def test_approve_join_request(self, monkeypatch, chat): 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 check_shortcut_call( chat.approve_join_request, chat.bot, 'approve_chat_join_request' ) assert check_defaults_handling(chat.approve_join_request, chat.bot) monkeypatch.setattr(chat.bot, 'approve_chat_join_request', make_assertion) assert chat.approve_join_request(user_id=42) def test_decline_join_request(self, monkeypatch, chat): 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 check_shortcut_call( chat.decline_join_request, chat.bot, 'decline_chat_join_request' ) assert check_defaults_handling(chat.decline_join_request, chat.bot) monkeypatch.setattr(chat.bot, 'decline_chat_join_request', make_assertion) assert chat.decline_join_request(user_id=42) 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) python-telegram-bot-13.11/tests/test_chataction.py000066400000000000000000000024371417656324400223320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ChatAction def test_slot_behaviour(recwarn, mro_slots): action = ChatAction() for attr in action.__slots__: assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not action.__dict__, f"got missing slot(s): {action.__dict__}" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" action.custom = 'should give warning' assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list python-telegram-bot-13.11/tests/test_chatinvitelink.py000066400000000000000000000122021417656324400232200ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 User, ChatInviteLink from telegram.utils.helpers import to_timestamp @pytest.fixture(scope='class') def creator(): return User(1, 'First name', False) @pytest.fixture(scope='class') def invite_link(creator): return ChatInviteLink( TestChatInviteLink.link, creator, TestChatInviteLink.primary, TestChatInviteLink.revoked, expire_date=TestChatInviteLink.expire_date, member_limit=TestChatInviteLink.member_limit, name=TestChatInviteLink.name, pending_join_request_count=TestChatInviteLink.pending_join_request_count, ) class TestChatInviteLink: link = "thisialink" primary = True revoked = False expire_date = datetime.datetime.utcnow() member_limit = 42 name = 'LinkName' pending_join_request_count = 42 def test_slot_behaviour(self, recwarn, mro_slots, invite_link): for attr in invite_link.__slots__: assert getattr(invite_link, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not invite_link.__dict__, f"got missing slot(s): {invite_link.__dict__}" assert len(mro_slots(invite_link)) == len(set(mro_slots(invite_link))), "duplicate slot" invite_link.custom = 'should give warning' assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json_required_args(self, bot, creator): json_dict = { 'invite_link': self.link, 'creator': creator.to_dict(), 'is_primary': self.primary, 'is_revoked': self.revoked, } invite_link = ChatInviteLink.de_json(json_dict, bot) assert invite_link.invite_link == self.link assert invite_link.creator == creator 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(), 'is_primary': self.primary, 'is_revoked': self.revoked, 'expire_date': to_timestamp(self.expire_date), 'member_limit': str(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.invite_link == self.link assert invite_link.creator == creator assert invite_link.is_primary == self.primary assert invite_link.is_revoked == self.revoked assert pytest.approx(invite_link.expire_date == self.expire_date) 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_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['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) b = ChatInviteLink("link", User(1, '', False), True, True) d = ChatInviteLink("link", User(2, '', False), False, True) d2 = ChatInviteLink("notalink", User(1, '', False), False, True) d3 = ChatInviteLink("notalink", User(1, '', False), True, True) e = User(1, '', False) 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 d2 != d3 assert hash(d2) != hash(d3) assert a != e assert hash(a) != hash(e) python-telegram-bot-13.11/tests/test_chatjoinrequest.py000066400000000000000000000142411417656324400234210ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 import pytz from telegram import ChatJoinRequest, User, Chat, ChatInviteLink, Bot from telegram.utils.helpers import to_timestamp from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling @pytest.fixture(scope='class') def time(): return datetime.datetime.now(tz=pytz.utc) @pytest.fixture(scope='class') def chat_join_request(bot, time): return ChatJoinRequest( chat=TestChatJoinRequest.chat, from_user=TestChatJoinRequest.from_user, date=time, bio=TestChatJoinRequest.bio, invite_link=TestChatJoinRequest.invite_link, bot=bot, ) class TestChatJoinRequest: chat = Chat(1, Chat.SUPERGROUP) from_user = User(2, 'first_name', False) bio = 'bio' invite_link = ChatInviteLink( 'https://invite.link', User(42, 'creator', False), name='InviteLink', is_revoked=False, is_primary=False, ) def test_slot_behaviour(self, chat_join_request, recwarn, mro_slots): inst = chat_join_request for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.bio = 'should give warning', self.bio assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self, bot, time): json_dict = { 'chat': self.chat.to_dict(), 'from': self.from_user.to_dict(), 'date': to_timestamp(time), } chat_join_request = ChatJoinRequest.de_json(json_dict, bot) assert chat_join_request.chat == self.chat assert chat_join_request.from_user == self.from_user assert pytest.approx(chat_join_request.date == time) assert to_timestamp(chat_join_request.date) == to_timestamp(time) 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.chat == self.chat assert chat_join_request.from_user == self.from_user assert pytest.approx(chat_join_request.date == time) assert to_timestamp(chat_join_request.date) == to_timestamp(time) assert chat_join_request.bio == self.bio assert chat_join_request.invite_link == self.invite_link 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() def test_equality(self, chat_join_request, time): a = chat_join_request b = ChatJoinRequest(self.chat, self.from_user, time) c = ChatJoinRequest(self.chat, self.from_user, time, bio='bio') d = ChatJoinRequest(self.chat, self.from_user, time + datetime.timedelta(1)) e = ChatJoinRequest(self.chat, User(-1, 'last_name', True), time) 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) def test_approve(self, monkeypatch, chat_join_request): 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 check_shortcut_call( chat_join_request.approve, chat_join_request.bot, 'approve_chat_join_request' ) assert check_defaults_handling(chat_join_request.approve, chat_join_request.bot) monkeypatch.setattr(chat_join_request.bot, 'approve_chat_join_request', make_assertion) assert chat_join_request.approve() def test_decline(self, monkeypatch, chat_join_request): 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 check_shortcut_call( chat_join_request.decline, chat_join_request.bot, 'decline_chat_join_request' ) assert check_defaults_handling(chat_join_request.decline, chat_join_request.bot) monkeypatch.setattr(chat_join_request.bot, 'decline_chat_join_request', make_assertion) assert chat_join_request.decline() python-telegram-bot-13.11/tests/test_chatjoinrequesthandler.py000066400000000000000000000163601417656324400247630ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest import pytz from telegram import ( Update, Bot, Message, User, Chat, CallbackQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, ChatJoinRequest, ChatInviteLink, ) from telegram.ext import CallbackContext, JobQueue, ChatJoinRequestHandler 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=pytz.utc) @pytest.fixture(scope='class') def chat_join_request(time, bot): return ChatJoinRequest( chat=Chat(1, Chat.SUPERGROUP), from_user=User(2, 'first_name', False), date=time, bio='bio', invite_link=ChatInviteLink( 'https://invite.link', User(42, 'creator', False), name='InviteLink', is_revoked=False, is_primary=False, ), bot=bot, ) @pytest.fixture(scope='function') 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, recwarn, mro_slots): action = ChatJoinRequestHandler(self.callback_basic) for attr in action.__slots__: assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not action.__dict__, f"got missing slot(s): {action.__dict__}" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" action.custom = 'should give warning' assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(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_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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_basic(self, dp, chat_join_request_update): handler = ChatJoinRequestHandler(self.callback_basic) dp.add_handler(handler) assert handler.check_update(chat_join_request_update) dp.process_update(chat_join_request_update) assert self.test_flag def test_pass_user_or_chat_data(self, dp, chat_join_request_update): handler = ChatJoinRequestHandler(self.callback_data_1, pass_user_data=True) dp.add_handler(handler) dp.process_update(chat_join_request_update) assert self.test_flag dp.remove_handler(handler) handler = ChatJoinRequestHandler(self.callback_data_1, pass_chat_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(chat_join_request_update) assert self.test_flag dp.remove_handler(handler) handler = ChatJoinRequestHandler( self.callback_data_2, pass_chat_data=True, pass_user_data=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(chat_join_request_update) assert self.test_flag def test_pass_job_or_update_queue(self, dp, chat_join_request_update): handler = ChatJoinRequestHandler(self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update(chat_join_request_update) assert self.test_flag dp.remove_handler(handler) handler = ChatJoinRequestHandler(self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(chat_join_request_update) assert self.test_flag dp.remove_handler(handler) handler = ChatJoinRequestHandler( self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(chat_join_request_update) assert self.test_flag def test_other_update_types(self, false_update): handler = ChatJoinRequestHandler(self.callback_basic) assert not handler.check_update(false_update) assert not handler.check_update(True) def test_context(self, cdp, chat_join_request_update): handler = ChatJoinRequestHandler(callback=self.callback_context) cdp.add_handler(handler) cdp.process_update(chat_join_request_update) assert self.test_flag python-telegram-bot-13.11/tests/test_chatlocation.py000066400000000000000000000053271417656324400226660ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 Location, ChatLocation, User @pytest.fixture(scope='class') def chat_location(bot): return ChatLocation(TestChatLocation.location, TestChatLocation.address) class TestChatLocation: location = Location(123, 456) address = 'The Shire' def test_slot_behaviour(self, chat_location, recwarn, mro_slots): inst = chat_location for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.address = 'should give warning', self.address assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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-13.11/tests/test_chatmember.py000066400000000000000000000246071417656324400223270ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import datetime from copy import deepcopy import pytest from telegram.utils.helpers import to_timestamp from telegram import ( User, ChatMember, ChatMemberOwner, ChatMemberAdministrator, ChatMemberMember, ChatMemberRestricted, ChatMemberLeft, ChatMemberBanned, Dice, ) @pytest.fixture(scope='class') def user(): return User(1, 'First name', False) @pytest.fixture( scope="class", params=[ (ChatMemberOwner, ChatMember.CREATOR), (ChatMemberAdministrator, ChatMember.ADMINISTRATOR), (ChatMemberMember, ChatMember.MEMBER), (ChatMemberRestricted, ChatMember.RESTRICTED), (ChatMemberLeft, ChatMember.LEFT), (ChatMemberBanned, ChatMember.KICKED), ], ids=[ ChatMember.CREATOR, ChatMember.ADMINISTRATOR, ChatMember.MEMBER, ChatMember.RESTRICTED, ChatMember.LEFT, ChatMember.KICKED, ], ) def chat_member_class_and_status(request): return request.param @pytest.fixture(scope='class') def chat_member_types(chat_member_class_and_status, user): return chat_member_class_and_status[0](status=chat_member_class_and_status[1], user=user) class TestChatMember: def test_slot_behaviour(self, chat_member_types, mro_slots, recwarn): for attr in chat_member_types.__slots__: assert getattr(chat_member_types, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not chat_member_types.__dict__, f"got missing slot(s): {chat_member_types.__dict__}" assert len(mro_slots(chat_member_types)) == len( set(mro_slots(chat_member_types)) ), "duplicate slot" chat_member_types.custom, chat_member_types.status = 'warning!', chat_member_types.status assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json_required_args(self, bot, chat_member_class_and_status, user): cls = chat_member_class_and_status[0] status = chat_member_class_and_status[1] assert cls.de_json({}, bot) is None json_dict = {'status': status, 'user': user.to_dict()} chat_member_type = ChatMember.de_json(json_dict, bot) assert isinstance(chat_member_type, ChatMember) assert isinstance(chat_member_type, cls) assert chat_member_type.status == status assert chat_member_type.user == user def test_de_json_all_args(self, bot, chat_member_class_and_status, user): cls = chat_member_class_and_status[0] status = chat_member_class_and_status[1] time = datetime.datetime.utcnow() json_dict = { 'user': user.to_dict(), 'status': status, 'custom_title': 'PTB', 'is_anonymous': True, 'until_date': to_timestamp(time), 'can_be_edited': False, 'can_change_info': True, 'can_post_messages': False, 'can_edit_messages': True, 'can_delete_messages': True, 'can_invite_users': False, 'can_restrict_members': True, 'can_pin_messages': False, 'can_promote_members': True, 'can_send_messages': False, 'can_send_media_messages': True, 'can_send_polls': False, 'can_send_other_messages': True, 'can_add_web_page_previews': False, 'can_manage_chat': True, 'can_manage_voice_chats': True, } chat_member_type = ChatMember.de_json(json_dict, bot) assert isinstance(chat_member_type, ChatMember) assert isinstance(chat_member_type, cls) assert chat_member_type.user == user assert chat_member_type.status == status if chat_member_type.custom_title is not None: assert chat_member_type.custom_title == 'PTB' assert type(chat_member_type) in {ChatMemberOwner, ChatMemberAdministrator} if chat_member_type.is_anonymous is not None: assert chat_member_type.is_anonymous is True assert type(chat_member_type) in {ChatMemberOwner, ChatMemberAdministrator} if chat_member_type.until_date is not None: assert type(chat_member_type) in {ChatMemberBanned, ChatMemberRestricted} if chat_member_type.can_be_edited is not None: assert chat_member_type.can_be_edited is False assert type(chat_member_type) == ChatMemberAdministrator if chat_member_type.can_change_info is not None: assert chat_member_type.can_change_info is True assert type(chat_member_type) in {ChatMemberAdministrator, ChatMemberRestricted} if chat_member_type.can_post_messages is not None: assert chat_member_type.can_post_messages is False assert type(chat_member_type) == ChatMemberAdministrator if chat_member_type.can_edit_messages is not None: assert chat_member_type.can_edit_messages is True assert type(chat_member_type) == ChatMemberAdministrator if chat_member_type.can_delete_messages is not None: assert chat_member_type.can_delete_messages is True assert type(chat_member_type) == ChatMemberAdministrator if chat_member_type.can_invite_users is not None: assert chat_member_type.can_invite_users is False assert type(chat_member_type) in {ChatMemberAdministrator, ChatMemberRestricted} if chat_member_type.can_restrict_members is not None: assert chat_member_type.can_restrict_members is True assert type(chat_member_type) == ChatMemberAdministrator if chat_member_type.can_pin_messages is not None: assert chat_member_type.can_pin_messages is False assert type(chat_member_type) in {ChatMemberAdministrator, ChatMemberRestricted} if chat_member_type.can_promote_members is not None: assert chat_member_type.can_promote_members is True assert type(chat_member_type) == ChatMemberAdministrator if chat_member_type.can_send_messages is not None: assert chat_member_type.can_send_messages is False assert type(chat_member_type) == ChatMemberRestricted if chat_member_type.can_send_media_messages is not None: assert chat_member_type.can_send_media_messages is True assert type(chat_member_type) == ChatMemberRestricted if chat_member_type.can_send_polls is not None: assert chat_member_type.can_send_polls is False assert type(chat_member_type) == ChatMemberRestricted if chat_member_type.can_send_other_messages is not None: assert chat_member_type.can_send_other_messages is True assert type(chat_member_type) == ChatMemberRestricted if chat_member_type.can_add_web_page_previews is not None: assert chat_member_type.can_add_web_page_previews is False assert type(chat_member_type) == ChatMemberRestricted if chat_member_type.can_manage_chat is not None: assert chat_member_type.can_manage_chat is True assert type(chat_member_type) == ChatMemberAdministrator if chat_member_type.can_manage_voice_chats is not None: assert chat_member_type.can_manage_voice_chats is True assert type(chat_member_type) == ChatMemberAdministrator def test_de_json_invalid_status(self, bot, user): json_dict = {'status': 'invalid', 'user': 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_class_and_status, bot, chat_id, user): """This makes sure that e.g. ChatMemberAdministrator(data, bot) never returns a ChatMemberKicked instance.""" cls = chat_member_class_and_status[0] time = datetime.datetime.utcnow() json_dict = { 'user': user.to_dict(), 'status': 'status', 'custom_title': 'PTB', 'is_anonymous': True, 'until_date': to_timestamp(time), 'can_be_edited': False, 'can_change_info': True, 'can_post_messages': False, 'can_edit_messages': True, 'can_delete_messages': True, 'can_invite_users': False, 'can_restrict_members': True, 'can_pin_messages': False, 'can_promote_members': True, 'can_send_messages': False, 'can_send_media_messages': True, 'can_send_polls': False, 'can_send_other_messages': True, 'can_add_web_page_previews': False, 'can_manage_chat': True, 'can_manage_voice_chats': True, } assert type(cls.de_json(json_dict, bot)) is cls def test_to_dict(self, chat_member_types, user): chat_member_dict = chat_member_types.to_dict() assert isinstance(chat_member_dict, dict) assert chat_member_dict['status'] == chat_member_types.status assert chat_member_dict['user'] == user.to_dict() def test_equality(self, chat_member_types, user): a = ChatMember(status='status', user=user) b = ChatMember(status='status', user=user) c = chat_member_types d = deepcopy(chat_member_types) 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-13.11/tests/test_chatmemberhandler.py000066400000000000000000000174331417656324400236640ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram import ( Update, Bot, Message, User, Chat, CallbackQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, ChatMemberUpdated, ChatMember, ) from telegram.ext import CallbackContext, JobQueue, ChatMemberHandler from telegram.utils.helpers import from_timestamp 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.CREATOR), ChatMember(User(1, '', False), ChatMember.CREATOR), ) @pytest.fixture(scope='function') def chat_member(bot, chat_member_updated): return Update(0, my_chat_member=chat_member_updated) class TestChatMemberHandler: test_flag = False def test_slot_behaviour(self, recwarn, mro_slots): action = ChatMemberHandler(self.callback_basic) for attr in action.__slots__: assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not action.__dict__, f"got missing slot(s): {action.__dict__}" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" action.custom = 'should give warning' assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(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_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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) ) def test_basic(self, dp, chat_member): handler = ChatMemberHandler(self.callback_basic) dp.add_handler(handler) assert handler.check_update(chat_member) dp.process_update(chat_member) assert self.test_flag @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'], ) def test_chat_member_types( self, dp, chat_member_updated, chat_member, expected, allowed_types ): result_1, result_2 = expected handler = ChatMemberHandler(self.callback_basic, chat_member_types=allowed_types) dp.add_handler(handler) assert handler.check_update(chat_member) == result_1 dp.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 dp.process_update(chat_member) assert self.test_flag == result_2 def test_pass_user_or_chat_data(self, dp, chat_member): handler = ChatMemberHandler(self.callback_data_1, pass_user_data=True) dp.add_handler(handler) dp.process_update(chat_member) assert self.test_flag dp.remove_handler(handler) handler = ChatMemberHandler(self.callback_data_1, pass_chat_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(chat_member) assert self.test_flag dp.remove_handler(handler) handler = ChatMemberHandler(self.callback_data_2, pass_chat_data=True, pass_user_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(chat_member) assert self.test_flag def test_pass_job_or_update_queue(self, dp, chat_member): handler = ChatMemberHandler(self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update(chat_member) assert self.test_flag dp.remove_handler(handler) handler = ChatMemberHandler(self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(chat_member) assert self.test_flag dp.remove_handler(handler) handler = ChatMemberHandler( self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(chat_member) assert self.test_flag def test_other_update_types(self, false_update): handler = ChatMemberHandler(self.callback_basic) assert not handler.check_update(false_update) assert not handler.check_update(True) def test_context(self, cdp, chat_member): handler = ChatMemberHandler(self.callback_context) cdp.add_handler(handler) cdp.process_update(chat_member) assert self.test_flag python-telegram-bot-13.11/tests/test_chatmemberupdated.py000066400000000000000000000206461417656324400236750ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 import pytz from telegram import User, ChatMember, Chat, ChatMemberUpdated, ChatInviteLink from telegram.utils.helpers import to_timestamp @pytest.fixture(scope='class') def user(): return User(1, 'First name', False) @pytest.fixture(scope='class') def chat(): return Chat(1, Chat.SUPERGROUP, 'Chat') @pytest.fixture(scope='class') def old_chat_member(user): return ChatMember(user, TestChatMemberUpdated.old_status) @pytest.fixture(scope='class') def new_chat_member(user): return ChatMember(user, TestChatMemberUpdated.new_status) @pytest.fixture(scope='class') def time(): return datetime.datetime.now(tz=pytz.utc) @pytest.fixture(scope='class') def invite_link(user): return ChatInviteLink('link', user, True, True) @pytest.fixture(scope='class') 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) class TestChatMemberUpdated: old_status = ChatMember.MEMBER new_status = ChatMember.ADMINISTRATOR def test_slot_behaviour(self, recwarn, mro_slots, chat_member_updated): action = chat_member_updated for attr in action.__slots__: assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not action.__dict__, f"got missing slot(s): {action.__dict__}" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" action.custom = 'should give warning' assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.chat == chat assert chat_member_updated.from_user == user assert pytest.approx(chat_member_updated.date == time) 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 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(), } chat_member_updated = ChatMemberUpdated.de_json(json_dict, bot) assert chat_member_updated.chat == chat assert chat_member_updated.from_user == user assert pytest.approx(chat_member_updated.date == time) 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 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() 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.CREATOR), new_chat_member, ) # wrong new_chat_member f = ChatMemberUpdated( Chat(1, 'chat'), User(1, '', False), time, old_chat_member, ChatMember(User(1, '', False), ChatMember.CREATOR), ) # wrong type g = ChatMember(User(1, '', False), ChatMember.CREATOR) 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 comparision doesn't # just happens by id/required args new_user = User(1, 'First name', False, last_name='last name') new_chat_member.user = new_user 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 [ name for name, param in inspect.signature(ChatMember).parameters.items() if name != 'self' and param.default != inspect.Parameter.empty ], ) def test_difference_optionals(self, optional_attribute, user, chat): # we use datetimes here, because we need that for `until_date` and it doesn't matter for # the other attributes old_value = datetime.datetime(2020, 1, 1) new_value = datetime.datetime(2021, 1, 1) old_chat_member = ChatMember(user, 'status', **{optional_attribute: old_value}) new_chat_member = ChatMember(user, 'status', **{optional_attribute: new_value}) 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)} python-telegram-bot-13.11/tests/test_chatpermissions.py000066400000000000000000000117471417656324400234340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope="class") def chat_permissions(): return ChatPermissions( can_send_messages=True, can_send_media_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, ) class TestChatPermissions: can_send_messages = True can_send_media_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 def test_slot_behaviour(self, chat_permissions, recwarn, mro_slots): inst = chat_permissions for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.can_send_polls = 'should give warning', self.can_send_polls assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self, bot): json_dict = { 'can_send_messages': self.can_send_messages, 'can_send_media_messages': self.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, } permissions = ChatPermissions.de_json(json_dict, bot) assert permissions.can_send_messages == self.can_send_messages assert permissions.can_send_media_messages == self.can_send_media_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 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_media_messages'] == chat_permissions.can_send_media_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 def test_equality(self): a = ChatPermissions( can_send_messages=True, can_send_media_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, can_send_media_messages=True, ) c = ChatPermissions( can_send_messages=False, can_send_media_messages=True, can_send_polls=True, can_send_other_messages=False, ) d = User(123, '', 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) python-telegram-bot-13.11/tests/test_chatphoto.py000066400000000000000000000164661417656324400222150ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import ChatPhoto, Voice, TelegramError, Bot from tests.conftest import ( expect_bad_request, check_shortcut_call, check_shortcut_signature, check_defaults_handling, ) @pytest.fixture(scope='function') def chatphoto_file(): f = open('tests/data/telegram.jpg', 'rb') yield f f.close() @pytest.fixture(scope='function') def chat_photo(bot, super_group_id): def func(): return bot.get_chat(super_group_id, timeout=50).photo return expect_bad_request(func, 'Type of file mismatch', 'Telegram did not accept the file.') class TestChatPhoto: 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' def test_slot_behaviour(self, chat_photo, recwarn, mro_slots): for attr in chat_photo.__slots__: assert getattr(chat_photo, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not chat_photo.__dict__, f"got missing slot(s): {chat_photo.__dict__}" assert len(mro_slots(chat_photo)) == len(set(mro_slots(chat_photo))), "duplicate slot" chat_photo.custom, chat_photo.big_file_id = 'gives warning', self.chatphoto_big_file_id assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @flaky(3, 1) def test_send_all_args(self, bot, super_group_id, chatphoto_file, chat_photo, thumb_file): def func(): assert bot.set_chat_photo(super_group_id, chatphoto_file) expect_bad_request(func, 'Type of file mismatch', 'Telegram did not accept the file.') @flaky(3, 1) def test_get_and_download(self, bot, chat_photo): new_file = bot.get_file(chat_photo.small_file_id) assert new_file.file_id == chat_photo.small_file_id assert new_file.file_path.startswith('https://') new_file.download('telegram.jpg') assert os.path.isfile('telegram.jpg') new_file = bot.get_file(chat_photo.big_file_id) assert new_file.file_id == chat_photo.big_file_id assert new_file.file_path.startswith('https://') new_file.download('telegram.jpg') assert os.path.isfile('telegram.jpg') def test_send_with_chat_photo(self, monkeypatch, bot, super_group_id, chat_photo): def test(url, data, **kwargs): return data['photo'] == chat_photo monkeypatch.setattr(bot.request, 'post', test) message = bot.set_chat_photo(photo=chat_photo, chat_id=super_group_id) assert message 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.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 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 @flaky(3, 1) def test_error_send_empty_file(self, bot, super_group_id): chatphoto_file = open(os.devnull, 'rb') with pytest.raises(TelegramError): bot.set_chat_photo(chat_id=super_group_id, photo=chatphoto_file) @flaky(3, 1) def test_error_send_empty_file_id(self, bot, super_group_id): with pytest.raises(TelegramError): bot.set_chat_photo(chat_id=super_group_id, photo='') def test_error_send_without_required_args(self, bot, super_group_id): with pytest.raises(TypeError): bot.set_chat_photo(chat_id=super_group_id) def test_get_small_file_instance_method(self, monkeypatch, chat_photo): 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 check_shortcut_call(chat_photo.get_small_file, chat_photo.bot, 'get_file') assert check_defaults_handling(chat_photo.get_small_file, chat_photo.bot) monkeypatch.setattr(chat_photo.bot, 'get_file', make_assertion) assert chat_photo.get_small_file() def test_get_big_file_instance_method(self, monkeypatch, chat_photo): 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 check_shortcut_call(chat_photo.get_big_file, chat_photo.bot, 'get_file') assert check_defaults_handling(chat_photo.get_big_file, chat_photo.bot) monkeypatch.setattr(chat_photo.bot, 'get_file', make_assertion) assert chat_photo.get_big_file() 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) python-telegram-bot-13.11/tests/test_choseninlineresult.py000066400000000000000000000072341417656324400241320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, ChosenInlineResult, Location, Voice @pytest.fixture(scope='class') def user(): return User(1, 'First name', False) @pytest.fixture(scope='class') def chosen_inline_result(user): return ChosenInlineResult(TestChosenInlineResult.result_id, user, TestChosenInlineResult.query) class TestChosenInlineResult: result_id = 'result id' query = 'query text' def test_slot_behaviour(self, chosen_inline_result, recwarn, mro_slots): inst = chosen_inline_result for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.result_id = 'should give warning', self.result_id assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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.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-13.11/tests/test_choseninlineresulthandler.py000066400000000000000000000175551417656324400254770ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram import ( Update, Chat, Bot, ChosenInlineResult, User, Message, CallbackQuery, InlineQuery, ShippingQuery, PreCheckoutQuery, ) from telegram.ext import ChosenInlineResultHandler, CallbackContext, JobQueue 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(): return Update( 1, chosen_inline_result=ChosenInlineResult('result_id', User(1, 'test_user', False), 'query'), ) class TestChosenInlineResultHandler: test_flag = False @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def test_slot_behaviour(self, recwarn, mro_slots): handler = ChosenInlineResultHandler(self.callback_basic) for attr in handler.__slots__: assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" handler.custom, handler.callback = 'should give warning', self.callback_basic assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def callback_basic(self, bot, update): test_bot = isinstance(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_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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_context_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_basic(self, dp, chosen_inline_result): handler = ChosenInlineResultHandler(self.callback_basic) dp.add_handler(handler) assert handler.check_update(chosen_inline_result) dp.process_update(chosen_inline_result) assert self.test_flag def test_pass_user_or_chat_data(self, dp, chosen_inline_result): handler = ChosenInlineResultHandler(self.callback_data_1, pass_user_data=True) dp.add_handler(handler) dp.process_update(chosen_inline_result) assert self.test_flag dp.remove_handler(handler) handler = ChosenInlineResultHandler(self.callback_data_1, pass_chat_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(chosen_inline_result) assert self.test_flag dp.remove_handler(handler) handler = ChosenInlineResultHandler( self.callback_data_2, pass_chat_data=True, pass_user_data=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(chosen_inline_result) assert self.test_flag def test_pass_job_or_update_queue(self, dp, chosen_inline_result): handler = ChosenInlineResultHandler(self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update(chosen_inline_result) assert self.test_flag dp.remove_handler(handler) handler = ChosenInlineResultHandler(self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(chosen_inline_result) assert self.test_flag dp.remove_handler(handler) handler = ChosenInlineResultHandler( self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(chosen_inline_result) assert self.test_flag def test_other_update_types(self, false_update): handler = ChosenInlineResultHandler(self.callback_basic) assert not handler.check_update(false_update) def test_context(self, cdp, chosen_inline_result): handler = ChosenInlineResultHandler(self.callback_context) cdp.add_handler(handler) cdp.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' def test_context_pattern(self, cdp, chosen_inline_result): handler = ChosenInlineResultHandler( self.callback_context_pattern, pattern=r'(?P.*)ult(?P.*)' ) cdp.add_handler(handler) cdp.process_update(chosen_inline_result) assert self.test_flag cdp.remove_handler(handler) handler = ChosenInlineResultHandler(self.callback_context_pattern, pattern=r'(res)ult(.*)') cdp.add_handler(handler) cdp.process_update(chosen_inline_result) assert self.test_flag python-telegram-bot-13.11/tests/test_commandhandler.py000066400000000000000000000467121417656324400231750ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest import itertools from telegram.utils.deprecate import TelegramDeprecationWarning from telegram import Message, Update, Chat, Bot from telegram.ext import CommandHandler, Filters, CallbackContext, JobQueue, PrefixHandler from tests.conftest import ( make_command_message, make_command_update, make_message, make_message_update, ) 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 PASS_KEYWORDS = ('pass_user_data', 'pass_chat_data', 'pass_job_queue', 'pass_update_queue') @pytest.fixture(scope='module', params=itertools.combinations(PASS_KEYWORDS, 2)) def pass_combination(self, request): return {key: True for key in request.param} def response(self, dispatcher, 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 dispatcher.process_update(update) return self.test_flag def callback_basic(self, bot, update): test_bot = isinstance(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 def callback_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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_context_args(self, update, context): self.test_flag = context.args == ['one', 'two'] def callback_context_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_context_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_context_args_or_regex(self, cdp, handler, text): cdp.add_handler(handler) update = make_command_update(text) assert not self.response(cdp, update) update.message.text += ' one two' assert self.response(cdp, 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, recwarn, mro_slots): handler = self.make_default_handler() for attr in handler.__slots__: assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" handler.custom, handler.command = 'should give warning', self.CMD assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(scope='class') def command(self): return self.CMD @pytest.fixture(scope='class') def command_message(self, command): return make_command_message(command) @pytest.fixture(scope='class') def command_update(self, command_message): return make_command_update(command_message) def ch_callback_args(self, bot, update, args): if update.message.text == self.CMD: self.test_flag = len(args) == 0 elif update.message.text == f'{self.CMD}@{bot.username}': self.test_flag = len(args) == 0 else: self.test_flag = args == ['one', 'two'] def make_default_handler(self, callback=None, **kwargs): callback = callback or self.callback_basic return CommandHandler(self.CMD[1:], callback, **kwargs) def test_basic(self, dp, command): """Test whether a command handler responds to its command and not to others, or badly formatted commands""" handler = self.make_default_handler() dp.add_handler(handler) assert self.response(dp, make_command_update(command)) assert not is_match(handler, make_command_update(command[1:])) assert not is_match(handler, make_command_update(f'/not{command[1:]}')) assert not is_match(handler, make_command_update(f'not {command} at start')) @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='not a valid bot command'): CommandHandler(cmd, self.callback_basic) def test_command_list(self): """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')) assert is_match(handler, make_command_update('/star')) assert not is_match(handler, make_command_update('/stop')) def test_deprecation_warning(self): """``allow_edited`` deprecated in favor of filters""" with pytest.warns(TelegramDeprecationWarning, match='See https://git.io/fxJuV'): self.make_default_handler(allow_edited=True) def test_edited(self, command_message): """Test that a CH responds to an edited message iff its filters allow it""" handler_edited = self.make_default_handler() handler_no_edited = self.make_default_handler(filters=~Filters.update.edited_message) self._test_edited(command_message, handler_edited, handler_no_edited) def test_edited_deprecated(self, command_message): """Test that a CH responds to an edited message iff ``allow_edited`` is True""" handler_edited = self.make_default_handler(allow_edited=True) handler_no_edited = self.make_default_handler(allow_edited=False) 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): """Test that a CH with a (generic) filter responds iff its filters match""" handler = self.make_default_handler(filters=Filters.group) assert is_match(handler, make_command_update(command, chat=Chat(-23, Chat.GROUP))) assert not is_match(handler, make_command_update(command, chat=Chat(23, Chat.PRIVATE))) def test_pass_args(self, dp, bot, command): """Test the passing of arguments alongside a command""" handler = self.make_default_handler(self.ch_callback_args, pass_args=True) dp.add_handler(handler) at_command = f'{command}@{bot.username}' assert self.response(dp, make_command_update(command)) assert self.response(dp, make_command_update(command + ' one two')) assert self.response(dp, make_command_update(at_command, bot=bot)) assert self.response(dp, make_command_update(at_command + ' one two', bot=bot)) def test_newline(self, dp, command): """Assert that newlines don't interfere with a command handler matching a message""" handler = self.make_default_handler() dp.add_handler(handler) update = make_command_update(command + '\nfoobar') assert is_match(handler, update) assert self.response(dp, update) @pytest.mark.parametrize('pass_keyword', BaseTest.PASS_KEYWORDS) def test_pass_data(self, dp, command_update, pass_combination, pass_keyword): handler = CommandHandler('test', self.make_callback_for(pass_keyword), **pass_combination) dp.add_handler(handler) assert self.response(dp, command_update) == pass_combination.get(pass_keyword, False) 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): """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')) assert not mock_filter.tested def test_context(self, cdp, command_update): """Test correct behaviour of CHs with context-based callbacks""" handler = self.make_default_handler(self.callback_context) cdp.add_handler(handler) assert self.response(cdp, command_update) def test_context_args(self, cdp, command): """Test CHs that pass arguments through ``context``""" handler = self.make_default_handler(self.callback_context_args) self._test_context_args_or_regex(cdp, handler, command) def test_context_regex(self, cdp, command): """Test CHs with context-based callbacks and a single filter""" handler = self.make_default_handler( self.callback_context_regex1, filters=Filters.regex('one two') ) self._test_context_args_or_regex(cdp, handler, command) def test_context_multiple_regex(self, cdp, command): """Test CHs with context-based callbacks and filters combined""" handler = self.make_default_handler( self.callback_context_regex2, filters=Filters.regex('one') & Filters.regex('two') ) self._test_context_args_or_regex(cdp, handler, command) # ----------------------------- PrefixHandler ----------------------------- 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, mro_slots, recwarn): handler = self.make_default_handler() for attr in handler.__slots__: assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" handler.custom, handler.command = 'should give warning', self.COMMANDS assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @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) def ch_callback_args(self, bot, update, args): if update.message.text in TestPrefixHandler.COMBINATIONS: self.test_flag = len(args) == 0 else: self.test_flag = args == ['one', 'two'] def test_basic(self, dp, prefix, command): """Test the basic expected response from a prefix handler""" handler = self.make_default_handler() dp.add_handler(handler) text = prefix + command assert self.response(dp, 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')) 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.update.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.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_pass_args(self, dp, prefix_message): handler = self.make_default_handler(self.ch_callback_args, pass_args=True) dp.add_handler(handler) assert self.response(dp, make_message_update(prefix_message)) update_with_args = make_message_update(prefix_message.text + ' one two') assert self.response(dp, update_with_args) @pytest.mark.parametrize('pass_keyword', BaseTest.PASS_KEYWORDS) def test_pass_data(self, dp, pass_combination, prefix_message_update, pass_keyword): """Assert that callbacks receive data iff its corresponding ``pass_*`` kwarg is enabled""" handler = self.make_default_handler( self.make_callback_for(pass_keyword), **pass_combination ) dp.add_handler(handler) assert self.response(dp, prefix_message_update) == pass_combination.get( pass_keyword, False ) 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 def test_edit_prefix(self): handler = self.make_default_handler() handler.prefix = ['?', '§'] assert handler._commands == list(combinations(['?', '§'], self.COMMANDS)) handler.prefix = '+' assert handler._commands == list(combinations(['+'], self.COMMANDS)) def test_edit_command(self): handler = self.make_default_handler() handler.command = 'foo' assert handler._commands == list(combinations(self.PREFIXES, ['foo'])) def test_basic_after_editing(self, dp, prefix, command): """Test the basic expected response from a prefix handler""" handler = self.make_default_handler() dp.add_handler(handler) text = prefix + command assert self.response(dp, make_message_update(text)) handler.command = 'foo' text = prefix + 'foo' assert self.response(dp, make_message_update(text)) def test_context(self, cdp, prefix_message_update): handler = self.make_default_handler(self.callback_context) cdp.add_handler(handler) assert self.response(cdp, prefix_message_update) def test_context_args(self, cdp, prefix_message_text): handler = self.make_default_handler(self.callback_context_args) self._test_context_args_or_regex(cdp, handler, prefix_message_text) def test_context_regex(self, cdp, prefix_message_text): handler = self.make_default_handler( self.callback_context_regex1, filters=Filters.regex('one two') ) self._test_context_args_or_regex(cdp, handler, prefix_message_text) def test_context_multiple_regex(self, cdp, prefix_message_text): handler = self.make_default_handler( self.callback_context_regex2, filters=Filters.regex('one') & Filters.regex('two') ) self._test_context_args_or_regex(cdp, handler, prefix_message_text) python-telegram-bot-13.11/tests/test_constants.py000066400000000000000000000035261417656324400222310ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import constants from telegram.error import BadRequest class TestConstants: @flaky(3, 1) def test_max_message_length(self, bot, chat_id): bot.send_message(chat_id=chat_id, text='a' * constants.MAX_MESSAGE_LENGTH) with pytest.raises( BadRequest, match='Message is too long', ): bot.send_message(chat_id=chat_id, text='a' * (constants.MAX_MESSAGE_LENGTH + 1)) @flaky(3, 1) def test_max_caption_length(self, bot, chat_id): good_caption = 'a' * constants.MAX_CAPTION_LENGTH with open('tests/data/telegram.png', 'rb') as f: good_msg = bot.send_photo(photo=f, caption=good_caption, chat_id=chat_id) assert good_msg.caption == good_caption bad_caption = good_caption + 'Z' with pytest.raises( BadRequest, match="Media_caption_too_long", ), open('tests/data/telegram.png', 'rb') as f: bot.send_photo(photo=f, caption=bad_caption, chat_id=chat_id) python-telegram-bot-13.11/tests/test_contact.py000066400000000000000000000126261417656324400216510ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import Contact, Voice from telegram.error import BadRequest @pytest.fixture(scope='class') def contact(): return Contact( TestContact.phone_number, TestContact.first_name, TestContact.last_name, TestContact.user_id, ) class TestContact: phone_number = '+11234567890' first_name = 'Leandro' last_name = 'Toledo' user_id = 23 def test_slot_behaviour(self, contact, recwarn, mro_slots): for attr in contact.__slots__: assert getattr(contact, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not contact.__dict__, f"got missing slot(s): {contact.__dict__}" assert len(mro_slots(contact)) == len(set(mro_slots(contact))), "duplicate slot" contact.custom, contact.first_name = 'should give warning', self.first_name assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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.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_send_with_contact(self, monkeypatch, bot, chat_id, contact): def test(url, data, **kwargs): 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', test) message = bot.send_contact(contact=contact, chat_id=chat_id) assert message @flaky(3, 1) @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'], ) def test_send_contact_default_allow_sending_without_reply( self, default_bot, chat_id, contact, custom ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_contact( chat_id, contact=contact, reply_to_message_id=reply_to_message.message_id ) def test_send_contact_without_required(self, bot, chat_id): with pytest.raises(ValueError, match='Either contact or phone_number and first_name'): bot.send_contact(chat_id=chat_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) python-telegram-bot-13.11/tests/test_contexttypes.py000066400000000000000000000036471417656324400227720ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ContextTypes, CallbackContext class SubClass(CallbackContext): pass class TestContextTypes: def test_slot_behaviour(self, mro_slots): 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" with pytest.raises(AttributeError): instance.custom 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-13.11/tests/test_conversationhandler.py000066400000000000000000001734351417656324400242740ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 logging from time import sleep import pytest from flaky import flaky from telegram import ( CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message, PreCheckoutQuery, ShippingQuery, Update, User, MessageEntity, ) from telegram.ext import ( ConversationHandler, CommandHandler, CallbackQueryHandler, MessageHandler, Filters, InlineQueryHandler, CallbackContext, DispatcherHandlerStop, TypeHandler, JobQueue, ) @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) @pytest.fixture(autouse=True) def start_stop_job_queue(dp): dp.job_queue = JobQueue() dp.job_queue.set_dispatcher(dp) dp.job_queue.start() yield dp.job_queue.stop() def raise_dphs(func): def decorator(self, *args, **kwargs): result = func(self, *args, **kwargs) if self.raise_dp_handler_stop: raise DispatcherHandlerStop(result) return result return decorator class TestConversationHandler: # 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_dp_handler_stop = False test_flag = False def test_slot_behaviour(self, recwarn, mro_slots): handler = ConversationHandler(self.entry_points, self.states, self.fallbacks) for attr in handler.__slots__: assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" handler.custom, handler._persistence = 'should give warning', handler._persistence assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), [ w.message for w in recwarn.list ] # Test related @pytest.fixture(autouse=True) def reset(self): self.raise_dp_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_dphs def start(self, bot, update): if isinstance(update, Update): return self._set_state(update, self.THIRSTY) return self._set_state(bot, self.THIRSTY) @raise_dphs def end(self, bot, update): return self._set_state(update, self.END) @raise_dphs def start_end(self, bot, update): return self._set_state(update, self.END) @raise_dphs def start_none(self, bot, update): return self._set_state(update, None) @raise_dphs def brew(self, bot, update): if isinstance(update, Update): return self._set_state(update, self.BREWING) return self._set_state(bot, self.BREWING) @raise_dphs def drink(self, bot, update): return self._set_state(update, self.DRINKING) @raise_dphs def code(self, bot, update): return self._set_state(update, self.CODING) @raise_dphs def passout(self, bot, update): assert update.message.text == '/brew' assert isinstance(update, Update) self.is_timeout = True @raise_dphs def passout2(self, bot, update): assert isinstance(update, Update) self.is_timeout = True @raise_dphs def passout_context(self, update, context): assert update.message.text == '/brew' assert isinstance(context, CallbackContext) self.is_timeout = True @raise_dphs def passout2_context(self, update, context): assert isinstance(context, CallbackContext) self.is_timeout = True # Drinking actions (nested) @raise_dphs def hold(self, bot, update): return self._set_state(update, self.HOLDING) @raise_dphs def sip(self, bot, update): return self._set_state(update, self.SIPPING) @raise_dphs def swallow(self, bot, update): return self._set_state(update, self.SWALLOWING) @raise_dphs def replenish(self, bot, update): return self._set_state(update, self.REPLENISHING) @raise_dphs def stop(self, bot, update): return self._set_state(update, self.STOPPING) # Tests @pytest.mark.parametrize( 'attr', [ 'entry_points', 'states', 'fallbacks', 'per_chat', 'name', 'per_user', 'allow_reentry', 'conversation_timeout', 'map_to_parent', ], indirect=False, ) def test_immutable(self, attr): ch = ConversationHandler( 'entry_points', {'states': ['states']}, 'fallbacks', per_chat='per_chat', per_user='per_user', per_message=False, allow_reentry='allow_reentry', conversation_timeout='conversation_timeout', name='name', map_to_parent='map_to_parent', ) value = getattr(ch, attr) if isinstance(value, list): assert value[0] == attr elif isinstance(value, dict): assert list(value.keys())[0] == attr else: assert getattr(ch, attr) == attr with pytest.raises(ValueError, match=f'You can not assign a new value to {attr}'): setattr(ch, attr, True) def test_immutable_per_message(self): ch = ConversationHandler( 'entry_points', {'states': ['states']}, 'fallbacks', per_chat='per_chat', per_user='per_user', per_message=False, allow_reentry='allow_reentry', conversation_timeout='conversation_timeout', name='name', map_to_parent='map_to_parent', ) assert ch.per_message is False with pytest.raises(ValueError, match='You can not assign a new value to per_message'): ch.per_message = True def test_per_all_false(self): with pytest.raises(ValueError, match="can't all be 'False'"): ConversationHandler( self.entry_points, self.states, self.fallbacks, per_chat=False, per_user=False, per_message=False, ) def test_name_and_persistent(self, dp): with pytest.raises(ValueError, match="when handler is unnamed"): dp.add_handler(ConversationHandler([], {}, [], persistent=True)) c = ConversationHandler([], {}, [], name="handler", persistent=True) assert c.name == "handler" def test_conversation_handler(self, dp, bot, user1, user2): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks ) dp.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')) ], bot=bot, ) dp.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') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING # Lets see if an invalid command makes sure, no state is changed. message.text = '/nothing' message.entities[0].length = len('/nothing') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING # Lets see if the state machine still works by pouring coffee. message.text = '/pourCoffee' message.entities[0].length = len('/pourCoffee') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING # Let's now verify that for another user, who did not start yet, # the state has not been changed. message.from_user = user2 dp.process_update(Update(update_id=0, message=message)) with pytest.raises(KeyError): self.current_state[user2.id] def test_conversation_handler_end(self, caplog, dp, bot, user1): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks ) dp.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')) ], bot=bot, ) dp.process_update(Update(update_id=0, message=message)) message.text = '/brew' message.entities[0].length = len('/brew') dp.process_update(Update(update_id=0, message=message)) message.text = '/pourCoffee' message.entities[0].length = len('/pourCoffee') dp.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): dp.process_update(Update(update_id=0, message=message)) assert len(caplog.records) == 0 assert self.current_state[user1.id] == self.END with pytest.raises(KeyError): print(handler.conversations[(self.group.id, user1.id)]) def test_conversation_handler_fallback(self, dp, bot, user1, user2): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks ) dp.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'))], bot=bot, ) dp.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') dp.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') dp.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') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY def test_unknown_state_warning(self, dp, bot, user1, recwarn): handler = ConversationHandler( entry_points=[CommandHandler("start", lambda u, c: 1)], states={ 1: [TypeHandler(Update, lambda u, c: 69)], 2: [TypeHandler(Update, lambda u, c: -1)], }, fallbacks=self.fallbacks, name="xyz", ) dp.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')) ], bot=bot, ) dp.process_update(Update(update_id=0, message=message)) sleep(0.5) dp.process_update(Update(update_id=1, message=message)) sleep(0.5) assert len(recwarn) == 1 assert str(recwarn[0].message) == ( "Handler returned state 69 which is unknown to the ConversationHandler xyz." ) def test_conversation_handler_per_chat(self, dp, bot, user1, user2): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, per_user=False, ) dp.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')) ], bot=bot, ) dp.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') dp.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') dp.process_update(Update(update_id=0, message=message)) assert handler.conversations[(self.group.id,)] == self.DRINKING def test_conversation_handler_per_user(self, dp, bot, user1): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, per_chat=False, ) dp.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')) ], bot=bot, ) dp.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') dp.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') dp.process_update(Update(update_id=0, message=message)) assert handler.conversations[(user1.id,)] == self.DRINKING def test_conversation_handler_per_message(self, dp, bot, user1, user2): def entry(bot, update): return 1 def one(bot, update): return 2 def two(bot, update): return ConversationHandler.END handler = ConversationHandler( entry_points=[CallbackQueryHandler(entry)], states={1: [CallbackQueryHandler(one)], 2: [CallbackQueryHandler(two)]}, fallbacks=[], per_message=True, ) dp.add_handler(handler) # User one, starts the state machine. message = Message( 0, None, self.group, from_user=user1, text='msg w/ inlinekeyboard', bot=bot ) cbq = CallbackQuery(0, user1, None, message=message, data='data', bot=bot) dp.process_update(Update(update_id=0, callback_query=cbq)) assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 1 dp.process_update(Update(update_id=0, callback_query=cbq)) assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 2 # Let's now verify that for a different user in the same group, the state will not be # updated cbq.from_user = user2 dp.process_update(Update(update_id=0, callback_query=cbq)) assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 2 def test_end_on_first_message(self, dp, bot, user1): handler = ConversationHandler( entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[] ) dp.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')) ], bot=bot, ) dp.process_update(Update(update_id=0, message=message)) assert len(handler.conversations) == 0 def test_end_on_first_message_async(self, dp, bot, user1): handler = ConversationHandler( entry_points=[ CommandHandler( 'start', lambda bot, update: dp.run_async(self.start_end, bot, update) ) ], states={}, fallbacks=[], ) dp.add_handler(handler) # User starts the state machine with an async function that immediately ends the # conversation. Async 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')) ], bot=bot, ) dp.update_queue.put(Update(update_id=0, message=message)) sleep(0.1) # Assert that the Promise has been accepted as the new state assert len(handler.conversations) == 1 message.text = 'resolve promise pls' message.entities[0].length = len('resolve promise pls') dp.update_queue.put(Update(update_id=0, message=message)) sleep(0.1) # Assert that the Promise has been resolved and the conversation ended. assert len(handler.conversations) == 0 def test_end_on_first_message_async_handler(self, dp, bot, user1): handler = ConversationHandler( entry_points=[CommandHandler('start', self.start_end, run_async=True)], states={}, fallbacks=[], ) dp.add_handler(handler) # User starts the state machine with an async function that immediately ends the # conversation. Async 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')) ], bot=bot, ) dp.update_queue.put(Update(update_id=0, message=message)) sleep(0.1) # Assert that the Promise has been accepted as the new state assert len(handler.conversations) == 1 message.text = 'resolve promise pls' message.entities[0].length = len('resolve promise pls') dp.update_queue.put(Update(update_id=0, message=message)) sleep(0.1) # Assert that the Promise has been resolved and the conversation ended. assert len(handler.conversations) == 0 def test_none_on_first_message(self, dp, bot, user1): handler = ConversationHandler( entry_points=[CommandHandler('start', self.start_none)], states={}, fallbacks=[] ) dp.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', bot=bot) dp.process_update(Update(update_id=0, message=message)) assert len(handler.conversations) == 0 def test_none_on_first_message_async(self, dp, bot, user1): handler = ConversationHandler( entry_points=[ CommandHandler( 'start', lambda bot, update: dp.run_async(self.start_none, bot, update) ) ], states={}, fallbacks=[], ) dp.add_handler(handler) # User starts the state machine with an async function that returns None # Async 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')) ], bot=bot, ) dp.update_queue.put(Update(update_id=0, message=message)) sleep(0.1) # Assert that the Promise has been accepted as the new state assert len(handler.conversations) == 1 message.text = 'resolve promise pls' dp.update_queue.put(Update(update_id=0, message=message)) sleep(0.1) # Assert that the Promise has been resolved and the conversation ended. assert len(handler.conversations) == 0 def test_none_on_first_message_async_handler(self, dp, bot, user1): handler = ConversationHandler( entry_points=[CommandHandler('start', self.start_none, run_async=True)], states={}, fallbacks=[], ) dp.add_handler(handler) # User starts the state machine with an async function that returns None # Async 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')) ], bot=bot, ) dp.update_queue.put(Update(update_id=0, message=message)) sleep(0.1) # Assert that the Promise has been accepted as the new state assert len(handler.conversations) == 1 message.text = 'resolve promise pls' dp.update_queue.put(Update(update_id=0, message=message)) sleep(0.1) # Assert that the Promise has been resolved and the conversation ended. assert len(handler.conversations) == 0 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, bot=bot) update = Update(0, callback_query=cbq) assert not handler.check_update(update) 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'), bot=bot) 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) def test_all_update_types(self, dp, bot, user1): handler = ConversationHandler( entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[] ) message = Message(0, None, self.group, from_user=user1, text='ignore', bot=bot) callback_query = CallbackQuery(0, user1, None, message=message, data='data', bot=bot) chosen_inline_result = ChosenInlineResult(0, user1, 'query', bot=bot) inline_query = InlineQuery(0, user1, 'query', 0, bot=bot) pre_checkout_query = PreCheckoutQuery(0, user1, 'USD', 100, [], bot=bot) shipping_query = ShippingQuery(0, user1, [], None, 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)) def test_no_jobqueue_warning(self, dp, bot, user1, caplog): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) # save dp.job_queue in temp variable jqueue # and then set dp.job_queue to None. jqueue = dp.job_queue dp.job_queue = None dp.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')) ], bot=bot, ) with caplog.at_level(logging.WARNING): dp.process_update(Update(update_id=0, message=message)) sleep(0.5) assert len(caplog.records) == 1 assert ( caplog.records[0].message == "Ignoring `conversation_timeout` because the Dispatcher has no JobQueue." ) # now set dp.job_queue back to it's original value dp.job_queue = jqueue def test_schedule_job_exception(self, dp, bot, user1, monkeypatch, caplog): def mocked_run_once(*a, **kw): raise Exception("job error") monkeypatch.setattr(dp.job_queue, "run_once", mocked_run_once) handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=100, ) dp.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')) ], bot=bot, ) with caplog.at_level(logging.ERROR): dp.process_update(Update(update_id=0, message=message)) sleep(0.5) assert len(caplog.records) == 2 assert ( caplog.records[0].message == "Failed to schedule timeout job due to the following exception:" ) assert caplog.records[1].message == "job error" def test_promise_exception(self, dp, bot, user1, caplog): """ Here we make sure that when a run_async handle raises an exception, the state isn't changed. """ def conv_entry(*a, **kw): return 1 def raise_error(*a, **kw): raise Exception("promise exception") handler = ConversationHandler( entry_points=[CommandHandler("start", conv_entry)], states={1: [MessageHandler(Filters.all, raise_error)]}, fallbacks=self.fallbacks, run_async=True, ) dp.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')) ], bot=bot, ) # start the conversation dp.process_update(Update(update_id=0, message=message)) sleep(0.1) message.text = "error" dp.process_update(Update(update_id=0, message=message)) sleep(0.1) message.text = "resolve promise pls" caplog.clear() with caplog.at_level(logging.ERROR): dp.process_update(Update(update_id=0, message=message)) sleep(0.5) assert len(caplog.records) == 3 assert caplog.records[0].message == "Promise function raised exception" assert caplog.records[1].message == "promise exception" # assert res is old state assert handler.conversations.get((self.group.id, user1.id))[0] == 1 def test_conversation_timeout(self, dp, bot, user1): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) dp.add_handler(handler) # 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')) ], bot=bot, ) dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY sleep(0.75) assert handler.conversations.get((self.group.id, user1.id)) is None # Start state machine, do something, then reach timeout dp.process_update(Update(update_id=1, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY message.text = '/brew' message.entities[0].length = len('/brew') dp.process_update(Update(update_id=2, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING sleep(0.7) assert handler.conversations.get((self.group.id, user1.id)) is None def test_timeout_not_triggered_on_conv_end_async(self, bot, dp, 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, run_async=True, ) dp.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')) ], bot=bot, ) # start the conversation dp.process_update(Update(update_id=0, message=message)) sleep(0.1) message.text = '/brew' message.entities[0].length = len('/brew') dp.process_update(Update(update_id=1, message=message)) sleep(0.1) message.text = '/pourCoffee' message.entities[0].length = len('/pourCoffee') dp.process_update(Update(update_id=2, message=message)) sleep(0.1) message.text = '/end' message.entities[0].length = len('/end') dp.process_update(Update(update_id=3, message=message)) sleep(1) # assert timeout handler didn't got called assert self.test_flag is False def test_conversation_timeout_dispatcher_handler_stop(self, dp, bot, user1, caplog): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) def timeout(*args, **kwargs): raise DispatcherHandlerStop() self.states.update({ConversationHandler.TIMEOUT: [TypeHandler(Update, timeout)]}) dp.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')) ], bot=bot, ) with caplog.at_level(logging.WARNING): dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY sleep(0.9) assert handler.conversations.get((self.group.id, user1.id)) is None assert len(caplog.records) == 1 rec = caplog.records[-1] assert rec.getMessage().startswith('DispatcherHandlerStop in TIMEOUT') def test_conversation_handler_timeout_update_and_context(self, cdp, bot, user1): context = None def start_callback(u, c): nonlocal context, self context = c return self.start(u, c) states = self.states timeout_handler = CommandHandler('start', None) states.update({ConversationHandler.TIMEOUT: [timeout_handler]}) handler = ConversationHandler( entry_points=[CommandHandler('start', start_callback)], states=states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) cdp.add_handler(handler) # 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')) ], bot=bot, ) update = Update(update_id=0, message=message) def timeout_callback(u, c): nonlocal update, context, self self.is_timeout = True assert u is update assert c is context timeout_handler.callback = timeout_callback cdp.process_update(update) sleep(0.7) assert handler.conversations.get((self.group.id, user1.id)) is None assert self.is_timeout @flaky(3, 1) def test_conversation_timeout_keeps_extending(self, dp, bot, user1): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) dp.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')) ], bot=bot, ) dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY sleep(0.35) # t=.35 assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY message.text = '/brew' message.entities[0].length = len('/brew') dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING sleep(0.25) # t=.6 assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING message.text = '/pourCoffee' message.entities[0].length = len('/pourCoffee') dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING sleep(0.4) # t=1.0 assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING sleep(0.3) # t=1.3 assert handler.conversations.get((self.group.id, user1.id)) is None def test_conversation_timeout_two_users(self, dp, bot, user1, user2): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, conversation_timeout=0.5, ) dp.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')) ], bot=bot, ) dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY message.text = '/brew' message.entities[0].length = len('/brew') message.entities[0].length = len('/brew') message.from_user = user2 dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user2.id)) is None message.text = '/start' message.entities[0].length = len('/start') dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user2.id)) == self.THIRSTY sleep(0.7) assert handler.conversations.get((self.group.id, user1.id)) is None assert handler.conversations.get((self.group.id, user2.id)) is None def test_conversation_handler_timeout_state(self, dp, 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, ) dp.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')) ], bot=bot, ) dp.process_update(Update(update_id=0, message=message)) message.text = '/brew' message.entities[0].length = len('/brew') dp.process_update(Update(update_id=0, message=message)) sleep(0.7) assert handler.conversations.get((self.group.id, user1.id)) is None assert self.is_timeout # MessageHandler timeout self.is_timeout = False message.text = '/start' message.entities[0].length = len('/start') dp.process_update(Update(update_id=1, message=message)) sleep(0.7) assert handler.conversations.get((self.group.id, user1.id)) is None assert self.is_timeout # Timeout but no valid handler self.is_timeout = False dp.process_update(Update(update_id=0, message=message)) message.text = '/brew' message.entities[0].length = len('/brew') dp.process_update(Update(update_id=0, message=message)) message.text = '/startCoding' message.entities[0].length = len('/startCoding') dp.process_update(Update(update_id=0, message=message)) sleep(0.7) assert handler.conversations.get((self.group.id, user1.id)) is None assert not self.is_timeout def test_conversation_handler_timeout_state_context(self, cdp, 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, ) cdp.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')) ], bot=bot, ) cdp.process_update(Update(update_id=0, message=message)) message.text = '/brew' message.entities[0].length = len('/brew') cdp.process_update(Update(update_id=0, message=message)) sleep(0.7) assert handler.conversations.get((self.group.id, user1.id)) is None assert self.is_timeout # MessageHandler timeout self.is_timeout = False message.text = '/start' message.entities[0].length = len('/start') cdp.process_update(Update(update_id=1, message=message)) sleep(0.7) assert handler.conversations.get((self.group.id, user1.id)) is None assert self.is_timeout # Timeout but no valid handler self.is_timeout = False cdp.process_update(Update(update_id=0, message=message)) message.text = '/brew' message.entities[0].length = len('/brew') cdp.process_update(Update(update_id=0, message=message)) message.text = '/startCoding' message.entities[0].length = len('/startCoding') cdp.process_update(Update(update_id=0, message=message)) sleep(0.7) assert handler.conversations.get((self.group.id, user1.id)) is None assert not self.is_timeout def test_conversation_timeout_cancel_conflict(self, dp, 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 def slowbrew(_bot, update): sleep(0.25) # Let's give to the original timeout a chance to execute 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, ) dp.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')) ], bot=bot, ) dp.process_update(Update(update_id=0, message=message)) sleep(0.25) message.text = '/slowbrew' message.entities[0].length = len('/slowbrew') dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) is not None assert not self.is_timeout sleep(0.7) assert handler.conversations.get((self.group.id, user1.id)) is None assert self.is_timeout def test_conversation_timeout_warning_only_shown_once(self, recwarn): ConversationHandler( entry_points=self.entry_points, states={ self.THIRSTY: [ ConversationHandler( entry_points=self.entry_points, states={ self.BREWING: [CommandHandler('pourCoffee', self.drink)], }, fallbacks=self.fallbacks, ) ], self.DRINKING: [ ConversationHandler( entry_points=self.entry_points, states={ self.CODING: [CommandHandler('startCoding', self.code)], }, fallbacks=self.fallbacks, ) ], }, fallbacks=self.fallbacks, conversation_timeout=100, ) assert len(recwarn) == 1 assert str(recwarn[0].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." ) def test_per_message_warning_is_only_shown_once(self, recwarn): ConversationHandler( entry_points=self.entry_points, states={ self.THIRSTY: [CommandHandler('pourCoffee', self.drink)], self.BREWING: [CommandHandler('startCoding', self.code)], }, fallbacks=self.fallbacks, per_message=True, ) assert len(recwarn) == 1 assert str(recwarn[0].message) == ( "If 'per_message=True', all entry points and state handlers" " must be 'CallbackQueryHandler', since no other handlers" " have a message context." ) def test_per_message_false_warning_is_only_shown_once(self, recwarn): ConversationHandler( entry_points=self.entry_points, states={ self.THIRSTY: [CallbackQueryHandler(self.drink)], self.BREWING: [CallbackQueryHandler(self.code)], }, fallbacks=self.fallbacks, per_message=False, ) assert len(recwarn) == 1 assert str(recwarn[0].message) == ( "If 'per_message=False', 'CallbackQueryHandler' will not be " "tracked for every message." ) def test_warnings_per_chat_is_only_shown_once(self, recwarn): def hello(bot, update): return self.BREWING def bye(bot, update): return ConversationHandler.END ConversationHandler( entry_points=self.entry_points, states={ self.THIRSTY: [InlineQueryHandler(hello)], self.BREWING: [InlineQueryHandler(bye)], }, fallbacks=self.fallbacks, per_chat=True, ) assert len(recwarn) == 1 assert str(recwarn[0].message) == ( "If 'per_chat=True', 'InlineQueryHandler' can not be used," " since inline queries have no chat context." ) def test_nested_conversation_handler(self, dp, 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 ) dp.add_handler(handler) # User one, starts the state machine. message = Message( 0, None, self.group, from_user=user1, text='/start', bot=bot, entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) ], ) dp.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') dp.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') dp.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') dp.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') dp.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') dp.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') dp.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') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.REPLENISHING assert handler.conversations[(0, user1.id)] == self.BREWING # The user wants to drink their coffee again message.text = '/pourCoffee' message.entities[0].length = len('/pourCoffee') dp.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') dp.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') dp.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') dp.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') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.END assert handler.conversations[(0, user1.id)] == self.CODING # The user wants to drink once more message.text = '/drinkMore' message.entities[0].length = len('/drinkMore') dp.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') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.STOPPING assert handler.conversations.get((0, user1.id)) is None def test_conversation_dispatcher_handler_stop(self, dp, 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 dp.add_handler(handler) dp.add_handler(TypeHandler(Update, test_callback), group=1) self.raise_dp_handler_stop = True # User one, starts the state machine. message = Message( 0, None, self.group, text='/start', bot=bot, from_user=user1, entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) ], ) dp.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') dp.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') dp.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') dp.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') dp.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') dp.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') dp.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') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.REPLENISHING assert handler.conversations[(0, user1.id)] == self.BREWING assert not self.test_flag # The user wants to drink their coffee again message.text = '/pourCoffee' message.entities[0].length = len('/pourCoffee') dp.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') dp.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') dp.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') dp.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') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.END assert handler.conversations[(0, user1.id)] == self.CODING assert not self.test_flag # The user wants to drink once more message.text = '/drinkMore' message.entities[0].length = len('/drinkMore') dp.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') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.STOPPING assert handler.conversations.get((0, user1.id)) is None assert not self.test_flag def test_conversation_handler_run_async_true(self, dp): conv_handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks, run_async=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.run_async def test_conversation_handler_run_async_false(self, dp): conv_handler = ConversationHandler( entry_points=[CommandHandler('start', self.start_end, run_async=True)], states=self.states, fallbacks=self.fallbacks, run_async=False, ) for handler in conv_handler.entry_points: assert handler.run_async all_handlers = conv_handler.fallbacks for state_handlers in conv_handler.states.values(): all_handlers += state_handlers for handler in all_handlers: assert not handler.run_async.value python-telegram-bot-13.11/tests/test_defaults.py000066400000000000000000000054101417656324400220160ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 Defaults from telegram import User class TestDefault: def test_slot_behaviour(self, recwarn, mro_slots): a = Defaults(parse_mode='HTML', quote=True) for attr in a.__slots__: assert getattr(a, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not a.__dict__, f"got missing slot(s): {a.__dict__}" assert len(mro_slots(a)) == len(set(mro_slots(a))), "duplicate slot" a.custom, a._parse_mode = 'should give warning', a._parse_mode assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_data_assignment(self, cdp): defaults = Defaults() with pytest.raises(AttributeError): defaults.parse_mode = True with pytest.raises(AttributeError): defaults.explanation_parse_mode = True with pytest.raises(AttributeError): defaults.disable_notification = True with pytest.raises(AttributeError): defaults.disable_web_page_preview = True with pytest.raises(AttributeError): defaults.allow_sending_without_reply = True with pytest.raises(AttributeError): defaults.timeout = True with pytest.raises(AttributeError): defaults.quote = True with pytest.raises(AttributeError): defaults.tzinfo = True with pytest.raises(AttributeError): defaults.run_async = True def test_equality(self): a = Defaults(parse_mode='HTML', quote=True) b = Defaults(parse_mode='HTML', quote=True) c = Defaults(parse_mode='HTML', quote=False) d = Defaults(parse_mode='HTML', timeout=50) e = User(123, 'test_user', 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-13.11/tests/test_dice.py000066400000000000000000000046301417656324400211160ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 Dice, BotCommand @pytest.fixture(scope="class", params=Dice.ALL_EMOJI) def dice(request): return Dice(value=5, emoji=request.param) class TestDice: value = 4 def test_slot_behaviour(self, dice, recwarn, mro_slots): for attr in dice.__slots__: assert getattr(dice, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not dice.__dict__, f"got missing slot(s): {dice.__dict__}" assert len(mro_slots(dice)) == len(set(mro_slots(dice))), "duplicate slot" dice.custom, dice.value = 'should give warning', self.value assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @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.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-13.11/tests/test_dispatcher.py000066400000000000000000001037501417656324400223430ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 logging from queue import Queue from threading import current_thread from time import sleep import pytest from telegram import TelegramError, Message, User, Chat, Update, Bot, MessageEntity from telegram.ext import ( MessageHandler, Filters, Defaults, CommandHandler, CallbackContext, JobQueue, BasePersistence, ContextTypes, ) from telegram.ext.dispatcher import run_async, Dispatcher, DispatcherHandlerStop from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import DEFAULT_FALSE from tests.conftest import create_dp from collections import defaultdict @pytest.fixture(scope='function') def dp2(bot): yield from create_dp(bot) class CustomContext(CallbackContext): pass class TestDispatcher: message_update = Update( 1, message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') ) received = None count = 0 def test_slot_behaviour(self, dp2, recwarn, mro_slots): for at in dp2.__slots__: at = f"_Dispatcher{at}" if at.startswith('__') and not at.endswith('__') else at assert getattr(dp2, at, 'err') != 'err', f"got extra slot '{at}'" assert not dp2.__dict__, f"got missing slot(s): {dp2.__dict__}" assert len(mro_slots(dp2)) == len(set(mro_slots(dp2))), "duplicate slot" dp2.custom, dp2.running = 'should give warning', dp2.running assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list class CustomDispatcher(Dispatcher): pass # Tests that setting custom attrs of Dispatcher subclass doesn't raise warning a = CustomDispatcher(None, None) a.my_custom = 'no error!' assert len(recwarn) == 1 dp2.__setattr__('__test', 'mangled success') assert getattr(dp2, '_Dispatcher__test', 'e') == 'mangled success', "mangling failed" @pytest.fixture(autouse=True, name='reset') def reset_fixture(self): self.reset() def reset(self): self.received = None self.count = 0 def error_handler(self, bot, update, error): self.received = error.message def error_handler_context(self, update, context): self.received = context.error.message def error_handler_raise_error(self, bot, update, error): raise Exception('Failing bigly') def callback_increase_count(self, bot, update): self.count += 1 def callback_set_count(self, count): def callback(bot, update): self.count = count return callback def callback_raise_error(self, bot, update): if isinstance(bot, Bot): raise TelegramError(update.message.text) raise TelegramError(bot.message.text) def callback_if_not_update_queue(self, bot, update, update_queue=None): if update_queue is not None: self.received = update.message 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 def test_less_than_one_worker_warning(self, dp, recwarn): Dispatcher(dp.bot, dp.update_queue, job_queue=dp.job_queue, workers=0, use_context=True) assert len(recwarn) == 1 assert ( str(recwarn[0].message) == 'Asynchronous callbacks can not be processed without at least one worker thread.' ) def test_one_context_per_update(self, cdp): def one(update, context): if update.message.text == 'test': context.my_flag = True def two(update, context): if update.message.text == 'test': if not hasattr(context, 'my_flag'): pytest.fail() else: if hasattr(context, 'my_flag'): pytest.fail() cdp.add_handler(MessageHandler(Filters.regex('test'), one), group=1) cdp.add_handler(MessageHandler(None, two), group=2) u = Update(1, Message(1, None, None, None, text='test')) cdp.process_update(u) u.message.text = 'something' cdp.process_update(u) def test_error_handler(self, dp): dp.add_error_handler(self.error_handler) error = TelegramError('Unauthorized.') dp.update_queue.put(error) sleep(0.1) assert self.received == 'Unauthorized.' # Remove handler dp.remove_error_handler(self.error_handler) self.reset() dp.update_queue.put(error) sleep(0.1) assert self.received is None def test_double_add_error_handler(self, dp, caplog): dp.add_error_handler(self.error_handler) with caplog.at_level(logging.DEBUG): dp.add_error_handler(self.error_handler) assert len(caplog.records) == 1 assert caplog.records[-1].getMessage().startswith('The callback is already registered') def test_construction_with_bad_persistence(self, caplog, bot): class my_per: def __init__(self): self.store_user_data = False self.store_chat_data = False self.store_bot_data = False self.store_callback_data = False with pytest.raises( TypeError, match='persistence must be based on telegram.ext.BasePersistence' ): Dispatcher(bot, None, persistence=my_per()) def test_error_handler_that_raises_errors(self, dp): """ Make sure that errors raised in error handlers don't break the main loop of the dispatcher """ handler_raise_error = MessageHandler(Filters.all, self.callback_raise_error) handler_increase_count = MessageHandler(Filters.all, self.callback_increase_count) error = TelegramError('Unauthorized.') dp.add_error_handler(self.error_handler_raise_error) # From errors caused by handlers dp.add_handler(handler_raise_error) dp.update_queue.put(self.message_update) sleep(0.1) # From errors in the update_queue dp.remove_handler(handler_raise_error) dp.add_handler(handler_increase_count) dp.update_queue.put(error) dp.update_queue.put(self.message_update) sleep(0.1) assert self.count == 1 @pytest.mark.parametrize(['run_async', 'expected_output'], [(True, 5), (False, 0)]) def test_default_run_async_error_handler(self, dp, monkeypatch, run_async, expected_output): def mock_async_err_handler(*args, **kwargs): self.count = 5 # set defaults value to dp.bot dp.bot.defaults = Defaults(run_async=run_async) try: dp.add_handler(MessageHandler(Filters.all, self.callback_raise_error)) dp.add_error_handler(self.error_handler) monkeypatch.setattr(dp, 'run_async', mock_async_err_handler) dp.process_update(self.message_update) assert self.count == expected_output finally: # reset dp.bot.defaults values dp.bot.defaults = None @pytest.mark.parametrize( ['run_async', 'expected_output'], [(True, 'running async'), (False, None)] ) def test_default_run_async(self, monkeypatch, dp, run_async, expected_output): def mock_run_async(*args, **kwargs): self.received = 'running async' # set defaults value to dp.bot dp.bot.defaults = Defaults(run_async=run_async) try: dp.add_handler(MessageHandler(Filters.all, lambda u, c: None)) monkeypatch.setattr(dp, 'run_async', mock_run_async) dp.process_update(self.message_update) assert self.received == expected_output finally: # reset defaults value dp.bot.defaults = None def test_run_async_multiple(self, bot, dp, dp2): def get_dispatcher_name(q): q.put(current_thread().name) q1 = Queue() q2 = Queue() dp.run_async(get_dispatcher_name, q1) dp2.run_async(get_dispatcher_name, q2) sleep(0.1) name1 = q1.get() name2 = q2.get() assert name1 != name2 def test_multiple_run_async_decorator(self, dp, dp2): # Make sure we got two dispatchers and that they are not the same assert isinstance(dp, Dispatcher) assert isinstance(dp2, Dispatcher) assert dp is not dp2 @run_async def must_raise_runtime_error(): pass with pytest.raises(RuntimeError): must_raise_runtime_error() def test_run_async_with_args(self, dp): dp.add_handler( MessageHandler( Filters.all, run_async(self.callback_if_not_update_queue), pass_update_queue=True ) ) dp.update_queue.put(self.message_update) sleep(0.1) assert self.received == self.message_update.message def test_multiple_run_async_deprecation(self, dp): assert isinstance(dp, Dispatcher) @run_async def callback(update, context): pass dp.add_handler(MessageHandler(Filters.all, callback)) with pytest.warns(TelegramDeprecationWarning, match='@run_async decorator'): dp.process_update(self.message_update) def test_async_raises_dispatcher_handler_stop(self, dp, caplog): @run_async def callback(update, context): raise DispatcherHandlerStop() dp.add_handler(MessageHandler(Filters.all, callback)) with caplog.at_level(logging.WARNING): dp.update_queue.put(self.message_update) sleep(0.1) assert len(caplog.records) == 1 assert ( caplog.records[-1] .getMessage() .startswith('DispatcherHandlerStop is not supported ' 'with async functions') ) def test_async_raises_exception(self, dp, caplog): @run_async def callback(update, context): raise RuntimeError('async raising exception') dp.add_handler(MessageHandler(Filters.all, callback)) with caplog.at_level(logging.WARNING): dp.update_queue.put(self.message_update) sleep(0.1) assert len(caplog.records) == 1 assert ( caplog.records[-1] .getMessage() .startswith('A promise with deactivated error handling') ) def test_add_async_handler(self, dp): dp.add_handler( MessageHandler( Filters.all, self.callback_if_not_update_queue, pass_update_queue=True, run_async=True, ) ) dp.update_queue.put(self.message_update) sleep(0.1) assert self.received == self.message_update.message def test_run_async_no_error_handler(self, dp, caplog): def func(): raise RuntimeError('Async Error') with caplog.at_level(logging.ERROR): dp.run_async(func) sleep(0.1) assert len(caplog.records) == 1 assert caplog.records[-1].getMessage().startswith('No error handlers are registered') def test_async_handler_error_handler(self, dp): dp.add_handler(MessageHandler(Filters.all, self.callback_raise_error, run_async=True)) dp.add_error_handler(self.error_handler) dp.update_queue.put(self.message_update) sleep(0.1) assert self.received == self.message_update.message.text def test_async_handler_async_error_handler_context(self, cdp): cdp.add_handler(MessageHandler(Filters.all, self.callback_raise_error, run_async=True)) cdp.add_error_handler(self.error_handler_context, run_async=True) cdp.update_queue.put(self.message_update) sleep(2) assert self.received == self.message_update.message.text def test_async_handler_error_handler_that_raises_error(self, dp, caplog): handler = MessageHandler(Filters.all, self.callback_raise_error, run_async=True) dp.add_handler(handler) dp.add_error_handler(self.error_handler_raise_error, run_async=False) with caplog.at_level(logging.ERROR): dp.update_queue.put(self.message_update) sleep(0.1) assert len(caplog.records) == 1 assert caplog.records[-1].getMessage().startswith('An uncaught error was raised') # Make sure that the main loop still runs dp.remove_handler(handler) dp.add_handler(MessageHandler(Filters.all, self.callback_increase_count, run_async=True)) dp.update_queue.put(self.message_update) sleep(0.1) assert self.count == 1 def test_async_handler_async_error_handler_that_raises_error(self, dp, caplog): handler = MessageHandler(Filters.all, self.callback_raise_error, run_async=True) dp.add_handler(handler) dp.add_error_handler(self.error_handler_raise_error, run_async=True) with caplog.at_level(logging.ERROR): dp.update_queue.put(self.message_update) sleep(0.1) assert len(caplog.records) == 1 assert caplog.records[-1].getMessage().startswith('An uncaught error was raised') # Make sure that the main loop still runs dp.remove_handler(handler) dp.add_handler(MessageHandler(Filters.all, self.callback_increase_count, run_async=True)) dp.update_queue.put(self.message_update) sleep(0.1) assert self.count == 1 def test_error_in_handler(self, dp): dp.add_handler(MessageHandler(Filters.all, self.callback_raise_error)) dp.add_error_handler(self.error_handler) dp.update_queue.put(self.message_update) sleep(0.1) assert self.received == self.message_update.message.text def test_add_remove_handler(self, dp): handler = MessageHandler(Filters.all, self.callback_increase_count) dp.add_handler(handler) dp.update_queue.put(self.message_update) sleep(0.1) assert self.count == 1 dp.remove_handler(handler) dp.update_queue.put(self.message_update) assert self.count == 1 def test_add_remove_handler_non_default_group(self, dp): handler = MessageHandler(Filters.all, self.callback_increase_count) dp.add_handler(handler, group=2) with pytest.raises(KeyError): dp.remove_handler(handler) dp.remove_handler(handler, group=2) def test_error_start_twice(self, dp): assert dp.running dp.start() def test_handler_order_in_group(self, dp): dp.add_handler(MessageHandler(Filters.photo, self.callback_set_count(1))) dp.add_handler(MessageHandler(Filters.all, self.callback_set_count(2))) dp.add_handler(MessageHandler(Filters.text, self.callback_set_count(3))) dp.update_queue.put(self.message_update) sleep(0.1) assert self.count == 2 def test_groups(self, dp): dp.add_handler(MessageHandler(Filters.all, self.callback_increase_count)) dp.add_handler(MessageHandler(Filters.all, self.callback_increase_count), group=2) dp.add_handler(MessageHandler(Filters.all, self.callback_increase_count), group=-1) dp.update_queue.put(self.message_update) sleep(0.1) assert self.count == 3 def test_add_handler_errors(self, dp): handler = 'not a handler' with pytest.raises(TypeError, match='handler is not an instance of'): dp.add_handler(handler) handler = MessageHandler(Filters.photo, self.callback_set_count(1)) with pytest.raises(TypeError, match='group is not int'): dp.add_handler(handler, 'one') def test_flow_stop(self, dp, bot): passed = [] def start1(b, u): passed.append('start1') raise DispatcherHandlerStop def start2(b, u): passed.append('start2') def start3(b, u): passed.append('start3') def error(b, u, e): passed.append('error') passed.append(e) update = Update( 1, message=Message( 1, None, None, None, text='/start', entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) ], bot=bot, ), ) # If Stop raised handlers in other groups should not be called. passed = [] dp.add_handler(CommandHandler('start', start1), 1) dp.add_handler(CommandHandler('start', start3), 1) dp.add_handler(CommandHandler('start', start2), 2) dp.process_update(update) assert passed == ['start1'] def test_exception_in_handler(self, dp, bot): passed = [] err = Exception('General exception') def start1(b, u): passed.append('start1') raise err def start2(b, u): passed.append('start2') def start3(b, u): passed.append('start3') def error(b, u, e): passed.append('error') passed.append(e) update = Update( 1, message=Message( 1, None, None, None, text='/start', entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) ], bot=bot, ), ) # 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 = [] dp.add_handler(CommandHandler('start', start1), 1) dp.add_handler(CommandHandler('start', start2), 1) dp.add_handler(CommandHandler('start', start3), 2) dp.add_error_handler(error) dp.process_update(update) assert passed == ['start1', 'error', err, 'start3'] def test_telegram_error_in_handler(self, dp, bot): passed = [] err = TelegramError('Telegram error') def start1(b, u): passed.append('start1') raise err def start2(b, u): passed.append('start2') def start3(b, u): passed.append('start3') def error(b, u, e): passed.append('error') passed.append(e) update = Update( 1, message=Message( 1, None, None, None, text='/start', entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) ], bot=bot, ), ) # If a TelegramException was caught, an error handler should be called and no further # handlers from the same group should be called. dp.add_handler(CommandHandler('start', start1), 1) dp.add_handler(CommandHandler('start', start2), 1) dp.add_handler(CommandHandler('start', start3), 2) dp.add_error_handler(error) dp.process_update(update) assert passed == ['start1', 'error', err, 'start3'] assert passed[2] is err def test_error_while_saving_chat_data(self, bot): increment = [] class OwnPersistence(BasePersistence): def __init__(self): super().__init__() self.store_user_data = True self.store_chat_data = True self.store_bot_data = True self.store_callback_data = True def get_callback_data(self): return None def update_callback_data(self, data): raise Exception def get_bot_data(self): return {} def update_bot_data(self, data): raise Exception def get_chat_data(self): return defaultdict(dict) def update_chat_data(self, chat_id, data): raise Exception def get_user_data(self): return defaultdict(dict) def update_user_data(self, user_id, data): raise Exception def get_conversations(self, name): pass def update_conversation(self, name, key, new_state): pass def start1(b, u): pass def error(b, u, e): increment.append("error") # If updating a user_data or chat_data from a persistence object throws an error, # the error handler should catch it update = Update( 1, message=Message( 1, None, Chat(1, "lala"), from_user=User(1, "Test", False), text='/start', entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) ], bot=bot, ), ) my_persistence = OwnPersistence() dp = Dispatcher(bot, None, persistence=my_persistence, use_context=False) dp.add_handler(CommandHandler('start', start1)) dp.add_error_handler(error) dp.process_update(update) assert increment == ["error", "error", "error", "error"] def test_flow_stop_in_error_handler(self, dp, bot): passed = [] err = TelegramError('Telegram error') def start1(b, u): passed.append('start1') raise err def start2(b, u): passed.append('start2') def start3(b, u): passed.append('start3') def error(b, u, e): passed.append('error') passed.append(e) raise DispatcherHandlerStop update = Update( 1, message=Message( 1, None, None, None, text='/start', entities=[ MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) ], bot=bot, ), ) # If a TelegramException was caught, an error handler should be called and no further # handlers from the same group should be called. dp.add_handler(CommandHandler('start', start1), 1) dp.add_handler(CommandHandler('start', start2), 1) dp.add_handler(CommandHandler('start', start3), 2) dp.add_error_handler(error) dp.process_update(update) assert passed == ['start1', 'error', err] assert passed[2] is err def test_error_handler_context(self, cdp): cdp.add_error_handler(self.callback_context) error = TelegramError('Unauthorized.') cdp.update_queue.put(error) sleep(0.1) assert self.received == 'Unauthorized.' def test_sensible_worker_thread_names(self, dp2): thread_names = [thread.name for thread in dp2._Dispatcher__async_threads] for thread_name in thread_names: assert thread_name.startswith(f"Bot:{dp2.bot.id}:worker:") def test_non_context_deprecation(self, dp): with pytest.warns(TelegramDeprecationWarning): Dispatcher( dp.bot, dp.update_queue, job_queue=dp.job_queue, workers=0, use_context=False ) def test_error_while_persisting(self, cdp, monkeypatch): class OwnPersistence(BasePersistence): def __init__(self): super().__init__() self.store_user_data = True self.store_chat_data = True self.store_bot_data = True self.store_callback_data = True def update(self, data): raise Exception('PersistenceError') def update_callback_data(self, data): self.update(data) def update_bot_data(self, data): self.update(data) def update_chat_data(self, chat_id, data): self.update(data) def update_user_data(self, user_id, data): self.update(data) def get_chat_data(self): pass def get_bot_data(self): pass def get_user_data(self): pass def get_callback_data(self): pass def get_conversations(self, name): pass def update_conversation(self, name, key, new_state): pass def refresh_bot_data(self, bot_data): pass def refresh_user_data(self, user_id, user_data): pass def refresh_chat_data(self, chat_id, chat_data): pass def callback(update, context): pass test_flag = False def error(update, context): nonlocal test_flag test_flag = str(context.error) == 'PersistenceError' raise Exception('ErrorHandlingError') def logger(message): assert 'uncaught error was raised while handling' in message update = Update( 1, message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') ) handler = MessageHandler(Filters.all, callback) cdp.add_handler(handler) cdp.add_error_handler(error) monkeypatch.setattr(cdp.logger, 'exception', logger) cdp.persistence = OwnPersistence() cdp.process_update(update) assert test_flag def test_persisting_no_user_no_chat(self, cdp): class OwnPersistence(BasePersistence): def __init__(self): super().__init__() self.store_user_data = True self.store_chat_data = True self.store_bot_data = True self.test_flag_bot_data = False self.test_flag_chat_data = False self.test_flag_user_data = False def update_bot_data(self, data): self.test_flag_bot_data = True def update_chat_data(self, chat_id, data): self.test_flag_chat_data = True def update_user_data(self, user_id, data): self.test_flag_user_data = True def update_conversation(self, name, key, new_state): pass def get_conversations(self, name): pass def get_user_data(self): pass def get_bot_data(self): pass def get_chat_data(self): pass def refresh_bot_data(self, bot_data): pass def refresh_user_data(self, user_id, user_data): pass def refresh_chat_data(self, chat_id, chat_data): pass def callback(update, context): pass handler = MessageHandler(Filters.all, callback) cdp.add_handler(handler) cdp.persistence = OwnPersistence() update = Update( 1, message=Message(1, None, None, from_user=User(1, '', False), text='Text') ) cdp.process_update(update) assert cdp.persistence.test_flag_bot_data assert cdp.persistence.test_flag_user_data assert not cdp.persistence.test_flag_chat_data cdp.persistence.test_flag_bot_data = False cdp.persistence.test_flag_user_data = False cdp.persistence.test_flag_chat_data = False update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text='Text')) cdp.process_update(update) assert cdp.persistence.test_flag_bot_data assert not cdp.persistence.test_flag_user_data assert cdp.persistence.test_flag_chat_data def test_update_persistence_once_per_update(self, monkeypatch, dp): def update_persistence(*args, **kwargs): self.count += 1 def dummy_callback(*args): pass monkeypatch.setattr(dp, 'update_persistence', update_persistence) for group in range(5): dp.add_handler(MessageHandler(Filters.text, dummy_callback), group=group) update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text=None)) dp.process_update(update) assert self.count == 0 update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text='text')) dp.process_update(update) assert self.count == 1 def test_update_persistence_all_async(self, monkeypatch, dp): def update_persistence(*args, **kwargs): self.count += 1 def dummy_callback(*args, **kwargs): pass monkeypatch.setattr(dp, 'update_persistence', update_persistence) monkeypatch.setattr(dp, 'run_async', dummy_callback) for group in range(5): dp.add_handler( MessageHandler(Filters.text, dummy_callback, run_async=True), group=group ) update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text='Text')) dp.process_update(update) assert self.count == 0 dp.bot.defaults = Defaults(run_async=True) try: for group in range(5): dp.add_handler(MessageHandler(Filters.text, dummy_callback), group=group) update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text='Text')) dp.process_update(update) assert self.count == 0 finally: dp.bot.defaults = None @pytest.mark.parametrize('run_async', [DEFAULT_FALSE, False]) def test_update_persistence_one_sync(self, monkeypatch, dp, run_async): def update_persistence(*args, **kwargs): self.count += 1 def dummy_callback(*args, **kwargs): pass monkeypatch.setattr(dp, 'update_persistence', update_persistence) monkeypatch.setattr(dp, 'run_async', dummy_callback) for group in range(5): dp.add_handler( MessageHandler(Filters.text, dummy_callback, run_async=True), group=group ) dp.add_handler(MessageHandler(Filters.text, dummy_callback, run_async=run_async), group=5) update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text='Text')) dp.process_update(update) assert self.count == 1 @pytest.mark.parametrize('run_async,expected', [(DEFAULT_FALSE, 1), (False, 1), (True, 0)]) def test_update_persistence_defaults_async(self, monkeypatch, dp, run_async, expected): def update_persistence(*args, **kwargs): self.count += 1 def dummy_callback(*args, **kwargs): pass monkeypatch.setattr(dp, 'update_persistence', update_persistence) monkeypatch.setattr(dp, 'run_async', dummy_callback) dp.bot.defaults = Defaults(run_async=run_async) try: for group in range(5): dp.add_handler(MessageHandler(Filters.text, dummy_callback), group=group) update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text='Text')) dp.process_update(update) assert self.count == expected finally: dp.bot.defaults = None def test_custom_context_init(self, bot): cc = ContextTypes( context=CustomContext, user_data=int, chat_data=float, bot_data=complex, ) dispatcher = Dispatcher(bot, Queue(), context_types=cc) assert isinstance(dispatcher.user_data[1], int) assert isinstance(dispatcher.chat_data[1], float) assert isinstance(dispatcher.bot_data, complex) def test_custom_context_error_handler(self, bot): def error_handler(_, context): self.received = ( type(context), type(context.user_data), type(context.chat_data), type(context.bot_data), ) dispatcher = Dispatcher( bot, Queue(), context_types=ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ), ) dispatcher.add_error_handler(error_handler) dispatcher.add_handler(MessageHandler(Filters.all, self.callback_raise_error)) dispatcher.process_update(self.message_update) sleep(0.1) assert self.received == (CustomContext, float, complex, int) def test_custom_context_handler_callback(self, bot): def callback(_, context): self.received = ( type(context), type(context.user_data), type(context.chat_data), type(context.bot_data), ) dispatcher = Dispatcher( bot, Queue(), context_types=ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ), ) dispatcher.add_handler(MessageHandler(Filters.all, callback)) dispatcher.process_update(self.message_update) sleep(0.1) assert self.received == (CustomContext, float, complex, int) python-telegram-bot-13.11/tests/test_document.py000066400000000000000000000315021417656324400220260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 import pytest from flaky import flaky from telegram import Document, PhotoSize, TelegramError, Voice, MessageEntity, Bot from telegram.error import BadRequest from telegram.utils.helpers import escape_markdown from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling @pytest.fixture(scope='function') def document_file(): f = open('tests/data/telegram.png', 'rb') yield f f.close() @pytest.fixture(scope='class') def document(bot, chat_id): with open('tests/data/telegram.png', 'rb') as f: return bot.send_document(chat_id, document=f, timeout=50).document class TestDocument: 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' def test_slot_behaviour(self, document, recwarn, mro_slots): for attr in document.__slots__: assert getattr(document, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not document.__dict__, f"got missing slot(s): {document.__dict__}" assert len(mro_slots(document)) == len(set(mro_slots(document))), "duplicate slot" document.custom, document.file_name = 'should give warning', self.file_name assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), f"{recwarn}" 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.thumb.file_size == self.thumb_file_size assert document.thumb.width == self.thumb_width assert document.thumb.height == self.thumb_height @flaky(3, 1) def test_send_all_args(self, bot, chat_id, document_file, document, thumb_file): message = bot.send_document( chat_id, document=document_file, caption=self.caption, disable_notification=False, protect_content=True, filename='telegram_custom.png', parse_mode='Markdown', thumb=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.thumb, 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.thumb.width == self.thumb_width assert message.document.thumb.height == self.thumb_height assert message.has_protected_content @flaky(3, 1) def test_get_and_download(self, bot, document): new_file = bot.get_file(document.file_id) assert new_file.file_size == document.file_size assert new_file.file_id == document.file_id assert new_file.file_unique_id == document.file_unique_id assert new_file.file_path.startswith('https://') new_file.download('telegram.png') assert os.path.isfile('telegram.png') @flaky(3, 1) def test_send_url_gif_file(self, bot, chat_id): message = 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.thumb, PhotoSize) assert document.file_name == 'telegram.gif' assert document.mime_type == 'image/gif' assert document.file_size == 3878 @flaky(3, 1) def test_send_resend(self, bot, chat_id, document): message = bot.send_document(chat_id=chat_id, document=document.file_id) assert message.document == document @pytest.mark.parametrize('disable_content_type_detection', [True, False, None]) def test_send_with_document( self, monkeypatch, bot, chat_id, document, disable_content_type_detection ): def make_assertion(url, data, **kwargs): 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 = bot.send_document( document=document, chat_id=chat_id, disable_content_type_detection=disable_content_type_detection, ) assert message @flaky(3, 1) 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 = bot.send_document( chat_id, document, caption=test_string, caption_entities=entities ) assert message.caption == test_string assert message.caption_entities == entities @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) 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 = default_bot.send_document(chat_id, document, caption=test_markdown_string) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_document_default_parse_mode_2(self, default_bot, chat_id, document): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_document_default_parse_mode_3(self, default_bot, chat_id, document): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) @flaky(3, 1) @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'], ) def test_send_document_default_allow_sending_without_reply( self, default_bot, chat_id, document, custom ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_document( chat_id, document, reply_to_message_id=reply_to_message.message_id ) def test_send_document_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('document') == expected and data.get('thumb') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.send_document(chat_id, file, thumb=file) assert test_flag monkeypatch.delattr(bot, '_post') def test_de_json(self, bot, document): json_dict = { 'file_id': self.document_file_id, 'file_unique_id': self.document_file_unique_id, 'thumb': document.thumb.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.file_id == self.document_file_id assert test_document.file_unique_id == self.document_file_unique_id assert test_document.thumb == document.thumb 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 @flaky(3, 1) def test_error_send_empty_file(self, bot, chat_id): with open(os.devnull, 'rb') as f, pytest.raises(TelegramError): bot.send_document(chat_id=chat_id, document=f) @flaky(3, 1) def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): bot.send_document(chat_id=chat_id, document='') def test_error_send_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): bot.send_document(chat_id=chat_id) def test_get_file_instance_method(self, monkeypatch, document): def make_assertion(*_, **kwargs): return kwargs['file_id'] == document.file_id assert check_shortcut_signature(Document.get_file, Bot.get_file, ['file_id'], []) assert check_shortcut_call(document.get_file, document.bot, 'get_file') assert check_defaults_handling(document.get_file, document.bot) monkeypatch.setattr(document.bot, 'get_file', make_assertion) assert document.get_file() 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) python-telegram-bot-13.11/tests/test_encryptedcredentials.py000066400000000000000000000060321417656324400244230ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def encrypted_credentials(): return EncryptedCredentials( TestEncryptedCredentials.data, TestEncryptedCredentials.hash, TestEncryptedCredentials.secret, ) class TestEncryptedCredentials: data = 'data' hash = 'hash' secret = 'secret' def test_slot_behaviour(self, encrypted_credentials, recwarn, mro_slots): inst = encrypted_credentials for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.data = 'should give warning', self.data assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_encryptedpassportelement.py000066400000000000000000000106671417656324400253640ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, PassportFile, PassportElementError @pytest.fixture(scope='class') def encrypted_passport_element(): return EncryptedPassportElement( TestEncryptedPassportElement.type_, data=TestEncryptedPassportElement.data, phone_number=TestEncryptedPassportElement.phone_number, email=TestEncryptedPassportElement.email, files=TestEncryptedPassportElement.files, front_side=TestEncryptedPassportElement.front_side, reverse_side=TestEncryptedPassportElement.reverse_side, selfie=TestEncryptedPassportElement.selfie, ) class TestEncryptedPassportElement: type_ = 'type' data = 'data' phone_number = 'phone_number' email = 'email' files = [PassportFile('file_id', 50, 0)] front_side = PassportFile('file_id', 50, 0) reverse_side = PassportFile('file_id', 50, 0) selfie = PassportFile('file_id', 50, 0) def test_slot_behaviour(self, encrypted_passport_element, recwarn, mro_slots): inst = encrypted_passport_element for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.phone_number = 'should give warning', self.phone_number assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_expected_values(self, encrypted_passport_element): assert encrypted_passport_element.type == self.type_ 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 == 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_equality(self): a = EncryptedPassportElement(self.type_, data=self.data) b = EncryptedPassportElement(self.type_, 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-13.11/tests/test_error.py000066400000000000000000000140561417656324400213460ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 import TelegramError, TelegramDecryptionError from telegram.error import ( Unauthorized, InvalidToken, NetworkError, BadRequest, TimedOut, ChatMigrated, RetryAfter, Conflict, ) from telegram.ext.callbackdatacache import InvalidCallbackData 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(Unauthorized, match="test message"): raise Unauthorized("test message") with pytest.raises(Unauthorized, match="^Test message$"): raise Unauthorized("Error: test message") with pytest.raises(Unauthorized, match="^Test message$"): raise Unauthorized("[Error]: test message") with pytest.raises(Unauthorized, match="^Test message$"): raise Unauthorized("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="Group migrated to supergroup. New chat id: 1234"): raise ChatMigrated(1234) try: raise ChatMigrated(1234) except ChatMigrated as e: assert e.new_chat_id == 1234 def test_retry_after(self): with pytest.raises(RetryAfter, match="Flood control exceeded. Retry in 12.0 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"]), (Unauthorized("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"]), (TelegramDecryptionError("test message"), ["message"]), (InvalidCallbackData('test data'), ['callback_data']), ], ) 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) def test_pickling_test_coverage(self): """ This test is only here to make sure that new errors will override __reduce__ properly. Add the new error class to the below covered_subclasses dict, if it's covered in the above test_errors_pickling test. """ 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: { Unauthorized, InvalidToken, NetworkError, ChatMigrated, RetryAfter, Conflict, TelegramDecryptionError, InvalidCallbackData, }, NetworkError: {BadRequest, TimedOut}, } ) make_assertion(TelegramError) python-telegram-bot-13.11/tests/test_file.py000066400000000000000000000173051417656324400211340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import File, TelegramError, Voice @pytest.fixture(scope='class') def file(bot): return File( TestFile.file_id, TestFile.file_unique_id, file_path=TestFile.file_path, file_size=TestFile.file_size, bot=bot, ) @pytest.fixture(scope='class') def local_file(bot): return File( TestFile.file_id, TestFile.file_unique_id, file_path=str(Path.cwd() / 'tests' / 'data' / 'local_file.txt'), file_size=TestFile.file_size, bot=bot, ) class TestFile: 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. def test_slot_behaviour(self, file, recwarn, mro_slots): for attr in file.__slots__: assert getattr(file, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not file.__dict__, f"got missing slot(s): {file.__dict__}" assert len(mro_slots(file)) == len(set(mro_slots(file))), "duplicate slot" file.custom, file.file_id = 'should give warning', self.file_id assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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 @flaky(3, 1) def test_error_get_empty_file_id(self, bot): with pytest.raises(TelegramError): bot.get_file(file_id='') def test_download_mutuall_exclusive(self, file): with pytest.raises(ValueError, match='custom_path and out are mutually exclusive'): file.download('custom_path', 'out') def test_download(self, monkeypatch, file): def test(*args, **kwargs): return self.file_content monkeypatch.setattr('telegram.utils.request.Request.retrieve', test) out_file = file.download() try: with open(out_file, 'rb') as fobj: assert fobj.read() == self.file_content finally: os.unlink(out_file) def test_download_local_file(self, local_file): assert local_file.download() == local_file.file_path def test_download_custom_path(self, monkeypatch, file): def test(*args, **kwargs): return self.file_content monkeypatch.setattr('telegram.utils.request.Request.retrieve', test) file_handle, custom_path = mkstemp() try: out_file = file.download(custom_path) assert out_file == custom_path with open(out_file, 'rb') as fobj: assert fobj.read() == self.file_content finally: os.close(file_handle) os.unlink(custom_path) def test_download_custom_path_local_file(self, local_file): file_handle, custom_path = mkstemp() try: out_file = local_file.download(custom_path) assert out_file == custom_path with open(out_file, 'rb') as fobj: assert fobj.read() == self.file_content finally: os.close(file_handle) os.unlink(custom_path) def test_download_no_filename(self, monkeypatch, file): def test(*args, **kwargs): return self.file_content file.file_path = None monkeypatch.setattr('telegram.utils.request.Request.retrieve', test) out_file = file.download() assert out_file[-len(file.file_id) :] == file.file_id try: with open(out_file, 'rb') as fobj: assert fobj.read() == self.file_content finally: os.unlink(out_file) def test_download_file_obj(self, monkeypatch, file): def test(*args, **kwargs): return self.file_content monkeypatch.setattr('telegram.utils.request.Request.retrieve', test) with TemporaryFile() as custom_fobj: out_fobj = file.download(out=custom_fobj) assert out_fobj is custom_fobj out_fobj.seek(0) assert out_fobj.read() == self.file_content def test_download_file_obj_local_file(self, local_file): with TemporaryFile() as custom_fobj: out_fobj = local_file.download(out=custom_fobj) assert out_fobj is custom_fobj out_fobj.seek(0) assert out_fobj.read() == self.file_content def test_download_bytearray(self, monkeypatch, file): def test(*args, **kwargs): return self.file_content monkeypatch.setattr('telegram.utils.request.Request.retrieve', test) # Check that a download to a newly allocated bytearray works. buf = 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 = file.download_as_bytearray(buf=buf2) assert buf3 is buf2 assert buf2[len(buf) :] == buf assert buf2[: len(buf)] == buf def test_download_bytearray_local_file(self, local_file): # Check that a download to a newly allocated bytearray works. buf = 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 = local_file.download_as_bytearray(buf=buf2) assert buf3 is buf2 assert buf2[len(buf) :] == buf assert buf2[: len(buf)] == buf 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) python-telegram-bot-13.11/tests/test_filters.py000066400000000000000000002544561417656324400216770ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 Message, User, Chat, MessageEntity, Document, Update, Dice from telegram.ext import Filters, BaseFilter, MessageFilter, UpdateFilter from sys import version_info as py_ver import inspect import re from telegram.utils.deprecate import TelegramDeprecationWarning @pytest.fixture(scope='function') def update(): return 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_from=User(0, "HAL9000", False), forward_from_chat=Chat(0, "Channel"), ), ) @pytest.fixture(scope='function', 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': MessageFilter}, {'class': UpdateFilter}], ids=['MessageFilter', 'UpdateFilter'], ) def base_class(request): return request.param['class'] class TestFilters: def test_all_filters_slot_behaviour(self, recwarn, mro_slots): """ 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. """ # The total no. of filters excluding filters defined in __all__ is about 70 as of 16/2/21. # Gather all the filters to test using DFS- visited = [] classes = inspect.getmembers(Filters, predicate=inspect.isclass) # 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__), ): 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 if inspect.isabstract(cls) or name in {'__class__', '__base__'}: continue assert '__slots__' in cls.__dict__, f"Filter {name!r} doesn't have __slots__" # get no. of args minus the 'self' argument args = len(inspect.signature(cls.__init__).parameters) - 1 if cls.__base__.__name__ == '_ChatUserBaseFilter': # Special case, only 1 arg needed inst = cls('1') else: inst = cls() if args < 1 else cls(*['blah'] * args) # unpack variable no. of args for attr in cls.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}' for {name}" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__} for {name}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), f"same slot in {name}" with pytest.warns(TelegramDeprecationWarning, match='custom attributes') as warn: inst.custom = 'should give warning' if not warn: pytest.fail(f"Filter {name!r} didn't warn when setting custom attr") assert '__dict__' not in BaseFilter.__slots__ if py_ver < (3, 7) else True, 'dict in abc' class CustomFilter(MessageFilter): def filter(self, message: Message): pass with pytest.warns(None): CustomFilter().custom = 'allowed' # Test setting custom attr to custom filters with pytest.warns(TelegramDeprecationWarning, match='custom attributes'): Filters().custom = 'raise warning' def test_filters_all(self, update): assert Filters.all(update) def test_filters_text(self, update): update.message.text = 'test' assert (Filters.text)(update) update.message.text = '/test' assert (Filters.text)(update) def test_filters_text_strings(self, update): update.message.text = '/test' assert Filters.text({'/test', 'test1'})(update) assert not Filters.text(['test1', 'test2'])(update) def test_filters_caption(self, update): update.message.caption = 'test' assert (Filters.caption)(update) update.message.caption = None assert not (Filters.caption)(update) def test_filters_caption_strings(self, update): update.message.caption = 'test' assert Filters.caption({'test', 'test1'})(update) assert not Filters.caption(['test1', 'test2'])(update) def test_filters_command_default(self, update): update.message.text = 'test' assert not Filters.command(update) update.message.text = '/test' assert not Filters.command(update) # Only accept commands at the beginning update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 3, 5)] assert not Filters.command(update) update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] assert Filters.command(update) def test_filters_command_anywhere(self, update): update.message.text = 'test /cmd' assert not (Filters.command(False))(update) update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 5, 4)] assert (Filters.command(False))(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')(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')(update) update.message.text = 'test' assert not Filters.regex(r'fail')(update) assert Filters.regex(r'test')(update) assert Filters.regex(re.compile(r'test'))(update) assert Filters.regex(re.compile(r'TEST', re.IGNORECASE))(update) update.message.text = 'i love python' assert Filters.regex(r'.\b[lo]{2}ve python')(update) update.message.text = None assert not Filters.regex(r'fail')(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'))(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'))(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'))(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'))(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'))(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)(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)(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'))(update) assert result is True def test_regex_complex_merges(self, update): SRE_TYPE = type(re.match("", "")) update.message.text = 'test it out' test_filter = Filters.regex('test') & ( (Filters.status_update | Filters.forwarded) | Filters.regex('out') ) result = test_filter(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.forward_date = datetime.datetime.utcnow() result = test_filter(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(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.forward_date = None result = test_filter(update) assert not result update.message.text = 'test it out' result = test_filter(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(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(update) assert not result update.message.text = 'test it out' update.message.forward_date = None update.message.pinned_message = None test_filter = (Filters.regex('test') | Filters.command) & ( Filters.regex('it') | Filters.status_update ) result = test_filter(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(update) assert not result update.message.pinned_message = True result = test_filter(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(update) assert not result update.message.text = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = test_filter(update) assert result assert isinstance(result, bool) update.message.text = '/start it' result = test_filter(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)] filter = ~Filters.regex(r'deep-linked param') result = filter(update) assert not result update.message.text = 'not it' result = filter(update) assert result assert isinstance(result, bool) filter = ~Filters.regex('linked') & Filters.command update.message.text = "it's linked" result = filter(update) assert not result update.message.text = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = filter(update) assert result update.message.text = '/linked' result = filter(update) assert not result filter = ~Filters.regex('linked') | Filters.command update.message.text = "it's linked" update.message.entities = [] result = filter(update) assert not result update.message.text = '/start linked' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = filter(update) assert result update.message.text = '/start' result = filter(update) assert result update.message.text = 'nothig' update.message.entities = [] result = filter(update) assert result def test_filters_caption_regex(self, update): SRE_TYPE = type(re.match("", "")) update.message.caption = '/start deep-linked param' result = Filters.caption_regex(r'deep-linked param')(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.caption_regex(r'help')(update) update.message.caption = 'test' assert not Filters.caption_regex(r'fail')(update) assert Filters.caption_regex(r'test')(update) assert Filters.caption_regex(re.compile(r'test'))(update) assert Filters.caption_regex(re.compile(r'TEST', re.IGNORECASE))(update) update.message.caption = 'i love python' assert Filters.caption_regex(r'.\b[lo]{2}ve python')(update) update.message.caption = None assert not Filters.caption_regex(r'fail')(update) def test_filters_caption_regex_multiple(self, update): SRE_TYPE = type(re.match("", "")) update.message.caption = '/start deep-linked param' result = (Filters.caption_regex('deep') & Filters.caption_regex(r'linked param'))(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.caption_regex('deep') | Filters.caption_regex(r'linked param'))(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.caption_regex('not int') | Filters.caption_regex(r'linked param'))( 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.caption_regex('not int') & Filters.caption_regex(r'linked param'))( 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.caption_regex(r'linked param'))(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.caption_regex(r'linked param') & Filters.command)(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.caption_regex(r'linked param') | Filters.command)(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.caption_regex(r'linked param'))(update) assert result is True def test_caption_regex_complex_merges(self, update): SRE_TYPE = type(re.match("", "")) update.message.caption = 'test it out' test_filter = Filters.caption_regex('test') & ( (Filters.status_update | Filters.forwarded) | Filters.caption_regex('out') ) result = test_filter(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.forward_date = datetime.datetime.utcnow() result = test_filter(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(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.forward_date = None result = test_filter(update) assert not result update.message.caption = 'test it out' result = test_filter(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(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(update) assert not result update.message.caption = 'test it out' update.message.forward_date = None update.message.pinned_message = None test_filter = (Filters.caption_regex('test') | Filters.command) & ( Filters.caption_regex('it') | Filters.status_update ) result = test_filter(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(update) assert not result update.message.pinned_message = True result = test_filter(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(update) assert not result update.message.caption = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = test_filter(update) assert result assert isinstance(result, bool) update.message.caption = '/start it' result = test_filter(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.caption_regex(r'deep-linked param') result = test_filter(update) assert not result update.message.caption = 'not it' result = test_filter(update) assert result assert isinstance(result, bool) test_filter = ~Filters.caption_regex('linked') & Filters.command update.message.caption = "it's linked" result = test_filter(update) assert not result update.message.caption = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = test_filter(update) assert result update.message.caption = '/linked' result = test_filter(update) assert not result test_filter = ~Filters.caption_regex('linked') | Filters.command update.message.caption = "it's linked" update.message.entities = [] result = test_filter(update) assert not result update.message.caption = '/start linked' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = test_filter(update) assert result update.message.caption = '/start' result = test_filter(update) assert result update.message.caption = 'nothig' update.message.entities = [] result = test_filter(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(update) update.message.reply_to_message = another_message assert Filters.reply(update) def test_filters_audio(self, update): assert not Filters.audio(update) update.message.audio = 'test' assert Filters.audio(update) def test_filters_document(self, update): assert not Filters.document(update) update.message.document = 'test' assert Filters.document(update) def test_filters_document_type(self, update): update.message.document = Document( "file_id", 'unique_id', mime_type="application/vnd.android.package-archive" ) assert Filters.document.apk(update) assert Filters.document.application(update) assert not Filters.document.doc(update) assert not Filters.document.audio(update) update.message.document.mime_type = "application/msword" assert Filters.document.doc(update) assert Filters.document.application(update) assert not Filters.document.docx(update) assert not Filters.document.audio(update) update.message.document.mime_type = ( "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ) assert Filters.document.docx(update) assert Filters.document.application(update) assert not Filters.document.exe(update) assert not Filters.document.audio(update) update.message.document.mime_type = "application/x-ms-dos-executable" assert Filters.document.exe(update) assert Filters.document.application(update) assert not Filters.document.docx(update) assert not Filters.document.audio(update) update.message.document.mime_type = "video/mp4" assert Filters.document.gif(update) assert Filters.document.video(update) assert not Filters.document.jpg(update) assert not Filters.document.text(update) update.message.document.mime_type = "image/jpeg" assert Filters.document.jpg(update) assert Filters.document.image(update) assert not Filters.document.mp3(update) assert not Filters.document.video(update) update.message.document.mime_type = "audio/mpeg" assert Filters.document.mp3(update) assert Filters.document.audio(update) assert not Filters.document.pdf(update) assert not Filters.document.image(update) update.message.document.mime_type = "application/pdf" assert Filters.document.pdf(update) assert Filters.document.application(update) assert not Filters.document.py(update) assert not Filters.document.audio(update) update.message.document.mime_type = "text/x-python" assert Filters.document.py(update) assert Filters.document.text(update) assert not Filters.document.svg(update) assert not Filters.document.application(update) update.message.document.mime_type = "image/svg+xml" assert Filters.document.svg(update) assert Filters.document.image(update) assert not Filters.document.txt(update) assert not Filters.document.video(update) update.message.document.mime_type = "text/plain" assert Filters.document.txt(update) assert Filters.document.text(update) assert not Filters.document.targz(update) assert not Filters.document.application(update) update.message.document.mime_type = "application/x-compressed-tar" assert Filters.document.targz(update) assert Filters.document.application(update) assert not Filters.document.wav(update) assert not Filters.document.audio(update) update.message.document.mime_type = "audio/x-wav" assert Filters.document.wav(update) assert Filters.document.audio(update) assert not Filters.document.xml(update) assert not Filters.document.image(update) update.message.document.mime_type = "application/xml" assert Filters.document.xml(update) assert Filters.document.application(update) assert not Filters.document.zip(update) assert not Filters.document.audio(update) update.message.document.mime_type = "application/zip" assert Filters.document.zip(update) assert Filters.document.application(update) assert not Filters.document.apk(update) assert not Filters.document.audio(update) update.message.document.mime_type = "image/x-rgb" assert not Filters.document.category("application/")(update) assert not Filters.document.mime_type("application/x-sh")(update) update.message.document.mime_type = "application/x-sh" assert Filters.document.category("application/")(update) assert Filters.document.mime_type("application/x-sh")(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", ) assert Filters.document.file_extension("jpg")(update) assert not Filters.document.file_extension("jpeg")(update) assert not Filters.document.file_extension("file.jpg")(update) update.message.document.file_name = "file.tar.gz" assert Filters.document.file_extension("tar.gz")(update) assert Filters.document.file_extension("gz")(update) assert not Filters.document.file_extension("tgz")(update) assert not Filters.document.file_extension("jpg")(update) update.message.document = None assert not Filters.document.file_extension("jpg")(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", ) assert not Filters.document.file_extension(".jpg")(update) assert not Filters.document.file_extension("e.jpg")(update) assert not Filters.document.file_extension("file.jpg")(update) assert not Filters.document.file_extension("")(update) update.message.document.file_name = "file..jpg" assert Filters.document.file_extension("jpg")(update) assert Filters.document.file_extension(".jpg")(update) assert not Filters.document.file_extension("..jpg")(update) update.message.document.file_name = "file.docx" assert Filters.document.file_extension("docx")(update) assert not Filters.document.file_extension("doc")(update) assert not Filters.document.file_extension("ocx")(update) update.message.document.file_name = "file" assert not Filters.document.file_extension("")(update) assert not Filters.document.file_extension("file")(update) update.message.document.file_name = "file." assert Filters.document.file_extension("")(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", ) assert not Filters.document.file_extension(None)(update) update.message.document.file_name = "file" assert Filters.document.file_extension(None)(update) assert not Filters.document.file_extension("None")(update) update.message.document.file_name = "file." assert not Filters.document.file_extension(None)(update) update.message.document = None assert not Filters.document.file_extension(None)(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", ) assert Filters.document.file_extension("JPG")(update) assert Filters.document.file_extension("jpG")(update) update.message.document.file_name = "file.JPG" assert Filters.document.file_extension("jpg")(update) assert not Filters.document.file_extension("jpg", case_sensitive=True)(update) update.message.document.file_name = "file.Dockerfile" assert Filters.document.file_extension("Dockerfile", case_sensitive=True)(update) assert not Filters.document.file_extension("DOCKERFILE", case_sensitive=True)(update) def test_filters_file_extension_name(self): assert Filters.document.file_extension("jpg").name == ( "Filters.document.file_extension('jpg')" ) assert Filters.document.file_extension("JPG").name == ( "Filters.document.file_extension('jpg')" ) assert Filters.document.file_extension("jpg", case_sensitive=True).name == ( "Filters.document.file_extension('jpg', case_sensitive=True)" ) assert Filters.document.file_extension("JPG", case_sensitive=True).name == ( "Filters.document.file_extension('JPG', case_sensitive=True)" ) assert Filters.document.file_extension(".jpg").name == ( "Filters.document.file_extension('.jpg')" ) assert Filters.document.file_extension("").name == "Filters.document.file_extension('')" assert ( Filters.document.file_extension(None).name == "Filters.document.file_extension(None)" ) def test_filters_animation(self, update): assert not Filters.animation(update) update.message.animation = 'test' assert Filters.animation(update) def test_filters_photo(self, update): assert not Filters.photo(update) update.message.photo = 'test' assert Filters.photo(update) def test_filters_sticker(self, update): assert not Filters.sticker(update) update.message.sticker = 'test' assert Filters.sticker(update) def test_filters_video(self, update): assert not Filters.video(update) update.message.video = 'test' assert Filters.video(update) def test_filters_voice(self, update): assert not Filters.voice(update) update.message.voice = 'test' assert Filters.voice(update) def test_filters_video_note(self, update): assert not Filters.video_note(update) update.message.video_note = 'test' assert Filters.video_note(update) def test_filters_contact(self, update): assert not Filters.contact(update) update.message.contact = 'test' assert Filters.contact(update) def test_filters_location(self, update): assert not Filters.location(update) update.message.location = 'test' assert Filters.location(update) def test_filters_venue(self, update): assert not Filters.venue(update) update.message.venue = 'test' assert Filters.venue(update) def test_filters_status_update(self, update): assert not Filters.status_update(update) update.message.new_chat_members = ['test'] assert Filters.status_update(update) assert Filters.status_update.new_chat_members(update) update.message.new_chat_members = None update.message.left_chat_member = 'test' assert Filters.status_update(update) assert Filters.status_update.left_chat_member(update) update.message.left_chat_member = None update.message.new_chat_title = 'test' assert Filters.status_update(update) assert Filters.status_update.new_chat_title(update) update.message.new_chat_title = '' update.message.new_chat_photo = 'test' assert Filters.status_update(update) assert Filters.status_update.new_chat_photo(update) update.message.new_chat_photo = None update.message.delete_chat_photo = True assert Filters.status_update(update) assert Filters.status_update.delete_chat_photo(update) update.message.delete_chat_photo = False update.message.group_chat_created = True assert Filters.status_update(update) assert Filters.status_update.chat_created(update) update.message.group_chat_created = False update.message.supergroup_chat_created = True assert Filters.status_update(update) assert Filters.status_update.chat_created(update) update.message.supergroup_chat_created = False update.message.channel_chat_created = True assert Filters.status_update(update) assert Filters.status_update.chat_created(update) update.message.channel_chat_created = False update.message.message_auto_delete_timer_changed = True assert Filters.status_update(update) assert Filters.status_update.message_auto_delete_timer_changed(update) update.message.message_auto_delete_timer_changed = False update.message.migrate_to_chat_id = 100 assert Filters.status_update(update) assert Filters.status_update.migrate(update) update.message.migrate_to_chat_id = 0 update.message.migrate_from_chat_id = 100 assert Filters.status_update(update) assert Filters.status_update.migrate(update) update.message.migrate_from_chat_id = 0 update.message.pinned_message = 'test' assert Filters.status_update(update) assert Filters.status_update.pinned_message(update) update.message.pinned_message = None update.message.connected_website = 'http://example.com/' assert Filters.status_update(update) assert Filters.status_update.connected_website(update) update.message.connected_website = None update.message.proximity_alert_triggered = 'alert' assert Filters.status_update(update) assert Filters.status_update.proximity_alert_triggered(update) update.message.proximity_alert_triggered = None update.message.voice_chat_scheduled = 'scheduled' assert Filters.status_update(update) assert Filters.status_update.voice_chat_scheduled(update) update.message.voice_chat_scheduled = None update.message.voice_chat_started = 'hello' assert Filters.status_update(update) assert Filters.status_update.voice_chat_started(update) update.message.voice_chat_started = None update.message.voice_chat_ended = 'bye' assert Filters.status_update(update) assert Filters.status_update.voice_chat_ended(update) update.message.voice_chat_ended = None update.message.voice_chat_participants_invited = 'invited' assert Filters.status_update(update) assert Filters.status_update.voice_chat_participants_invited(update) update.message.voice_chat_participants_invited = None def test_filters_forwarded(self, update): assert not Filters.forwarded(update) update.message.forward_date = datetime.datetime.utcnow() assert Filters.forwarded(update) def test_filters_game(self, update): assert not Filters.game(update) update.message.game = 'test' assert Filters.game(update) def test_entities_filter(self, update, message_entity): update.message.entities = [message_entity] assert Filters.entity(message_entity.type)(update) update.message.entities = [] assert not Filters.entity(MessageEntity.MENTION)(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)(update) assert not Filters.caption_entity(message_entity.type)(update) def test_caption_entities_filter(self, update, message_entity): update.message.caption_entities = [message_entity] assert Filters.caption_entity(message_entity.type)(update) update.message.caption_entities = [] assert not Filters.caption_entity(MessageEntity.MENTION)(update) second = message_entity.to_dict() second['type'] = 'bold' second = MessageEntity.de_json(second, None) update.message.caption_entities = [message_entity, second] assert Filters.caption_entity(message_entity.type)(update) assert not Filters.entity(message_entity.type)(update) def test_private_filter(self, update): assert Filters.private(update) update.message.chat.type = 'group' assert not Filters.private(update) def test_private_filter_deprecation(self, update): with pytest.warns(TelegramDeprecationWarning): Filters.private(update) def test_group_filter(self, update): assert not Filters.group(update) update.message.chat.type = 'group' assert Filters.group(update) update.message.chat.type = 'supergroup' assert Filters.group(update) def test_group_filter_deprecation(self, update): with pytest.warns(TelegramDeprecationWarning): Filters.group(update) @pytest.mark.parametrize( ('chat_type, results'), [ (None, (False, False, False, False, False, False)), (Chat.PRIVATE, (True, True, False, False, False, False)), (Chat.GROUP, (True, False, True, False, True, False)), (Chat.SUPERGROUP, (True, False, False, True, True, False)), (Chat.CHANNEL, (True, False, False, False, False, True)), ], ) def test_filters_chat_types(self, update, chat_type, results): update.message.chat.type = chat_type assert Filters.chat_type(update) is results[0] assert Filters.chat_type.private(update) is results[1] assert Filters.chat_type.group(update) is results[2] assert Filters.chat_type.supergroup(update) is results[3] assert Filters.chat_type.groups(update) is results[4] assert Filters.chat_type.channel(update) is results[5] 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()(update) assert Filters.user(allow_empty=True)(update) def test_filters_user_id(self, update): assert not Filters.user(user_id=1)(update) update.message.from_user.id = 1 assert Filters.user(user_id=1)(update) update.message.from_user.id = 2 assert Filters.user(user_id=[1, 2])(update) assert not Filters.user(user_id=[3, 4])(update) update.message.from_user = None assert not Filters.user(user_id=[3, 4])(update) def test_filters_username(self, update): assert not Filters.user(username='user')(update) assert not Filters.user(username='Testuser')(update) update.message.from_user.username = 'user@' assert Filters.user(username='@user@')(update) assert Filters.user(username='user@')(update) assert Filters.user(username=['user1', 'user@', 'user2'])(update) assert not Filters.user(username=['@username', '@user_2'])(update) update.message.from_user = None assert not Filters.user(username=['@username', '@user_2'])(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(update) update.message.from_user.id = 2 assert not f(update) f.user_ids = 2 assert f.user_ids == {2} assert f(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(update) update.message.from_user.username = 'User' assert not f(update) f.usernames = 'User' assert f(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(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(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(update) f.add_user_ids(1) f.add_user_ids([2, 3]) for user in users: update.message.from_user.username = user assert f(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(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(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(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(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) and 'foobar' in str(f) with pytest.raises(RuntimeError, match='Cannot set name'): f.name = 'foo' 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()(update) assert Filters.chat(allow_empty=True)(update) def test_filters_chat_id(self, update): assert not Filters.chat(chat_id=1)(update) update.message.chat.id = 1 assert Filters.chat(chat_id=1)(update) update.message.chat.id = 2 assert Filters.chat(chat_id=[1, 2])(update) assert not Filters.chat(chat_id=[3, 4])(update) update.message.chat = None assert not Filters.chat(chat_id=[3, 4])(update) def test_filters_chat_username(self, update): assert not Filters.chat(username='chat')(update) assert not Filters.chat(username='Testchat')(update) update.message.chat.username = 'chat@' assert Filters.chat(username='@chat@')(update) assert Filters.chat(username='chat@')(update) assert Filters.chat(username=['chat1', 'chat@', 'chat2'])(update) assert not Filters.chat(username=['@username', '@chat_2'])(update) update.message.chat = None assert not Filters.chat(username=['@username', '@chat_2'])(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(update) update.message.chat.id = 2 assert not f(update) f.chat_ids = 2 assert f.chat_ids == {2} assert f(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(update) update.message.chat.username = 'User' assert not f(update) f.usernames = 'User' assert f(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(update) f.add_usernames('chat_a') f.add_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.chat.username = chat assert f(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(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.chat.username = chat assert f(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(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(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(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.chat.username = chat assert not f(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) and '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.forwarded_from(chat_id=1, username='chat') def test_filters_forwarded_from_allow_empty(self, update): assert not Filters.forwarded_from()(update) assert Filters.forwarded_from(allow_empty=True)(update) def test_filters_forwarded_from_id(self, update): # Test with User id- assert not Filters.forwarded_from(chat_id=1)(update) update.message.forward_from.id = 1 assert Filters.forwarded_from(chat_id=1)(update) update.message.forward_from.id = 2 assert Filters.forwarded_from(chat_id=[1, 2])(update) assert not Filters.forwarded_from(chat_id=[3, 4])(update) update.message.forward_from = None assert not Filters.forwarded_from(chat_id=[3, 4])(update) # Test with Chat id- update.message.forward_from_chat.id = 4 assert Filters.forwarded_from(chat_id=[4])(update) assert Filters.forwarded_from(chat_id=[3, 4])(update) update.message.forward_from_chat.id = 2 assert not Filters.forwarded_from(chat_id=[3, 4])(update) assert Filters.forwarded_from(chat_id=2)(update) def test_filters_forwarded_from_username(self, update): # For User username assert not Filters.forwarded_from(username='chat')(update) assert not Filters.forwarded_from(username='Testchat')(update) update.message.forward_from.username = 'chat@' assert Filters.forwarded_from(username='@chat@')(update) assert Filters.forwarded_from(username='chat@')(update) assert Filters.forwarded_from(username=['chat1', 'chat@', 'chat2'])(update) assert not Filters.forwarded_from(username=['@username', '@chat_2'])(update) update.message.forward_from = None assert not Filters.forwarded_from(username=['@username', '@chat_2'])(update) # For Chat username assert not Filters.forwarded_from(username='chat')(update) assert not Filters.forwarded_from(username='Testchat')(update) update.message.forward_from_chat.username = 'chat@' assert Filters.forwarded_from(username='@chat@')(update) assert Filters.forwarded_from(username='chat@')(update) assert Filters.forwarded_from(username=['chat1', 'chat@', 'chat2'])(update) assert not Filters.forwarded_from(username=['@username', '@chat_2'])(update) update.message.forward_from_chat = None assert not Filters.forwarded_from(username=['@username', '@chat_2'])(update) def test_filters_forwarded_from_change_id(self, update): f = Filters.forwarded_from(chat_id=1) # For User ids- assert f.chat_ids == {1} update.message.forward_from.id = 1 assert f(update) update.message.forward_from.id = 2 assert not f(update) f.chat_ids = 2 assert f.chat_ids == {2} assert f(update) # For Chat ids- f = Filters.forwarded_from(chat_id=1) # reset this update.message.forward_from = None # and change this to None, only one of them can be True assert f.chat_ids == {1} update.message.forward_from_chat.id = 1 assert f(update) update.message.forward_from_chat.id = 2 assert not f(update) f.chat_ids = 2 assert f.chat_ids == {2} assert f(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.forwarded_from(username='chat') update.message.forward_from.username = 'chat' assert f(update) update.message.forward_from.username = 'User' assert not f(update) f.usernames = 'User' assert f(update) # For Chat usernames update.message.forward_from = None f = Filters.forwarded_from(username='chat') update.message.forward_from_chat.username = 'chat' assert f(update) update.message.forward_from_chat.username = 'User' assert not f(update) f.usernames = 'User' assert f(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.forwarded_from() # For User usernames for chat in chats: update.message.forward_from.username = chat assert not f(update) f.add_usernames('chat_a') f.add_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.forward_from.username = chat assert f(update) # For Chat usernames update.message.forward_from = None f = Filters.forwarded_from() for chat in chats: update.message.forward_from_chat.username = chat assert not f(update) f.add_usernames('chat_a') f.add_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.forward_from_chat.username = chat assert f(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.forwarded_from() # For User ids for chat in chats: update.message.forward_from.id = chat assert not f(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.forward_from.username = chat assert f(update) # For Chat ids- update.message.forward_from = None f = Filters.forwarded_from() for chat in chats: update.message.forward_from_chat.id = chat assert not f(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.forward_from_chat.username = chat assert f(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.forwarded_from(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_from.username = chat assert f(update) f.remove_usernames('chat_a') f.remove_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.forward_from.username = chat assert not f(update) # For Chat usernames update.message.forward_from = None f = Filters.forwarded_from(username=chats) for chat in chats: update.message.forward_from_chat.username = chat assert f(update) f.remove_usernames('chat_a') f.remove_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.forward_from_chat.username = chat assert not f(update) def test_filters_forwarded_from_remove_chat_by_id(self, update): chats = [1, 2, 3] f = Filters.forwarded_from(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_from.id = chat assert f(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.forward_from.username = chat assert not f(update) # For Chat ids update.message.forward_from = None f = Filters.forwarded_from(chat_id=chats) for chat in chats: update.message.forward_from_chat.id = chat assert f(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.forward_from_chat.username = chat assert not f(update) def test_filters_forwarded_from_repr(self): f = Filters.forwarded_from([1, 2]) assert str(f) == 'Filters.forwarded_from(1, 2)' f.remove_chat_ids(1) f.remove_chat_ids(2) assert str(f) == 'Filters.forwarded_from()' f.add_usernames('@foobar') assert str(f) == 'Filters.forwarded_from(foobar)' f.add_usernames('@barfoo') assert str(f).startswith('Filters.forwarded_from(') # we don't know the exact order assert 'barfoo' in str(f) and '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.sender_chat(chat_id=1, username='chat') def test_filters_sender_chat_allow_empty(self, update): assert not Filters.sender_chat()(update) assert Filters.sender_chat(allow_empty=True)(update) def test_filters_sender_chat_id(self, update): assert not Filters.sender_chat(chat_id=1)(update) update.message.sender_chat.id = 1 assert Filters.sender_chat(chat_id=1)(update) update.message.sender_chat.id = 2 assert Filters.sender_chat(chat_id=[1, 2])(update) assert not Filters.sender_chat(chat_id=[3, 4])(update) update.message.sender_chat = None assert not Filters.sender_chat(chat_id=[3, 4])(update) def test_filters_sender_chat_username(self, update): assert not Filters.sender_chat(username='chat')(update) assert not Filters.sender_chat(username='Testchat')(update) update.message.sender_chat.username = 'chat@' assert Filters.sender_chat(username='@chat@')(update) assert Filters.sender_chat(username='chat@')(update) assert Filters.sender_chat(username=['chat1', 'chat@', 'chat2'])(update) assert not Filters.sender_chat(username=['@username', '@chat_2'])(update) update.message.sender_chat = None assert not Filters.sender_chat(username=['@username', '@chat_2'])(update) def test_filters_sender_chat_change_id(self, update): f = Filters.sender_chat(chat_id=1) assert f.chat_ids == {1} update.message.sender_chat.id = 1 assert f(update) update.message.sender_chat.id = 2 assert not f(update) f.chat_ids = 2 assert f.chat_ids == {2} assert f(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.usernames = 'chat' def test_filters_sender_chat_change_username(self, update): f = Filters.sender_chat(username='chat') update.message.sender_chat.username = 'chat' assert f(update) update.message.sender_chat.username = 'User' assert not f(update) f.usernames = 'User' assert f(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.sender_chat() for chat in chats: update.message.sender_chat.username = chat assert not f(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(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.sender_chat() for chat in chats: update.message.sender_chat.id = chat assert not f(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.sender_chat.username = chat assert f(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.sender_chat(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(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(update) def test_filters_sender_chat_remove_sender_chat_by_id(self, update): chats = [1, 2, 3] f = Filters.sender_chat(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(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(update) def test_filters_sender_chat_repr(self): f = Filters.sender_chat([1, 2]) assert str(f) == 'Filters.sender_chat(1, 2)' f.remove_chat_ids(1) f.remove_chat_ids(2) assert str(f) == 'Filters.sender_chat()' f.add_usernames('@foobar') assert str(f) == 'Filters.sender_chat(foobar)' f.add_usernames('@barfoo') assert str(f).startswith('Filters.sender_chat(') # we don't know th exact order assert 'barfoo' in str(f) and '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.sender_chat.super_group(update) update.message.sender_chat.type = Chat.CHANNEL assert not Filters.sender_chat.super_group(update) update.message.sender_chat.type = Chat.SUPERGROUP assert Filters.sender_chat.super_group(update) update.message.sender_chat = None assert not Filters.sender_chat.super_group(update) def test_filters_sender_chat_channel(self, update): update.message.sender_chat.type = Chat.PRIVATE assert not Filters.sender_chat.channel(update) update.message.sender_chat.type = Chat.SUPERGROUP assert not Filters.sender_chat.channel(update) update.message.sender_chat.type = Chat.CHANNEL assert Filters.sender_chat.channel(update) update.message.sender_chat = None assert not Filters.sender_chat.channel(update) def test_filters_is_automatic_forward(self, update): assert not Filters.is_automatic_forward(update) update.message.is_automatic_forward = True assert Filters.is_automatic_forward(update) def test_filters_has_protected_content(self, update): assert not Filters.has_protected_content(update) update.message.has_protected_content = True assert Filters.has_protected_content(update) def test_filters_invoice(self, update): assert not Filters.invoice(update) update.message.invoice = 'test' assert Filters.invoice(update) def test_filters_successful_payment(self, update): assert not Filters.successful_payment(update) update.message.successful_payment = 'test' assert Filters.successful_payment(update) def test_filters_passport_data(self, update): assert not Filters.passport_data(update) update.message.passport_data = 'test' assert Filters.passport_data(update) def test_filters_poll(self, update): assert not Filters.poll(update) update.message.poll = 'test' assert Filters.poll(update) @pytest.mark.parametrize('emoji', Dice.ALL_EMOJI) def test_filters_dice(self, update, emoji): update.message.dice = Dice(4, emoji) assert Filters.dice(update) update.message.dice = None assert not Filters.dice(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)(update) update.message.dice = Dice(5, emoji) assert Filters.dice(5)(update) assert Filters.dice({5, 6})(update) assert not Filters.dice(1)(update) assert not Filters.dice([2, 3])(update) def test_filters_dice_type(self, update): update.message.dice = Dice(5, '🎲') assert Filters.dice.dice(update) assert Filters.dice.dice([4, 5])(update) assert not Filters.dice.darts(update) assert not Filters.dice.basketball(update) assert not Filters.dice.dice([6])(update) update.message.dice = Dice(5, '🎯') assert Filters.dice.darts(update) assert Filters.dice.darts([4, 5])(update) assert not Filters.dice.dice(update) assert not Filters.dice.basketball(update) assert not Filters.dice.darts([6])(update) update.message.dice = Dice(5, '🏀') assert Filters.dice.basketball(update) assert Filters.dice.basketball([4, 5])(update) assert not Filters.dice.dice(update) assert not Filters.dice.darts(update) assert not Filters.dice.basketball([4])(update) update.message.dice = Dice(5, '⚽') assert Filters.dice.football(update) assert Filters.dice.football([4, 5])(update) assert not Filters.dice.dice(update) assert not Filters.dice.darts(update) assert not Filters.dice.football([4])(update) update.message.dice = Dice(5, '🎰') assert Filters.dice.slot_machine(update) assert Filters.dice.slot_machine([4, 5])(update) assert not Filters.dice.dice(update) assert not Filters.dice.darts(update) assert not Filters.dice.slot_machine([4])(update) update.message.dice = Dice(5, '🎳') assert Filters.dice.bowling(update) assert Filters.dice.bowling([4, 5])(update) assert not Filters.dice.dice(update) assert not Filters.dice.darts(update) assert not Filters.dice.bowling([4])(update) def test_language_filter_single(self, update): update.message.from_user.language_code = 'en_US' assert (Filters.language('en_US'))(update) assert (Filters.language('en'))(update) assert not (Filters.language('en_GB'))(update) assert not (Filters.language('da'))(update) update.message.from_user.language_code = 'da' assert not (Filters.language('en_US'))(update) assert not (Filters.language('en'))(update) assert not (Filters.language('en_GB'))(update) assert (Filters.language('da'))(update) def test_language_filter_multiple(self, update): f = Filters.language(['en_US', 'da']) update.message.from_user.language_code = 'en_US' assert f(update) update.message.from_user.language_code = 'en_GB' assert not f(update) update.message.from_user.language_code = 'da' assert f(update) def test_and_filters(self, update): update.message.text = 'test' update.message.forward_date = datetime.datetime.utcnow() assert (Filters.text & Filters.forwarded)(update) update.message.text = '/test' assert (Filters.text & Filters.forwarded)(update) update.message.text = 'test' update.message.forward_date = None assert not (Filters.text & Filters.forwarded)(update) update.message.text = 'test' update.message.forward_date = datetime.datetime.utcnow() assert (Filters.text & Filters.forwarded & Filters.private)(update) def test_or_filters(self, update): update.message.text = 'test' assert (Filters.text | Filters.status_update)(update) update.message.group_chat_created = True assert (Filters.text | Filters.status_update)(update) update.message.text = None assert (Filters.text | Filters.status_update)(update) update.message.group_chat_created = False assert not (Filters.text | Filters.status_update)(update) def test_and_or_filters(self, update): update.message.text = 'test' update.message.forward_date = datetime.datetime.utcnow() assert (Filters.text & (Filters.status_update | Filters.forwarded))(update) update.message.forward_date = None assert not (Filters.text & (Filters.forwarded | Filters.status_update))(update) update.message.pinned_message = True assert Filters.text & (Filters.forwarded | Filters.status_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))(update) update.message.text = None update.effective_user.id = 1234 assert not (Filters.text ^ Filters.user(123))(update) update.message.text = 'test' assert (Filters.text ^ Filters.user(123))(update) update.message.text = None update.effective_user.id = 123 assert (Filters.text ^ Filters.user(123))(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): update.message.text = 'test' update.message.forward_date = datetime.datetime.utcnow() assert (Filters.forwarded & (Filters.text ^ Filters.user(123)))(update) update.message.text = None update.effective_user.id = 123 assert (Filters.forwarded & (Filters.text ^ Filters.user(123)))(update) update.message.text = 'test' assert not (Filters.forwarded & (Filters.text ^ Filters.user(123)))(update) update.message.forward_date = None update.message.text = None update.effective_user.id = 123 assert not (Filters.forwarded & (Filters.text ^ Filters.user(123)))(update) update.message.text = 'test' update.effective_user.id = 456 assert not (Filters.forwarded & (Filters.text ^ Filters.user(123)))(update) assert ( str(Filters.forwarded & (Filters.text ^ Filters.user(123))) == '>' ) def test_xor_regex_filters(self, update): SRE_TYPE = type(re.match("", "")) update.message.text = 'test' update.message.forward_date = datetime.datetime.utcnow() assert not (Filters.forwarded ^ Filters.regex('^test$'))(update) update.message.forward_date = None result = (Filters.forwarded ^ Filters.regex('^test$'))(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert type(matches[0]) is SRE_TYPE update.message.forward_date = datetime.datetime.utcnow() update.message.text = None assert (Filters.forwarded ^ Filters.regex('^test$'))(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(update) assert not (~Filters.command)(update) update.message.text = 'test' update.message.entities = [] assert not Filters.command(update) assert (~Filters.command)(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): update.message.text = '/test' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] update.message.forward_date = 1 assert (Filters.forwarded & Filters.command)(update) assert not (~Filters.forwarded & Filters.command)(update) assert not (Filters.forwarded & ~Filters.command)(update) assert not (~(Filters.forwarded & Filters.command))(update) update.message.forward_date = None assert not (Filters.forwarded & Filters.command)(update) assert (~Filters.forwarded & Filters.command)(update) assert not (Filters.forwarded & ~Filters.command)(update) assert (~(Filters.forwarded & Filters.command))(update) update.message.text = 'test' update.message.entities = [] assert not (Filters.forwarded & Filters.command)(update) assert not (~Filters.forwarded & Filters.command)(update) assert not (Filters.forwarded & ~Filters.command)(update) assert (~(Filters.forwarded & Filters.command))(update) def test_faulty_custom_filter(self, update): class _CustomFilter(BaseFilter): pass with pytest.raises(TypeError, match='Can\'t instantiate abstract class _CustomFilter'): _CustomFilter() def test_custom_unnamed_filter(self, update, base_class): class Unnamed(base_class): def filter(self, mes): return True unnamed = Unnamed() assert str(unnamed) == Unnamed.__name__ def test_update_type_message(self, update): assert Filters.update.message(update) assert not Filters.update.edited_message(update) assert Filters.update.messages(update) assert not Filters.update.channel_post(update) assert not Filters.update.edited_channel_post(update) assert not Filters.update.channel_posts(update) assert Filters.update(update) def test_update_type_edited_message(self, update): update.edited_message, update.message = update.message, update.edited_message assert not Filters.update.message(update) assert Filters.update.edited_message(update) assert Filters.update.messages(update) assert not Filters.update.channel_post(update) assert not Filters.update.edited_channel_post(update) assert not Filters.update.channel_posts(update) assert Filters.update(update) def test_update_type_channel_post(self, update): update.channel_post, update.message = update.message, update.edited_message assert not Filters.update.message(update) assert not Filters.update.edited_message(update) assert not Filters.update.messages(update) assert Filters.update.channel_post(update) assert not Filters.update.edited_channel_post(update) assert Filters.update.channel_posts(update) assert Filters.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.update.message(update) assert not Filters.update.edited_message(update) assert not Filters.update.messages(update) assert not Filters.update.channel_post(update) assert Filters.update.edited_channel_post(update) assert Filters.update.channel_posts(update) assert Filters.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)(update) update.message.text = 'test' update.message.entities = [] (Filters.command & raising_filter)(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)(update) update.message.text = '/test' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] (Filters.command | raising_filter)(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]} result = (Filters.command & DataFilter('blah'))(update) assert result['test'] == ['blah'] result = (DataFilter('blah1') & DataFilter('blah2'))(update) assert result['test'] == ['blah1', 'blah2'] update.message.text = 'test' update.message.entities = [] result = (Filters.command & DataFilter('blah'))(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'))(update) assert result result = (DataFilter('blah1') | DataFilter('blah2'))(update) assert result['test'] == ['blah1'] update.message.text = 'test' result = (Filters.command | DataFilter('blah'))(update) assert result['test'] == ['blah'] def test_filters_via_bot_init(self): with pytest.raises(RuntimeError, match='in conjunction with'): Filters.via_bot(bot_id=1, username='bot') def test_filters_via_bot_allow_empty(self, update): assert not Filters.via_bot()(update) assert Filters.via_bot(allow_empty=True)(update) def test_filters_via_bot_id(self, update): assert not Filters.via_bot(bot_id=1)(update) update.message.via_bot.id = 1 assert Filters.via_bot(bot_id=1)(update) update.message.via_bot.id = 2 assert Filters.via_bot(bot_id=[1, 2])(update) assert not Filters.via_bot(bot_id=[3, 4])(update) update.message.via_bot = None assert not Filters.via_bot(bot_id=[3, 4])(update) def test_filters_via_bot_username(self, update): assert not Filters.via_bot(username='bot')(update) assert not Filters.via_bot(username='Testbot')(update) update.message.via_bot.username = 'bot@' assert Filters.via_bot(username='@bot@')(update) assert Filters.via_bot(username='bot@')(update) assert Filters.via_bot(username=['bot1', 'bot@', 'bot2'])(update) assert not Filters.via_bot(username=['@username', '@bot_2'])(update) update.message.via_bot = None assert not Filters.user(username=['@username', '@bot_2'])(update) def test_filters_via_bot_change_id(self, update): f = Filters.via_bot(bot_id=3) assert f.bot_ids == {3} update.message.via_bot.id = 3 assert f(update) update.message.via_bot.id = 2 assert not f(update) f.bot_ids = 2 assert f.bot_ids == {2} assert f(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.usernames = 'user' def test_filters_via_bot_change_username(self, update): f = Filters.via_bot(username='bot') update.message.via_bot.username = 'bot' assert f(update) update.message.via_bot.username = 'Bot' assert not f(update) f.usernames = 'Bot' assert f(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.via_bot() for user in users: update.message.via_bot.username = user assert not f(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(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.via_bot() for user in users: update.message.via_bot.id = user assert not f(update) f.add_bot_ids(1) f.add_bot_ids([2, 3]) for user in users: update.message.via_bot.username = user assert f(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.via_bot(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(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(update) def test_filters_via_bot_remove_user_by_id(self, update): users = [1, 2, 3] f = Filters.via_bot(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(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(update) def test_filters_via_bot_repr(self): f = Filters.via_bot([1, 2]) assert str(f) == 'Filters.via_bot(1, 2)' f.remove_bot_ids(1) f.remove_bot_ids(2) assert str(f) == 'Filters.via_bot()' f.add_usernames('@foobar') assert str(f) == 'Filters.via_bot(foobar)' f.add_usernames('@barfoo') assert str(f).startswith('Filters.via_bot(') # we don't know th exact order assert 'barfoo' in str(f) and '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(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(up) python-telegram-bot-13.11/tests/test_forcereply.py000066400000000000000000000056671417656324400223770ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import ForceReply, ReplyKeyboardRemove @pytest.fixture(scope='class') def force_reply(): return ForceReply( TestForceReply.force_reply, TestForceReply.selective, TestForceReply.input_field_placeholder, ) class TestForceReply: force_reply = True selective = True input_field_placeholder = 'force replies can be annoying if not used properly' def test_slot_behaviour(self, force_reply, recwarn, mro_slots): for attr in force_reply.__slots__: assert getattr(force_reply, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not force_reply.__dict__, f"got missing slot(s): {force_reply.__dict__}" assert len(mro_slots(force_reply)) == len(set(mro_slots(force_reply))), "duplicate slot" force_reply.custom, force_reply.force_reply = 'should give warning', self.force_reply assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @flaky(3, 1) def test_send_message_with_force_reply(self, bot, chat_id, force_reply): message = bot.send_message(chat_id, 'text', reply_markup=force_reply) assert message.text == 'text' 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, False) b = ForceReply(False, False) c = ForceReply(True, 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) python-telegram-bot-13.11/tests/test_game.py000066400000000000000000000116131417656324400211220ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, Game, PhotoSize, Animation @pytest.fixture(scope='function') def game(): return Game( TestGame.title, TestGame.description, TestGame.photo, text=TestGame.text, text_entities=TestGame.text_entities, animation=TestGame.animation, ) class TestGame: 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) def test_slot_behaviour(self, game, recwarn, mro_slots): for attr in game.__slots__: assert getattr(game, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not game.__dict__, f"got missing slot(s): {game.__dict__}" assert len(mro_slots(game)) == len(set(mro_slots(game))), "duplicate slot" game.custom, game.title = 'should give warning', self.title assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.title == self.title assert game.description == self.description assert game.photo == 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.title == self.title assert game.description == self.description assert game.photo == self.photo assert game.text == self.text assert game.text_entities == 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_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'} 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) python-telegram-bot-13.11/tests/test_gamehighscore.py000066400000000000000000000056301417656324400230200ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def game_highscore(): return GameHighScore( TestGameHighScore.position, TestGameHighScore.user, TestGameHighScore.score ) class TestGameHighScore: position = 12 user = User(2, 'test user', False) score = 42 def test_slot_behaviour(self, game_highscore, recwarn, mro_slots): for attr in game_highscore.__slots__: assert getattr(game_highscore, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not game_highscore.__dict__, f"got missing slot(s): {game_highscore.__dict__}" assert len(mro_slots(game_highscore)) == len(set(mro_slots(game_highscore))), "same slot" game_highscore.custom, game_highscore.position = 'should give warning', self.position assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.position == self.position assert highscore.user == self.user assert highscore.score == self.score 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-13.11/tests/test_handler.py000066400000000000000000000032221417656324400216230ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 sys import version_info as py_ver from telegram.ext import Handler class TestHandler: def test_slot_behaviour(self, recwarn, mro_slots): class SubclassHandler(Handler): __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 not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" assert '__dict__' not in Handler.__slots__ if py_ver < (3, 7) else True, 'dict in abc' inst.custom = 'should not give warning' assert len(recwarn) == 0, recwarn.list python-telegram-bot-13.11/tests/test_helpers.py000066400000000000000000000375171417656324400216660ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 time import datetime as dtm from importlib import reload from pathlib import Path from unittest import mock import pytest from telegram import Sticker, InputFile, Animation from telegram import Update from telegram import User from telegram import MessageEntity from telegram.ext import Defaults from telegram.message import Message from telegram.utils import helpers from telegram.utils.helpers import _datetime_to_float_timestamp # sample time specification values categorised into absolute / delta / time-of-day from tests.conftest import env_var_2_bool ABSOLUTE_TIME_SPECS = [ dtm.datetime.now(tz=dtm.timezone(dtm.timedelta(hours=-7))), dtm.datetime.utcnow(), ] 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 TIME_SPECS = ABSOLUTE_TIME_SPECS + RELATIVE_TIME_SPECS """ This part is here for ptb-raw, where we don't have pytz (unless the user installs it) Because imports in pytest are intricate, we just run pytest -k test_helpers.py with the TEST_NO_PYTZ environment variable set in addition to the regular test suite. Because actually uninstalling pytz would lead to errors in the test suite we just mock the import to raise the expected exception. Note that a fixture that just does this for every test that needs it is a nice idea, but for some reason makes test_updater.py hang indefinitely on GitHub Actions (at least when Hinrich tried that) """ TEST_NO_PYTZ = env_var_2_bool(os.getenv('TEST_NO_PYTZ', False)) if TEST_NO_PYTZ: orig_import = __import__ def import_mock(module_name, *args, **kwargs): if module_name == 'pytz': raise ModuleNotFoundError('We are testing without pytz here') return orig_import(module_name, *args, **kwargs) with mock.patch('builtins.__import__', side_effect=import_mock): reload(helpers) class TestHelpers: def test_helpers_utc(self): # Here we just test, that we got the correct UTC variant if TEST_NO_PYTZ: assert helpers.UTC is helpers.DTM_UTC else: assert helpers.UTC is not helpers.DTM_UTC def test_escape_markdown(self): test_str = '*bold*, _italic_, `code`, [text_link](http://github.com/)' expected_str = r'\*bold\*, \_italic\_, \`code\`, \[text\_link](http://github.com/)' assert expected_str == helpers.escape_markdown(test_str) def test_escape_markdown_v2(self): test_str = 'a_b*c[d]e (fg) h~I`>JK#L+MN -O=|p{qr}s.t! u' expected_str = r'a\_b\*c\[d\]e \(fg\) h\~I\`\>JK\#L\+MN \-O\=\|p\{qr\}s\.t\! u' assert expected_str == helpers.escape_markdown(test_str, version=2) def test_escape_markdown_v2_monospaced(self): test_str = r'mono/pre: `abc` \int (`\some \`stuff)' expected_str = 'mono/pre: \\`abc\\` \\\\int (\\`\\\\some \\\\\\`stuff)' assert expected_str == helpers.escape_markdown( test_str, version=2, entity_type=MessageEntity.PRE ) assert expected_str == helpers.escape_markdown( test_str, version=2, entity_type=MessageEntity.CODE ) def test_escape_markdown_v2_text_link(self): test_str = 'https://url.containing/funny)cha)\\ra\\)cter\\s' expected_str = 'https://url.containing/funny\\)cha\\)\\\\ra\\\\\\)cter\\\\s' assert expected_str == helpers.escape_markdown( test_str, version=2, entity_type=MessageEntity.TEXT_LINK ) def test_markdown_invalid_version(self): with pytest.raises(ValueError): helpers.escape_markdown('abc', version=-1) 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 helpers.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(helpers, 'UTC', helpers.DTM_UTC) datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10 ** 5) assert helpers.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 = timezone.localize(test_datetime) assert ( helpers.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): helpers.to_float_timestamp(dtm.datetime(2019, 11, 11), reference_timestamp=123) @pytest.mark.parametrize('time_spec', DELTA_TIME_SPECS, ids=str) def test_to_float_timestamp_delta(self, time_spec): """Conversion from a 'delta' time specification to timestamp""" reference_t = 0 delta = time_spec.total_seconds() if hasattr(time_spec, 'total_seconds') else time_spec assert helpers.to_float_timestamp(time_spec, reference_t) == reference_t + delta def test_to_float_timestamp_time_of_day(self): """Conversion from time-of-day specification to timestamp""" hour, hour_delta = 12, 1 ref_t = _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 helpers.to_float_timestamp(time_future, ref_t) == ref_t + 60 * 60 * hour_delta assert helpers.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 = _datetime_to_float_timestamp(ref_datetime), ref_datetime.time() aware_time_of_day = timezone.localize(ref_datetime).timetz() # first test that naive time is assumed to be utc: assert helpers.to_float_timestamp(time_of_day, ref_t) == pytest.approx(ref_t) # test that by setting the timezone the timestamp changes accordingly: assert helpers.to_float_timestamp(aware_time_of_day, ref_t) == pytest.approx( ref_t + (-utc_offset.total_seconds() % (24 * 60 * 60)) ) @pytest.mark.parametrize('time_spec', RELATIVE_TIME_SPECS, ids=str) def test_to_float_timestamp_default_reference(self, time_spec): """The reference timestamp for relative time specifications should default to now""" now = time.time() assert helpers.to_float_timestamp(time_spec) == pytest.approx( helpers.to_float_timestamp(time_spec, reference_timestamp=now) ) def test_to_float_timestamp_error(self): with pytest.raises(TypeError, match='Defaults'): helpers.to_float_timestamp(Defaults()) @pytest.mark.parametrize('time_spec', TIME_SPECS, ids=str) def test_to_timestamp(self, time_spec): # delegate tests to `to_float_timestamp` assert helpers.to_timestamp(time_spec) == int(helpers.to_float_timestamp(time_spec)) def test_to_timestamp_none(self): # this 'convenience' behaviour has been left left for backwards compatibility assert helpers.to_timestamp(None) is None def test_from_timestamp_none(self): assert helpers.from_timestamp(None) is None def test_from_timestamp_naive(self): datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, tzinfo=None) assert helpers.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 = timezone.localize(test_datetime) assert ( helpers.from_timestamp( 1573431976.1 - timezone.utcoffset(test_datetime).total_seconds() ) == datetime ) 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): helpers.create_deep_linked_url(username, 'text with spaces') with pytest.raises(ValueError): helpers.create_deep_linked_url(username, '0' * 65) with pytest.raises(ValueError): helpers.create_deep_linked_url(None, None) with pytest.raises(ValueError): # too short username (4 is minimum) helpers.create_deep_linked_url("abc", None) def test_effective_message_type(self): def build_test_message(**kwargs): config = dict( message_id=1, from_user=None, date=None, chat=None, ) config.update(**kwargs) return Message(**config) test_message = build_test_message(text='Test') assert helpers.effective_message_type(test_message) == 'text' test_message.text = None test_message = build_test_message( sticker=Sticker('sticker_id', 'unique_id', 50, 50, False, False) ) assert helpers.effective_message_type(test_message) == 'sticker' test_message.sticker = None test_message = build_test_message(new_chat_members=[User(55, 'new_user', False)]) assert helpers.effective_message_type(test_message) == 'new_chat_members' test_message = build_test_message(left_chat_member=[User(55, 'new_user', False)]) assert helpers.effective_message_type(test_message) == 'left_chat_member' test_update = Update(1) test_message = build_test_message(text='Test') test_update.message = test_message assert helpers.effective_message_type(test_update) == 'text' empty_update = Update(2) assert helpers.effective_message_type(empty_update) is None def test_mention_html(self): expected = 'the name' assert expected == helpers.mention_html(1, 'the name') def test_mention_markdown(self): expected = '[the name](tg://user?id=1)' assert expected == helpers.mention_markdown(1, 'the name') def test_mention_markdown_2(self): expected = r'[the\_name](tg://user?id=1)' assert expected == helpers.mention_markdown(1, 'the_name') @pytest.mark.parametrize( 'string,expected', [ ('tests/data/game.gif', True), ('tests/data', False), (str(Path.cwd() / 'tests' / 'data' / 'game.gif'), True), (str(Path.cwd() / 'tests' / 'data'), False), (Path.cwd() / 'tests' / 'data' / 'game.gif', True), (Path.cwd() / 'tests' / 'data', False), ('https:/api.org/file/botTOKEN/document/file_3', False), (None, False), ], ) def test_is_local_file(self, string, expected): assert helpers.is_local_file(string) == expected @pytest.mark.parametrize( 'string,expected', [ ('tests/data/game.gif', (Path.cwd() / 'tests' / 'data' / 'game.gif').as_uri()), ('tests/data', 'tests/data'), ('file://foobar', 'file://foobar'), ( str(Path.cwd() / 'tests' / 'data' / 'game.gif'), (Path.cwd() / 'tests' / 'data' / 'game.gif').as_uri(), ), (str(Path.cwd() / 'tests' / 'data'), str(Path.cwd() / 'tests' / 'data')), ( Path.cwd() / 'tests' / 'data' / 'game.gif', (Path.cwd() / 'tests' / 'data' / 'game.gif').as_uri(), ), (Path.cwd() / 'tests' / 'data', Path.cwd() / 'tests' / 'data'), ( 'https:/api.org/file/botTOKEN/document/file_3', 'https:/api.org/file/botTOKEN/document/file_3', ), ], ) def test_parse_file_input_string(self, string, expected): assert helpers.parse_file_input(string) == expected def test_parse_file_input_file_like(self): with open('tests/data/game.gif', 'rb') as file: parsed = helpers.parse_file_input(file) assert isinstance(parsed, InputFile) assert not parsed.attach assert parsed.filename == 'game.gif' with open('tests/data/game.gif', 'rb') as file: parsed = helpers.parse_file_input(file, attach=True, filename='test_file') assert isinstance(parsed, InputFile) assert parsed.attach assert parsed.filename == 'test_file' def test_parse_file_input_bytes(self): with open('tests/data/text_file.txt', 'rb') as file: parsed = helpers.parse_file_input(file.read()) assert isinstance(parsed, InputFile) assert not parsed.attach assert parsed.filename == 'application.octet-stream' with open('tests/data/text_file.txt', 'rb') as file: parsed = helpers.parse_file_input(file.read(), attach=True, filename='test_file') assert isinstance(parsed, InputFile) assert parsed.attach assert parsed.filename == 'test_file' def test_parse_file_input_tg_object(self): animation = Animation('file_id', 'unique_id', 1, 1, 1) assert helpers.parse_file_input(animation, Animation) == 'file_id' assert helpers.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 helpers.parse_file_input(obj) is obj python-telegram-bot-13.11/tests/test_inlinekeyboardbutton.py000066400000000000000000000150541417656324400244470ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, LoginUrl @pytest.fixture(scope='class') def inline_keyboard_button(): return InlineKeyboardButton( TestInlineKeyboardButton.text, url=TestInlineKeyboardButton.url, callback_data=TestInlineKeyboardButton.callback_data, switch_inline_query=TestInlineKeyboardButton.switch_inline_query, switch_inline_query_current_chat=TestInlineKeyboardButton.switch_inline_query_current_chat, callback_game=TestInlineKeyboardButton.callback_game, pay=TestInlineKeyboardButton.pay, login_url=TestInlineKeyboardButton.login_url, ) class TestInlineKeyboardButton: 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 = 'callback_game' pay = 'pay' login_url = LoginUrl("http://google.com") def test_slot_behaviour(self, inline_keyboard_button, recwarn, mro_slots): inst = inline_keyboard_button for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.text = 'should give warning', self.text assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 inline_keyboard_button.callback_game == self.callback_game assert inline_keyboard_button.pay == self.pay assert inline_keyboard_button.login_url == self.login_url 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 assert inline_keyboard_button_dict['pay'] == inline_keyboard_button.pay assert ( inline_keyboard_button_dict['login_url'] == inline_keyboard_button.login_url.to_dict() ) # NOQA: E127 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, 'pay': self.pay, } inline_keyboard_button = InlineKeyboardButton.de_json(json_dict, None) 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 inline_keyboard_button.callback_game == self.callback_game assert inline_keyboard_button.pay == self.pay 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 = 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) @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-13.11/tests/test_inlinekeyboardmarkup.py000066400000000000000000000174041417656324400244340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ReplyMarkup, ReplyKeyboardMarkup @pytest.fixture(scope='class') def inline_keyboard_markup(): return InlineKeyboardMarkup(TestInlineKeyboardMarkup.inline_keyboard) class TestInlineKeyboardMarkup: inline_keyboard = [ [ InlineKeyboardButton(text='button1', callback_data='data1'), InlineKeyboardButton(text='button2', callback_data='data2'), ] ] def test_slot_behaviour(self, inline_keyboard_markup, recwarn, mro_slots): inst = inline_keyboard_markup for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.inline_keyboard = 'should give warning', self.inline_keyboard assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @flaky(3, 1) def test_send_message_with_inline_keyboard_markup(self, bot, chat_id, inline_keyboard_markup): message = bot.send_message( chat_id, 'Testing InlineKeyboardMarkup', reply_markup=inline_keyboard_markup ) assert message.text == 'Testing InlineKeyboardMarkup' 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 == self.inline_keyboard def test_expected_values_empty_switch(self, inline_keyboard_markup, bot, monkeypatch): def test( url, data, reply_to_message_id=None, disable_notification=None, reply_markup=None, timeout=None, **kwargs, ): if reply_markup is not None: if isinstance(reply_markup, ReplyMarkup): 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].callback_data = None inline_keyboard_markup.inline_keyboard[0][0].switch_inline_query = '' 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, '_message', test) bot.send_message(123, 'test', reply_markup=inline_keyboard_markup) 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 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) python-telegram-bot-13.11/tests/test_inlinequery.py000066400000000000000000000121201417656324400225470ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, Location, InlineQuery, Update, Bot, Chat from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling @pytest.fixture(scope='class') def inline_query(bot): return InlineQuery( TestInlineQuery.id_, TestInlineQuery.from_user, TestInlineQuery.query, TestInlineQuery.offset, location=TestInlineQuery.location, chat_type=TestInlineQuery.chat_type, bot=bot, ) class TestInlineQuery: id_ = 1234 from_user = User(1, 'First name', False) query = 'query text' offset = 'offset' location = Location(8.8, 53.1) chat_type = Chat.SENDER def test_slot_behaviour(self, inline_query, recwarn, mro_slots): for attr in inline_query.__slots__: assert getattr(inline_query, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inline_query.__dict__, f"got missing slot(s): {inline_query.__dict__}" assert len(mro_slots(inline_query)) == len(set(mro_slots(inline_query))), "duplicate slot" inline_query.custom, inline_query.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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(), 'chat_type': self.chat_type, } inline_query_json = InlineQuery.de_json(json_dict, bot) 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 assert inline_query_json.chat_type == self.chat_type 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 assert inline_query_dict['chat_type'] == inline_query.chat_type def test_answer(self, monkeypatch, inline_query): 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 check_shortcut_call(inline_query.answer, inline_query.bot, 'answer_inline_query') assert check_defaults_handling(inline_query.answer, inline_query.bot) monkeypatch.setattr(inline_query.bot, 'answer_inline_query', make_assertion) assert inline_query.answer(results=[]) def test_answer_error(self, inline_query): with pytest.raises(TypeError, match='mutually exclusive'): inline_query.answer(results=[], auto_pagination=True, current_offset='foobar') def test_answer_auto_pagination(self, monkeypatch, inline_query): 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.bot, 'answer_inline_query', make_assertion) assert inline_query.answer(results=[], auto_pagination=True) 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) python-telegram-bot-13.11/tests/test_inlinequeryhandler.py000066400000000000000000000224301417656324400241120ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram import ( Update, CallbackQuery, Bot, Message, User, Chat, InlineQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, Location, ) from telegram.ext import InlineQueryHandler, CallbackContext, JobQueue 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='function') def inline_query(bot): return Update( 0, inline_query=InlineQuery( 'id', User(2, 'test user', False), 'test query', offset='22', location=Location(latitude=-23.691288, longitude=-46.788279), ), ) class TestInlineQueryHandler: test_flag = False def test_slot_behaviour(self, recwarn, mro_slots): handler = InlineQueryHandler(self.callback_context) for attr in handler.__slots__: assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" handler.custom, handler.callback = 'should give warning', self.callback_basic assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(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', ' query') if groupdict is not None: self.test_flag = groupdict == {'begin': 't', 'end': ' query'} def callback_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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_context_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_basic(self, dp, inline_query): handler = InlineQueryHandler(self.callback_basic) dp.add_handler(handler) assert handler.check_update(inline_query) dp.process_update(inline_query) assert self.test_flag def test_with_pattern(self, inline_query): handler = InlineQueryHandler(self.callback_basic, pattern='(?P.*)est(?P.*)') assert handler.check_update(inline_query) inline_query.inline_query.query = 'nothing here' assert not handler.check_update(inline_query) def test_with_passing_group_dict(self, dp, inline_query): handler = InlineQueryHandler( self.callback_group, pattern='(?P.*)est(?P.*)', pass_groups=True ) dp.add_handler(handler) dp.process_update(inline_query) assert self.test_flag dp.remove_handler(handler) handler = InlineQueryHandler( self.callback_group, pattern='(?P.*)est(?P.*)', pass_groupdict=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(inline_query) assert self.test_flag def test_pass_user_or_chat_data(self, dp, inline_query): handler = InlineQueryHandler(self.callback_data_1, pass_user_data=True) dp.add_handler(handler) dp.process_update(inline_query) assert self.test_flag dp.remove_handler(handler) handler = InlineQueryHandler(self.callback_data_1, pass_chat_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(inline_query) assert self.test_flag dp.remove_handler(handler) handler = InlineQueryHandler( self.callback_data_2, pass_chat_data=True, pass_user_data=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(inline_query) assert self.test_flag def test_pass_job_or_update_queue(self, dp, inline_query): handler = InlineQueryHandler(self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update(inline_query) assert self.test_flag dp.remove_handler(handler) handler = InlineQueryHandler(self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(inline_query) assert self.test_flag dp.remove_handler(handler) handler = InlineQueryHandler( self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(inline_query) assert self.test_flag def test_other_update_types(self, false_update): handler = InlineQueryHandler(self.callback_basic) assert not handler.check_update(false_update) def test_context(self, cdp, inline_query): handler = InlineQueryHandler(self.callback_context) cdp.add_handler(handler) cdp.process_update(inline_query) assert self.test_flag def test_context_pattern(self, cdp, inline_query): handler = InlineQueryHandler( self.callback_context_pattern, pattern=r'(?P.*)est(?P.*)' ) cdp.add_handler(handler) cdp.process_update(inline_query) assert self.test_flag cdp.remove_handler(handler) handler = InlineQueryHandler(self.callback_context_pattern, pattern=r'(t)est(.*)') cdp.add_handler(handler) cdp.process_update(inline_query) assert self.test_flag @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)] ) def test_chat_types(self, cdp, inline_query, chat_types, chat_type, result): try: inline_query.inline_query.chat_type = chat_type handler = InlineQueryHandler(self.callback_context, chat_types=chat_types) cdp.add_handler(handler) cdp.process_update(inline_query) if not chat_types: assert self.test_flag is False else: assert self.test_flag == result finally: inline_query.chat_type = None python-telegram-bot-13.11/tests/test_inlinequeryresultarticle.py000066400000000000000000000132131417656324400253560ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InlineQueryResultAudio, InlineQueryResultArticle, InlineKeyboardButton, InputTextMessageContent, ) @pytest.fixture(scope='class') def inline_query_result_article(): return InlineQueryResultArticle( TestInlineQueryResultArticle.id_, TestInlineQueryResultArticle.title, input_message_content=TestInlineQueryResultArticle.input_message_content, reply_markup=TestInlineQueryResultArticle.reply_markup, url=TestInlineQueryResultArticle.url, hide_url=TestInlineQueryResultArticle.hide_url, description=TestInlineQueryResultArticle.description, thumb_url=TestInlineQueryResultArticle.thumb_url, thumb_height=TestInlineQueryResultArticle.thumb_height, thumb_width=TestInlineQueryResultArticle.thumb_width, ) class TestInlineQueryResultArticle: 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' thumb_url = 'thumb url' thumb_height = 10 thumb_width = 15 def test_slot_behaviour(self, inline_query_result_article, mro_slots, recwarn): inst = inline_query_result_article for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb_url == self.thumb_url assert inline_query_result_article.thumb_height == self.thumb_height assert inline_query_result_article.thumb_width == self.thumb_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['thumb_url'] == inline_query_result_article.thumb_url ) assert ( inline_query_result_article_dict['thumb_height'] == inline_query_result_article.thumb_height ) assert ( inline_query_result_article_dict['thumb_width'] == inline_query_result_article.thumb_width ) 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-13.11/tests/test_inlinequeryresultaudio.py000066400000000000000000000131011417656324400250300ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InlineKeyboardButton, InlineQueryResultAudio, InputTextMessageContent, InlineQueryResultVoice, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_audio(): return InlineQueryResultAudio( TestInlineQueryResultAudio.id_, TestInlineQueryResultAudio.audio_url, TestInlineQueryResultAudio.title, performer=TestInlineQueryResultAudio.performer, audio_duration=TestInlineQueryResultAudio.audio_duration, caption=TestInlineQueryResultAudio.caption, parse_mode=TestInlineQueryResultAudio.parse_mode, caption_entities=TestInlineQueryResultAudio.caption_entities, input_message_content=TestInlineQueryResultAudio.input_message_content, reply_markup=TestInlineQueryResultAudio.reply_markup, ) class TestInlineQueryResultAudio: 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')]]) def test_slot_behaviour(self, inline_query_result_audio, mro_slots, recwarn): inst = inline_query_result_audio for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == 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_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-13.11/tests/test_inlinequeryresultcachedaudio.py000066400000000000000000000123761417656324400261750ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InlineQueryResultCachedAudio, InlineKeyboardMarkup, InlineKeyboardButton, InlineQueryResultCachedVoice, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_cached_audio(): return InlineQueryResultCachedAudio( TestInlineQueryResultCachedAudio.id_, TestInlineQueryResultCachedAudio.audio_file_id, caption=TestInlineQueryResultCachedAudio.caption, parse_mode=TestInlineQueryResultCachedAudio.parse_mode, caption_entities=TestInlineQueryResultCachedAudio.caption_entities, input_message_content=TestInlineQueryResultCachedAudio.input_message_content, reply_markup=TestInlineQueryResultCachedAudio.reply_markup, ) class TestInlineQueryResultCachedAudio: 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')]]) def test_slot_behaviour(self, inline_query_result_cached_audio, mro_slots, recwarn): inst = inline_query_result_cached_audio for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == 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_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-13.11/tests/test_inlinequeryresultcacheddocument.py000066400000000000000000000142171417656324400267060ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ( InlineQueryResultCachedDocument, InlineKeyboardButton, InlineKeyboardMarkup, InputTextMessageContent, InlineQueryResultCachedVoice, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_cached_document(): return InlineQueryResultCachedDocument( TestInlineQueryResultCachedDocument.id_, TestInlineQueryResultCachedDocument.title, TestInlineQueryResultCachedDocument.document_file_id, caption=TestInlineQueryResultCachedDocument.caption, parse_mode=TestInlineQueryResultCachedDocument.parse_mode, caption_entities=TestInlineQueryResultCachedDocument.caption_entities, description=TestInlineQueryResultCachedDocument.description, input_message_content=TestInlineQueryResultCachedDocument.input_message_content, reply_markup=TestInlineQueryResultCachedDocument.reply_markup, ) class TestInlineQueryResultCachedDocument: 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')]]) def test_slot_behaviour(self, inline_query_result_cached_document, mro_slots, recwarn): inst = inline_query_result_cached_document for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == 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_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-13.11/tests/test_inlinequeryresultcachedgif.py000066400000000000000000000125111417656324400256300ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InputTextMessageContent, InlineQueryResultCachedVoice, InlineKeyboardMarkup, InlineQueryResultCachedGif, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_cached_gif(): return InlineQueryResultCachedGif( TestInlineQueryResultCachedGif.id_, TestInlineQueryResultCachedGif.gif_file_id, title=TestInlineQueryResultCachedGif.title, caption=TestInlineQueryResultCachedGif.caption, parse_mode=TestInlineQueryResultCachedGif.parse_mode, caption_entities=TestInlineQueryResultCachedGif.caption_entities, input_message_content=TestInlineQueryResultCachedGif.input_message_content, reply_markup=TestInlineQueryResultCachedGif.reply_markup, ) class TestInlineQueryResultCachedGif: 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')]]) def test_slot_behaviour(self, inline_query_result_cached_gif, recwarn, mro_slots): inst = inline_query_result_cached_gif for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == 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_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-13.11/tests/test_inlinequeryresultcachedmpeg4gif.py000066400000000000000000000134241417656324400265710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ( InlineQueryResultCachedMpeg4Gif, InlineKeyboardButton, InputTextMessageContent, InlineKeyboardMarkup, InlineQueryResultCachedVoice, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_cached_mpeg4_gif(): return InlineQueryResultCachedMpeg4Gif( TestInlineQueryResultCachedMpeg4Gif.id_, TestInlineQueryResultCachedMpeg4Gif.mpeg4_file_id, title=TestInlineQueryResultCachedMpeg4Gif.title, caption=TestInlineQueryResultCachedMpeg4Gif.caption, parse_mode=TestInlineQueryResultCachedMpeg4Gif.parse_mode, caption_entities=TestInlineQueryResultCachedMpeg4Gif.caption_entities, input_message_content=TestInlineQueryResultCachedMpeg4Gif.input_message_content, reply_markup=TestInlineQueryResultCachedMpeg4Gif.reply_markup, ) class TestInlineQueryResultCachedMpeg4Gif: 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')]]) def test_slot_behaviour(self, inline_query_result_cached_mpeg4_gif, mro_slots, recwarn): inst = inline_query_result_cached_mpeg4_gif for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == 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_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-13.11/tests/test_inlinequeryresultcachedphoto.py000066400000000000000000000135411417656324400262200ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InlineQueryResultCachedPhoto, InlineKeyboardButton, InlineQueryResultCachedVoice, InlineKeyboardMarkup, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_cached_photo(): return InlineQueryResultCachedPhoto( TestInlineQueryResultCachedPhoto.id_, TestInlineQueryResultCachedPhoto.photo_file_id, title=TestInlineQueryResultCachedPhoto.title, description=TestInlineQueryResultCachedPhoto.description, caption=TestInlineQueryResultCachedPhoto.caption, parse_mode=TestInlineQueryResultCachedPhoto.parse_mode, caption_entities=TestInlineQueryResultCachedPhoto.caption_entities, input_message_content=TestInlineQueryResultCachedPhoto.input_message_content, reply_markup=TestInlineQueryResultCachedPhoto.reply_markup, ) class TestInlineQueryResultCachedPhoto: 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')]]) def test_slot_behaviour(self, inline_query_result_cached_photo, recwarn, mro_slots): inst = inline_query_result_cached_photo for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == 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_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-13.11/tests/test_inlinequeryresultcachedsticker.py000066400000000000000000000105701417656324400265320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InlineKeyboardButton, InlineQueryResultCachedSticker, InlineQueryResultCachedVoice, InlineKeyboardMarkup, ) @pytest.fixture(scope='class') def inline_query_result_cached_sticker(): return InlineQueryResultCachedSticker( TestInlineQueryResultCachedSticker.id_, TestInlineQueryResultCachedSticker.sticker_file_id, input_message_content=TestInlineQueryResultCachedSticker.input_message_content, reply_markup=TestInlineQueryResultCachedSticker.reply_markup, ) class TestInlineQueryResultCachedSticker: id_ = 'id' type_ = 'sticker' sticker_file_id = 'sticker file id' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) def test_slot_behaviour(self, inline_query_result_cached_sticker, mro_slots, recwarn): inst = inline_query_result_cached_sticker for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_inlinequeryresultcachedvideo.py000066400000000000000000000136171417656324400262010ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InlineKeyboardButton, InputTextMessageContent, InlineQueryResultCachedVideo, InlineQueryResultCachedVoice, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_cached_video(): return InlineQueryResultCachedVideo( TestInlineQueryResultCachedVideo.id_, TestInlineQueryResultCachedVideo.video_file_id, TestInlineQueryResultCachedVideo.title, caption=TestInlineQueryResultCachedVideo.caption, parse_mode=TestInlineQueryResultCachedVideo.parse_mode, caption_entities=TestInlineQueryResultCachedVideo.caption_entities, description=TestInlineQueryResultCachedVideo.description, input_message_content=TestInlineQueryResultCachedVideo.input_message_content, reply_markup=TestInlineQueryResultCachedVideo.reply_markup, ) class TestInlineQueryResultCachedVideo: 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')]]) def test_slot_behaviour(self, inline_query_result_cached_video, recwarn, mro_slots): inst = inline_query_result_cached_video for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == 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_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-13.11/tests/test_inlinequeryresultcachedvoice.py000066400000000000000000000131011417656324400261640ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ( InlineQueryResultCachedVoice, InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultCachedAudio, InputTextMessageContent, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_cached_voice(): return InlineQueryResultCachedVoice( TestInlineQueryResultCachedVoice.id_, TestInlineQueryResultCachedVoice.voice_file_id, TestInlineQueryResultCachedVoice.title, caption=TestInlineQueryResultCachedVoice.caption, parse_mode=TestInlineQueryResultCachedVoice.parse_mode, caption_entities=TestInlineQueryResultCachedVoice.caption_entities, input_message_content=TestInlineQueryResultCachedVoice.input_message_content, reply_markup=TestInlineQueryResultCachedVoice.reply_markup, ) class TestInlineQueryResultCachedVoice: 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')]]) def test_slot_behaviour(self, inline_query_result_cached_voice, recwarn, mro_slots): inst = inline_query_result_cached_voice for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == 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_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-13.11/tests/test_inlinequeryresultcontact.py000066400000000000000000000130101417656324400253610ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ( InlineQueryResultVoice, InputTextMessageContent, InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultContact, ) @pytest.fixture(scope='class') def inline_query_result_contact(): return InlineQueryResultContact( TestInlineQueryResultContact.id_, TestInlineQueryResultContact.phone_number, TestInlineQueryResultContact.first_name, last_name=TestInlineQueryResultContact.last_name, thumb_url=TestInlineQueryResultContact.thumb_url, thumb_width=TestInlineQueryResultContact.thumb_width, thumb_height=TestInlineQueryResultContact.thumb_height, input_message_content=TestInlineQueryResultContact.input_message_content, reply_markup=TestInlineQueryResultContact.reply_markup, ) class TestInlineQueryResultContact: id_ = 'id' type_ = 'contact' phone_number = 'phone_number' first_name = 'first_name' last_name = 'last_name' thumb_url = 'thumb url' thumb_width = 10 thumb_height = 15 input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) def test_slot_behaviour(self, inline_query_result_contact, mro_slots, recwarn): inst = inline_query_result_contact for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb_url == self.thumb_url assert inline_query_result_contact.thumb_width == self.thumb_width assert inline_query_result_contact.thumb_height == self.thumb_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['thumb_url'] == inline_query_result_contact.thumb_url ) assert ( inline_query_result_contact_dict['thumb_width'] == inline_query_result_contact.thumb_width ) assert ( inline_query_result_contact_dict['thumb_height'] == inline_query_result_contact.thumb_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-13.11/tests/test_inlinequeryresultdocument.py000066400000000000000000000154571417656324400255650ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InputTextMessageContent, InlineQueryResultDocument, InlineKeyboardMarkup, InlineQueryResultVoice, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_document(): return InlineQueryResultDocument( TestInlineQueryResultDocument.id_, TestInlineQueryResultDocument.document_url, TestInlineQueryResultDocument.title, TestInlineQueryResultDocument.mime_type, caption=TestInlineQueryResultDocument.caption, parse_mode=TestInlineQueryResultDocument.parse_mode, caption_entities=TestInlineQueryResultDocument.caption_entities, description=TestInlineQueryResultDocument.description, thumb_url=TestInlineQueryResultDocument.thumb_url, thumb_width=TestInlineQueryResultDocument.thumb_width, thumb_height=TestInlineQueryResultDocument.thumb_height, input_message_content=TestInlineQueryResultDocument.input_message_content, reply_markup=TestInlineQueryResultDocument.reply_markup, ) class TestInlineQueryResultDocument: 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' thumb_url = 'thumb url' thumb_width = 10 thumb_height = 15 input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) def test_slot_behaviour(self, inline_query_result_document, recwarn, mro_slots): inst = inline_query_result_document for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == 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.thumb_url == self.thumb_url assert inline_query_result_document.thumb_width == self.thumb_width assert inline_query_result_document.thumb_height == self.thumb_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_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['thumb_url'] == inline_query_result_document.thumb_url ) assert ( inline_query_result_document_dict['thumb_width'] == inline_query_result_document.thumb_width ) assert ( inline_query_result_document_dict['thumb_height'] == inline_query_result_document.thumb_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-13.11/tests/test_inlinequeryresultgame.py000066400000000000000000000067151417656324400246550ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InlineQueryResultGame, InlineQueryResultVoice, InlineKeyboardMarkup, ) @pytest.fixture(scope='class') def inline_query_result_game(): return InlineQueryResultGame( TestInlineQueryResultGame.id_, TestInlineQueryResultGame.game_short_name, reply_markup=TestInlineQueryResultGame.reply_markup, ) class TestInlineQueryResultGame: id_ = 'id' type_ = 'game' game_short_name = 'game short name' reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) def test_slot_behaviour(self, inline_query_result_game, mro_slots, recwarn): inst = inline_query_result_game for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_inlinequeryresultgif.py000066400000000000000000000142361417656324400245060ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InputTextMessageContent, InlineQueryResultGif, InlineQueryResultVoice, InlineKeyboardMarkup, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_gif(): return InlineQueryResultGif( TestInlineQueryResultGif.id_, TestInlineQueryResultGif.gif_url, TestInlineQueryResultGif.thumb_url, gif_width=TestInlineQueryResultGif.gif_width, gif_height=TestInlineQueryResultGif.gif_height, gif_duration=TestInlineQueryResultGif.gif_duration, title=TestInlineQueryResultGif.title, caption=TestInlineQueryResultGif.caption, parse_mode=TestInlineQueryResultGif.parse_mode, caption_entities=TestInlineQueryResultGif.caption_entities, input_message_content=TestInlineQueryResultGif.input_message_content, reply_markup=TestInlineQueryResultGif.reply_markup, thumb_mime_type=TestInlineQueryResultGif.thumb_mime_type, ) class TestInlineQueryResultGif: id_ = 'id' type_ = 'gif' gif_url = 'gif url' gif_width = 10 gif_height = 15 gif_duration = 1 thumb_url = 'thumb url' thumb_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')]]) def test_slot_behaviour(self, inline_query_result_gif, recwarn, mro_slots): inst = inline_query_result_gif for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb_url == self.thumb_url assert inline_query_result_gif.thumb_mime_type == self.thumb_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 == 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['thumb_url'] == inline_query_result_gif.thumb_url assert ( inline_query_result_gif_dict['thumb_mime_type'] == inline_query_result_gif.thumb_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.thumb_url) b = InlineQueryResultGif(self.id_, self.gif_url, self.thumb_url) c = InlineQueryResultGif(self.id_, '', self.thumb_url) d = InlineQueryResultGif('', self.gif_url, self.thumb_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-13.11/tests/test_inlinequeryresultlocation.py000066400000000000000000000153651417656324400255550ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InlineQueryResultLocation, InlineKeyboardButton, InlineQueryResultVoice, InlineKeyboardMarkup, ) @pytest.fixture(scope='class') def inline_query_result_location(): return InlineQueryResultLocation( TestInlineQueryResultLocation.id_, TestInlineQueryResultLocation.latitude, TestInlineQueryResultLocation.longitude, TestInlineQueryResultLocation.title, live_period=TestInlineQueryResultLocation.live_period, thumb_url=TestInlineQueryResultLocation.thumb_url, thumb_width=TestInlineQueryResultLocation.thumb_width, thumb_height=TestInlineQueryResultLocation.thumb_height, input_message_content=TestInlineQueryResultLocation.input_message_content, reply_markup=TestInlineQueryResultLocation.reply_markup, horizontal_accuracy=TestInlineQueryResultLocation.horizontal_accuracy, heading=TestInlineQueryResultLocation.heading, proximity_alert_radius=TestInlineQueryResultLocation.proximity_alert_radius, ) class TestInlineQueryResultLocation: id_ = 'id' type_ = 'location' latitude = 0.0 longitude = 1.0 title = 'title' horizontal_accuracy = 999 live_period = 70 heading = 90 proximity_alert_radius = 1000 thumb_url = 'thumb url' thumb_width = 10 thumb_height = 15 input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) def test_slot_behaviour(self, inline_query_result_location, mro_slots, recwarn): inst = inline_query_result_location for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb_url == self.thumb_url assert inline_query_result_location.thumb_width == self.thumb_width assert inline_query_result_location.thumb_height == self.thumb_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['thumb_url'] == inline_query_result_location.thumb_url ) assert ( inline_query_result_location_dict['thumb_width'] == inline_query_result_location.thumb_width ) assert ( inline_query_result_location_dict['thumb_height'] == inline_query_result_location.thumb_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-13.11/tests/test_inlinequeryresultmpeg4gif.py000066400000000000000000000155461417656324400254500ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ( InlineQueryResultMpeg4Gif, InlineKeyboardButton, InlineQueryResultVoice, InlineKeyboardMarkup, InputTextMessageContent, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_mpeg4_gif(): return InlineQueryResultMpeg4Gif( TestInlineQueryResultMpeg4Gif.id_, TestInlineQueryResultMpeg4Gif.mpeg4_url, TestInlineQueryResultMpeg4Gif.thumb_url, mpeg4_width=TestInlineQueryResultMpeg4Gif.mpeg4_width, mpeg4_height=TestInlineQueryResultMpeg4Gif.mpeg4_height, mpeg4_duration=TestInlineQueryResultMpeg4Gif.mpeg4_duration, title=TestInlineQueryResultMpeg4Gif.title, caption=TestInlineQueryResultMpeg4Gif.caption, parse_mode=TestInlineQueryResultMpeg4Gif.parse_mode, caption_entities=TestInlineQueryResultMpeg4Gif.caption_entities, input_message_content=TestInlineQueryResultMpeg4Gif.input_message_content, reply_markup=TestInlineQueryResultMpeg4Gif.reply_markup, thumb_mime_type=TestInlineQueryResultMpeg4Gif.thumb_mime_type, ) class TestInlineQueryResultMpeg4Gif: id_ = 'id' type_ = 'mpeg4_gif' mpeg4_url = 'mpeg4 url' mpeg4_width = 10 mpeg4_height = 15 mpeg4_duration = 1 thumb_url = 'thumb url' thumb_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')]]) def test_slot_behaviour(self, inline_query_result_mpeg4_gif, recwarn, mro_slots): inst = inline_query_result_mpeg4_gif for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb_url == self.thumb_url assert inline_query_result_mpeg4_gif.thumb_mime_type == self.thumb_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 == 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_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['thumb_url'] == inline_query_result_mpeg4_gif.thumb_url ) assert ( inline_query_result_mpeg4_gif_dict['thumb_mime_type'] == inline_query_result_mpeg4_gif.thumb_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.thumb_url) b = InlineQueryResultMpeg4Gif(self.id_, self.mpeg4_url, self.thumb_url) c = InlineQueryResultMpeg4Gif(self.id_, '', self.thumb_url) d = InlineQueryResultMpeg4Gif('', self.mpeg4_url, self.thumb_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-13.11/tests/test_inlinequeryresultphoto.py000066400000000000000000000141541417656324400250710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultPhoto, InlineQueryResultVoice, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_photo(): return InlineQueryResultPhoto( TestInlineQueryResultPhoto.id_, TestInlineQueryResultPhoto.photo_url, TestInlineQueryResultPhoto.thumb_url, photo_width=TestInlineQueryResultPhoto.photo_width, photo_height=TestInlineQueryResultPhoto.photo_height, title=TestInlineQueryResultPhoto.title, description=TestInlineQueryResultPhoto.description, caption=TestInlineQueryResultPhoto.caption, parse_mode=TestInlineQueryResultPhoto.parse_mode, caption_entities=TestInlineQueryResultPhoto.caption_entities, input_message_content=TestInlineQueryResultPhoto.input_message_content, reply_markup=TestInlineQueryResultPhoto.reply_markup, ) class TestInlineQueryResultPhoto: id_ = 'id' type_ = 'photo' photo_url = 'photo url' photo_width = 10 photo_height = 15 thumb_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')]]) def test_slot_behaviour(self, inline_query_result_photo, recwarn, mro_slots): inst = inline_query_result_photo for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb_url == self.thumb_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 == 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_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['thumb_url'] == inline_query_result_photo.thumb_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.thumb_url) b = InlineQueryResultPhoto(self.id_, self.photo_url, self.thumb_url) c = InlineQueryResultPhoto(self.id_, '', self.thumb_url) d = InlineQueryResultPhoto('', self.photo_url, self.thumb_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-13.11/tests/test_inlinequeryresultvenue.py000066400000000000000000000155571417656324400250720ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ( InlineQueryResultVoice, InputTextMessageContent, InlineKeyboardButton, InlineQueryResultVenue, InlineKeyboardMarkup, ) @pytest.fixture(scope='class') def inline_query_result_venue(): return InlineQueryResultVenue( TestInlineQueryResultVenue.id_, TestInlineQueryResultVenue.latitude, TestInlineQueryResultVenue.longitude, TestInlineQueryResultVenue.title, TestInlineQueryResultVenue.address, foursquare_id=TestInlineQueryResultVenue.foursquare_id, foursquare_type=TestInlineQueryResultVenue.foursquare_type, thumb_url=TestInlineQueryResultVenue.thumb_url, thumb_width=TestInlineQueryResultVenue.thumb_width, thumb_height=TestInlineQueryResultVenue.thumb_height, input_message_content=TestInlineQueryResultVenue.input_message_content, reply_markup=TestInlineQueryResultVenue.reply_markup, google_place_id=TestInlineQueryResultVenue.google_place_id, google_place_type=TestInlineQueryResultVenue.google_place_type, ) class TestInlineQueryResultVenue: 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' thumb_url = 'thumb url' thumb_width = 10 thumb_height = 15 input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) def test_slot_behaviour(self, inline_query_result_venue, mro_slots, recwarn): inst = inline_query_result_venue for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb_url == self.thumb_url assert inline_query_result_venue.thumb_width == self.thumb_width assert inline_query_result_venue.thumb_height == self.thumb_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['thumb_url'] == inline_query_result_venue.thumb_url assert ( inline_query_result_venue_dict['thumb_width'] == inline_query_result_venue.thumb_width ) assert ( inline_query_result_venue_dict['thumb_height'] == inline_query_result_venue.thumb_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-13.11/tests/test_inlinequeryresultvideo.py000066400000000000000000000154571417656324400250550ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InputTextMessageContent, InlineQueryResultVideo, InlineKeyboardMarkup, InlineQueryResultVoice, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_video(): return InlineQueryResultVideo( TestInlineQueryResultVideo.id_, TestInlineQueryResultVideo.video_url, TestInlineQueryResultVideo.mime_type, TestInlineQueryResultVideo.thumb_url, TestInlineQueryResultVideo.title, video_width=TestInlineQueryResultVideo.video_width, video_height=TestInlineQueryResultVideo.video_height, video_duration=TestInlineQueryResultVideo.video_duration, caption=TestInlineQueryResultVideo.caption, parse_mode=TestInlineQueryResultVideo.parse_mode, caption_entities=TestInlineQueryResultVideo.caption_entities, description=TestInlineQueryResultVideo.description, input_message_content=TestInlineQueryResultVideo.input_message_content, reply_markup=TestInlineQueryResultVideo.reply_markup, ) class TestInlineQueryResultVideo: id_ = 'id' type_ = 'video' video_url = 'video url' mime_type = 'mime type' video_width = 10 video_height = 15 video_duration = 15 thumb_url = 'thumb 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')]]) def test_slot_behaviour(self, inline_query_result_video, recwarn, mro_slots): inst = inline_query_result_video for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb_url == self.thumb_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 == 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_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['thumb_url'] == inline_query_result_video.thumb_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.thumb_url, self.title ) b = InlineQueryResultVideo( self.id_, self.video_url, self.mime_type, self.thumb_url, self.title ) c = InlineQueryResultVideo(self.id_, '', self.mime_type, self.thumb_url, self.title) d = InlineQueryResultVideo('', self.video_url, self.mime_type, self.thumb_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-13.11/tests/test_inlinequeryresultvoice.py000066400000000000000000000126071417656324400250460ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, InputTextMessageContent, InlineQueryResultAudio, InlineQueryResultVoice, InlineKeyboardMarkup, MessageEntity, ) @pytest.fixture(scope='class') def inline_query_result_voice(): return InlineQueryResultVoice( type=TestInlineQueryResultVoice.type_, id=TestInlineQueryResultVoice.id_, voice_url=TestInlineQueryResultVoice.voice_url, title=TestInlineQueryResultVoice.title, voice_duration=TestInlineQueryResultVoice.voice_duration, caption=TestInlineQueryResultVoice.caption, parse_mode=TestInlineQueryResultVoice.parse_mode, caption_entities=TestInlineQueryResultVoice.caption_entities, input_message_content=TestInlineQueryResultVoice.input_message_content, reply_markup=TestInlineQueryResultVoice.reply_markup, ) class TestInlineQueryResultVoice: 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')]]) def test_slot_behaviour(self, inline_query_result_voice, mro_slots, recwarn): inst = inline_query_result_voice for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == 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_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-13.11/tests/test_inputcontactmessagecontent.py000066400000000000000000000063131417656324400256650ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def input_contact_message_content(): return InputContactMessageContent( TestInputContactMessageContent.phone_number, TestInputContactMessageContent.first_name, last_name=TestInputContactMessageContent.last_name, ) class TestInputContactMessageContent: phone_number = 'phone number' first_name = 'first name' last_name = 'last name' def test_slot_behaviour(self, input_contact_message_content, mro_slots, recwarn): inst = input_contact_message_content for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.first_name = 'should give warning', self.first_name assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_inputfile.py000066400000000000000000000126331417656324400222130ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 logging import os import subprocess import sys from io import BytesIO from telegram import InputFile class TestInputFile: png = os.path.join('tests', 'data', 'game.png') def test_slot_behaviour(self, recwarn, mro_slots): 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 not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.filename = 'should give warning', inst.filename assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_subprocess_pipe(self): if sys.platform == 'win32': cmd = ['type', self.png] else: cmd = ['cat', self.png] proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=(sys.platform == 'win32')) in_file = InputFile(proc.stdout) assert in_file.input_file_content == open(self.png, 'rb').read() assert in_file.mimetype == 'image/png' assert in_file.filename == 'image.png' try: proc.kill() except ProcessLookupError: # This exception may be thrown if the process has finished before we had the chance # to kill it. pass def test_mimetypes(self, caplog): # Only test a few to make sure logic works okay assert InputFile(open('tests/data/telegram.jpg', 'rb')).mimetype == 'image/jpeg' assert InputFile(open('tests/data/telegram.webp', 'rb')).mimetype == 'image/webp' assert InputFile(open('tests/data/telegram.mp3', 'rb')).mimetype == 'audio/mpeg' # 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 with caplog.at_level(logging.DEBUG): assert InputFile(open('tests/data/text_file.txt')).mimetype == 'text/plain' assert len(caplog.records) == 1 assert caplog.records[0].getMessage().startswith('Could not parse file content') def test_filenames(self): assert InputFile(open('tests/data/telegram.jpg', 'rb')).filename == 'telegram.jpg' assert InputFile(open('tests/data/telegram.jpg', 'rb'), filename='blah').filename == 'blah' assert ( InputFile(open('tests/data/telegram.jpg', 'rb'), filename='blah.jpg').filename == 'blah.jpg' ) assert InputFile(open('tests/data/telegram', 'rb')).filename == 'telegram' assert InputFile(open('tests/data/telegram', 'rb'), filename='blah').filename == 'blah' assert ( InputFile(open('tests/data/telegram', 'rb'), filename='blah.jpg').filename == 'blah.jpg' ) class MockedFileobject: # A open(?, 'rb') without a .name def __init__(self, f): self.f = open(f, 'rb') def read(self): return self.f.read() assert InputFile(MockedFileobject('tests/data/telegram.jpg')).filename == 'image.jpeg' assert ( InputFile(MockedFileobject('tests/data/telegram.jpg'), filename='blah').filename == 'blah' ) assert ( InputFile(MockedFileobject('tests/data/telegram.jpg'), filename='blah.jpg').filename == 'blah.jpg' ) assert ( InputFile(MockedFileobject('tests/data/telegram')).filename == 'application.octet-stream' ) assert ( InputFile(MockedFileobject('tests/data/telegram'), filename='blah').filename == 'blah' ) assert ( InputFile(MockedFileobject('tests/data/telegram'), filename='blah.jpg').filename == 'blah.jpg' ) 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 with open('tests/data/text_file.txt', 'rb') as file: message = bot.send_document(chat_id, file.read()) out = BytesIO() assert message.document.get_file().download(out=out) out.seek(0) assert out.read().decode('utf-8') == 'PTB Rocks!' python-telegram-bot-13.11/tests/test_inputinvoicemessagecontent.py000066400000000000000000000312101417656324400256600ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, LabeledPrice, InputTextMessageContent, ) @pytest.fixture(scope='class') def input_invoice_message_content(): return InputInvoiceMessageContent( title=TestInputInvoiceMessageContent.title, description=TestInputInvoiceMessageContent.description, payload=TestInputInvoiceMessageContent.payload, provider_token=TestInputInvoiceMessageContent.provider_token, currency=TestInputInvoiceMessageContent.currency, prices=TestInputInvoiceMessageContent.prices, max_tip_amount=TestInputInvoiceMessageContent.max_tip_amount, suggested_tip_amounts=TestInputInvoiceMessageContent.suggested_tip_amounts, provider_data=TestInputInvoiceMessageContent.provider_data, photo_url=TestInputInvoiceMessageContent.photo_url, photo_size=TestInputInvoiceMessageContent.photo_size, photo_width=TestInputInvoiceMessageContent.photo_width, photo_height=TestInputInvoiceMessageContent.photo_height, need_name=TestInputInvoiceMessageContent.need_name, need_phone_number=TestInputInvoiceMessageContent.need_phone_number, need_email=TestInputInvoiceMessageContent.need_email, need_shipping_address=TestInputInvoiceMessageContent.need_shipping_address, send_phone_number_to_provider=TestInputInvoiceMessageContent.send_phone_number_to_provider, send_email_to_provider=TestInputInvoiceMessageContent.send_email_to_provider, is_flexible=TestInputInvoiceMessageContent.is_flexible, ) class TestInputInvoiceMessageContent: 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 def test_slot_behaviour(self, input_invoice_message_content, recwarn, mro_slots): inst = input_invoice_message_content for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.title = 'should give warning', self.title assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == self.prices assert input_invoice_message_content.max_tip_amount == self.max_tip_amount assert input_invoice_message_content.suggested_tip_amounts == [ 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_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'] == 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.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 == self.prices assert input_invoice_message_content.max_tip_amount == self.max_tip_amount assert input_invoice_message_content.suggested_tip_amounts == [ 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-13.11/tests/test_inputlocationmessagecontent.py000066400000000000000000000102111417656324400260320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def input_location_message_content(): return InputLocationMessageContent( TestInputLocationMessageContent.latitude, TestInputLocationMessageContent.longitude, live_period=TestInputLocationMessageContent.live_period, horizontal_accuracy=TestInputLocationMessageContent.horizontal_accuracy, heading=TestInputLocationMessageContent.heading, proximity_alert_radius=TestInputLocationMessageContent.proximity_alert_radius, ) class TestInputLocationMessageContent: latitude = -23.691288 longitude = -46.788279 live_period = 80 horizontal_accuracy = 50.5 heading = 90 proximity_alert_radius = 999 def test_slot_behaviour(self, input_location_message_content, mro_slots, recwarn): inst = input_location_message_content for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.heading = 'should give warning', self.heading assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_inputmedia.py000066400000000000000000000730511417656324400223540ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # 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/]. from pathlib import Path import pytest from flaky import flaky from telegram import ( InputMediaVideo, InputMediaPhoto, InputMediaAnimation, Message, InputFile, InputMediaAudio, InputMediaDocument, MessageEntity, ParseMode, ) # noinspection PyUnresolvedReferences from telegram.error import BadRequest from .test_animation import animation, animation_file # noqa: F401 # 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, photo, thumb # noqa: F401 # noinspection PyUnresolvedReferences from .test_video import video, video_file # noqa: F401 from tests.conftest import expect_bad_request @pytest.fixture(scope='class') def input_media_video(class_thumb_file): return InputMediaVideo( media=TestInputMediaVideo.media, caption=TestInputMediaVideo.caption, width=TestInputMediaVideo.width, height=TestInputMediaVideo.height, duration=TestInputMediaVideo.duration, parse_mode=TestInputMediaVideo.parse_mode, caption_entities=TestInputMediaVideo.caption_entities, thumb=class_thumb_file, supports_streaming=TestInputMediaVideo.supports_streaming, ) @pytest.fixture(scope='class') def input_media_photo(class_thumb_file): return InputMediaPhoto( media=TestInputMediaPhoto.media, caption=TestInputMediaPhoto.caption, parse_mode=TestInputMediaPhoto.parse_mode, caption_entities=TestInputMediaPhoto.caption_entities, ) @pytest.fixture(scope='class') def input_media_animation(class_thumb_file): return InputMediaAnimation( media=TestInputMediaAnimation.media, caption=TestInputMediaAnimation.caption, parse_mode=TestInputMediaAnimation.parse_mode, caption_entities=TestInputMediaAnimation.caption_entities, width=TestInputMediaAnimation.width, height=TestInputMediaAnimation.height, thumb=class_thumb_file, duration=TestInputMediaAnimation.duration, ) @pytest.fixture(scope='class') def input_media_audio(class_thumb_file): return InputMediaAudio( media=TestInputMediaAudio.media, caption=TestInputMediaAudio.caption, duration=TestInputMediaAudio.duration, performer=TestInputMediaAudio.performer, title=TestInputMediaAudio.title, thumb=class_thumb_file, parse_mode=TestInputMediaAudio.parse_mode, caption_entities=TestInputMediaAudio.caption_entities, ) @pytest.fixture(scope='class') def input_media_document(class_thumb_file): return InputMediaDocument( media=TestInputMediaDocument.media, caption=TestInputMediaDocument.caption, thumb=class_thumb_file, parse_mode=TestInputMediaDocument.parse_mode, caption_entities=TestInputMediaDocument.caption_entities, disable_content_type_detection=TestInputMediaDocument.disable_content_type_detection, ) class TestInputMediaVideo: 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)] def test_slot_behaviour(self, input_media_video, recwarn, mro_slots): inst = input_media_video for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == self.caption_entities assert input_media_video.supports_streaming == self.supports_streaming assert isinstance(input_media_video.thumb, InputFile) 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 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( 'tests/data/telegram.mp4', thumb='tests/data/telegram.jpg' ) assert input_media_video.media == (Path.cwd() / 'tests/data/telegram.mp4/').as_uri() assert input_media_video.thumb == (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() class TestInputMediaPhoto: type_ = "photo" media = "NOTAREALFILEID" caption = "My Caption" parse_mode = 'Markdown' caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] def test_slot_behaviour(self, input_media_photo, recwarn, mro_slots): inst = input_media_photo for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == self.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 ] 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('tests/data/telegram.mp4') assert input_media_photo.media == (Path.cwd() / 'tests/data/telegram.mp4/').as_uri() class TestInputMediaAnimation: type_ = "animation" media = "NOTAREALFILEID" caption = "My Caption" parse_mode = 'Markdown' caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] width = 30 height = 30 duration = 1 def test_slot_behaviour(self, input_media_animation, recwarn, mro_slots): inst = input_media_animation for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == self.caption_entities assert isinstance(input_media_animation.thumb, InputFile) 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 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( 'tests/data/telegram.mp4', thumb='tests/data/telegram.jpg' ) assert input_media_animation.media == (Path.cwd() / 'tests/data/telegram.mp4').as_uri() assert input_media_animation.thumb == (Path.cwd() / 'tests/data/telegram.jpg').as_uri() class TestInputMediaAudio: type_ = "audio" media = "NOTAREALFILEID" caption = "My Caption" duration = 3 performer = 'performer' title = 'title' parse_mode = 'HTML' caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] def test_slot_behaviour(self, input_media_audio, recwarn, mro_slots): inst = input_media_audio for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == self.caption_entities assert isinstance(input_media_audio.thumb, InputFile) 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( 'tests/data/telegram.mp4', thumb='tests/data/telegram.jpg' ) assert input_media_audio.media == (Path.cwd() / 'tests/data/telegram.mp4/').as_uri() assert input_media_audio.thumb == (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() class TestInputMediaDocument: type_ = "document" media = "NOTAREALFILEID" caption = "My Caption" parse_mode = 'HTML' caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)] disable_content_type_detection = True def test_slot_behaviour(self, input_media_document, recwarn, mro_slots): inst = input_media_document for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 == self.caption_entities assert ( input_media_document.disable_content_type_detection == self.disable_content_type_detection ) assert isinstance(input_media_document.thumb, InputFile) 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( 'tests/data/telegram.mp4', thumb='tests/data/telegram.jpg' ) assert input_media_document.media == (Path.cwd() / 'tests/data/telegram.mp4').as_uri() assert input_media_document.thumb == (Path.cwd() / 'tests/data/telegram.jpg').as_uri() @pytest.fixture(scope='function') # noqa: F811 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)] ), ] class TestSendMediaGroup: @flaky(3, 1) def test_send_media_group_photo(self, bot, chat_id, media_group): messages = bot.send_media_group(chat_id, media_group) assert isinstance(messages, list) 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 ) @flaky(3, 1) def test_send_media_group_all_args(self, bot, chat_id, media_group): m1 = bot.send_message(chat_id, text="test") messages = bot.send_media_group( chat_id, media_group, disable_notification=True, reply_to_message_id=m1.message_id, protect_content=True, ) assert isinstance(messages, list) 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) @flaky(3, 1) 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, ): def make_assertion(url, data, **kwargs): result = all(im.media.filename == 'custom_filename' for im in data['media']) # We are a bit hacky here b/c Bot.send_media_group expects a list of Message-dicts return [Message(0, None, None, text=result).to_dict()] 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'), ] assert bot.send_media_group(chat_id, media)[0].text is True def test_send_media_group_with_thumbs( self, bot, chat_id, video_file, photo_file, monkeypatch # noqa: F811 ): def test(*args, **kwargs): data = kwargs['fields'] video_check = data[input_video.media.attach] == input_video.media.field_tuple thumb_check = data[input_video.thumb.attach] == input_video.thumb.field_tuple result = video_check and thumb_check raise Exception(f"Test was {'successful' if result else 'failing'}") monkeypatch.setattr('telegram.utils.request.Request._request_wrapper', test) input_video = InputMediaVideo(video_file, thumb=photo_file) with pytest.raises(Exception, match='Test was successful'): bot.send_media_group(chat_id, [input_video, input_video]) @flaky(3, 1) # noqa: F811 def test_send_media_group_new_files( self, bot, chat_id, video_file, photo_file, animation_file # noqa: F811 ): # noqa: F811 def func(): with open('tests/data/telegram.jpg', 'rb') as file: return bot.send_media_group( chat_id, [ InputMediaVideo(video_file), InputMediaPhoto(photo_file), InputMediaPhoto(file.read()), ], ) messages = expect_bad_request( func, 'Type of file mismatch', 'Telegram did not accept the file.' ) assert isinstance(messages, list) 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) @flaky(3, 1) @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'], ) def test_send_media_group_default_allow_sending_without_reply( self, default_bot, chat_id, media_group, custom ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: messages = 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 = 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 not found'): default_bot.send_media_group( chat_id, media_group, reply_to_message_id=reply_to_message.message_id ) @flaky(3, 1) def test_edit_message_media(self, bot, chat_id, media_group): messages = bot.send_media_group(chat_id, media_group) cid = messages[-1].chat.id mid = messages[-1].message_id new_message = bot.edit_message_media(chat_id=cid, message_id=mid, media=media_group[0]) assert isinstance(new_message, Message) @flaky(3, 1) def test_edit_message_media_new_file(self, bot, chat_id, media_group, thumb_file): messages = bot.send_media_group(chat_id, media_group) cid = messages[-1].chat.id mid = messages[-1].message_id new_message = bot.edit_message_media( chat_id=cid, message_id=mid, media=InputMediaPhoto(thumb_file) ) assert isinstance(new_message, Message) def test_edit_message_media_with_thumb( self, bot, chat_id, video_file, photo_file, monkeypatch # noqa: F811 ): def test(*args, **kwargs): data = kwargs['fields'] video_check = data[input_video.media.attach] == input_video.media.field_tuple thumb_check = data[input_video.thumb.attach] == input_video.thumb.field_tuple result = video_check and thumb_check raise Exception(f"Test was {'successful' if result else 'failing'}") monkeypatch.setattr('telegram.utils.request.Request._request_wrapper', test) input_video = InputMediaVideo(video_file, thumb=photo_file) with pytest.raises(Exception, match='Test was successful'): bot.edit_message_media(chat_id=chat_id, message_id=123, media=input_video) @flaky(3, 1) @pytest.mark.parametrize( 'default_bot', [{'parse_mode': ParseMode.HTML}], indirect=True, ids=['HTML-Bot'] ) @pytest.mark.parametrize('media_type', ['animation', 'document', 'audio', 'photo', 'video']) 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) message = default_bot.send_photo(chat_id, photo) message = default_bot.edit_message_media( message.chat_id, message.message_id, media=build_media(parse_mode=ParseMode.HTML, med_type=media_type), ) assert message.caption == test_caption assert message.caption_entities == test_entities # Remove caption to avoid "Message not changed" message.edit_caption() message = default_bot.edit_message_media( message.chat_id, message.message_id, media=build_media(parse_mode=ParseMode.MARKDOWN_V2, med_type=media_type), ) assert message.caption == test_caption assert message.caption_entities == test_entities # Remove caption to avoid "Message not changed" message.edit_caption() message = default_bot.edit_message_media( message.chat_id, message.message_id, media=build_media(parse_mode=None, med_type=media_type), ) assert message.caption == markdown_caption assert message.caption_entities == [] python-telegram-bot-13.11/tests/test_inputtextmessagecontent.py000066400000000000000000000067741417656324400252310ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, ParseMode, MessageEntity @pytest.fixture(scope='class') def input_text_message_content(): return InputTextMessageContent( TestInputTextMessageContent.message_text, parse_mode=TestInputTextMessageContent.parse_mode, entities=TestInputTextMessageContent.entities, disable_web_page_preview=TestInputTextMessageContent.disable_web_page_preview, ) class TestInputTextMessageContent: message_text = '*message text*' parse_mode = ParseMode.MARKDOWN entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)] disable_web_page_preview = True def test_slot_behaviour(self, input_text_message_content, mro_slots, recwarn): inst = input_text_message_content for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.message_text = 'should give warning', self.message_text assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.disable_web_page_preview == self.disable_web_page_preview assert input_text_message_content.entities == self.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['disable_web_page_preview'] == input_text_message_content.disable_web_page_preview ) 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) python-telegram-bot-13.11/tests/test_inputvenuemessagecontent.py000066400000000000000000000112501417656324400253500ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def input_venue_message_content(): return InputVenueMessageContent( TestInputVenueMessageContent.latitude, TestInputVenueMessageContent.longitude, TestInputVenueMessageContent.title, TestInputVenueMessageContent.address, foursquare_id=TestInputVenueMessageContent.foursquare_id, foursquare_type=TestInputVenueMessageContent.foursquare_type, google_place_id=TestInputVenueMessageContent.google_place_id, google_place_type=TestInputVenueMessageContent.google_place_type, ) class TestInputVenueMessageContent: 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' def test_slot_behaviour(self, input_venue_message_content, recwarn, mro_slots): inst = input_venue_message_content for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.title = 'should give warning', self.title assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_invoice.py000066400000000000000000000255651417656324400216600ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import LabeledPrice, Invoice from telegram.error import BadRequest @pytest.fixture(scope='class') def invoice(): return Invoice( TestInvoice.title, TestInvoice.description, TestInvoice.start_parameter, TestInvoice.currency, TestInvoice.total_amount, ) class TestInvoice: 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] def test_slot_behaviour(self, invoice, mro_slots, recwarn): for attr in invoice.__slots__: assert getattr(invoice, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not invoice.__dict__, f"got missing slot(s): {invoice.__dict__}" assert len(mro_slots(invoice)) == len(set(mro_slots(invoice))), "duplicate slot" invoice.custom, invoice.title = 'should give warning', self.title assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self, bot): invoice_json = Invoice.de_json( { 'title': TestInvoice.title, 'description': TestInvoice.description, 'start_parameter': TestInvoice.start_parameter, 'currency': TestInvoice.currency, 'total_amount': TestInvoice.total_amount, }, bot, ) 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 @flaky(3, 1) def test_send_required_args_only(self, bot, chat_id, provider_token): message = 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 message.invoice.start_parameter == '' assert message.invoice.description == self.description assert message.invoice.title == self.title assert message.invoice.total_amount == self.total_amount @flaky(3, 1) def test_send_all_args(self, bot, chat_id, provider_token, monkeypatch): message = 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, ) assert message.invoice.currency == self.currency assert message.invoice.start_parameter == self.start_parameter assert message.invoice.description == self.description assert message.invoice.title == self.title assert message.invoice.total_amount == self.total_amount assert message.has_protected_content # We do this next one as safety guard to make sure that we pass all of the optional # parameters correctly because #2526 went unnoticed for 3 years … def make_assertion(*args, **_): kwargs = args[1] return ( kwargs['chat_id'] == 'chat_id' and kwargs['title'] == 'title' and kwargs['description'] == 'description' and kwargs['payload'] == 'payload' and kwargs['provider_token'] == 'provider_token' and kwargs['currency'] == 'currency' and kwargs['prices'] == [p.to_dict() for p in self.prices] and kwargs['max_tip_amount'] == 'max_tip_amount' and kwargs['suggested_tip_amounts'] == 'suggested_tip_amounts' and kwargs['start_parameter'] == 'start_parameter' and kwargs['provider_data'] == 'provider_data' and kwargs['photo_url'] == 'photo_url' and kwargs['photo_size'] == 'photo_size' and kwargs['photo_width'] == 'photo_width' and kwargs['photo_height'] == 'photo_height' and kwargs['need_name'] == 'need_name' and kwargs['need_phone_number'] == 'need_phone_number' and kwargs['need_email'] == 'need_email' and kwargs['need_shipping_address'] == 'need_shipping_address' and kwargs['send_phone_number_to_provider'] == 'send_phone_number_to_provider' and kwargs['send_email_to_provider'] == 'send_email_to_provider' and kwargs['is_flexible'] == 'is_flexible' ) monkeypatch.setattr(bot, '_message', make_assertion) assert bot.send_invoice( chat_id='chat_id', title='title', description='description', payload='payload', provider_token='provider_token', currency='currency', prices=self.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, ) def test_send_object_as_provider_data(self, monkeypatch, bot, chat_id, provider_token): def test(url, data, **kwargs): # depends on whether we're using ujson return data['provider_data'] in ['{"test_data": 123456789}', '{"test_data":123456789}'] monkeypatch.setattr(bot.request, 'post', test) assert 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, ) @flaky(3, 1) @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'], ) def test_send_invoice_default_allow_sending_without_reply( self, default_bot, chat_id, custom, provider_token ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): 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, ) 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) python-telegram-bot-13.11/tests/test_jobqueue.py000066400000000000000000000463621417656324400220410ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 calendar import datetime as dtm import logging import os import platform import time from queue import Queue from time import sleep import pytest import pytz from apscheduler.schedulers import SchedulerNotRunningError from flaky import flaky from telegram.ext import JobQueue, Updater, Job, CallbackContext, Dispatcher, ContextTypes class CustomContext(CallbackContext): pass @pytest.fixture(scope='function') def job_queue(bot, _dp): jq = JobQueue() jq.set_dispatcher(_dp) jq.start() yield jq jq.stop() @pytest.mark.skipif( os.getenv('GITHUB_ACTIONS', False) and platform.system() in ['Windows', 'Darwin'], reason="On Windows & MacOS precise timings are not accurate.", ) @flaky(10, 1) # Timings aren't quite perfect class TestJobQueue: result = 0 job_time = 0 received_error = None def test_slot_behaviour(self, job_queue, recwarn, mro_slots, _dp): for attr in job_queue.__slots__: assert getattr(job_queue, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not job_queue.__dict__, f"got missing slot(s): {job_queue.__dict__}" assert len(mro_slots(job_queue)) == len(set(mro_slots(job_queue))), "duplicate slot" job_queue.custom, job_queue._dispatcher = 'should give warning', _dp assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.result = 0 self.job_time = 0 self.received_error = None def job_run_once(self, bot, job): self.result += 1 def job_with_exception(self, bot, job=None): raise Exception('Test Error') def job_remove_self(self, bot, job): self.result += 1 job.schedule_removal() def job_run_once_with_context(self, bot, job): self.result += job.context def job_datetime_tests(self, bot, job): self.job_time = time.time() def job_context_based_callback(self, context): if ( isinstance(context, CallbackContext) and isinstance(context.job, Job) and isinstance(context.update_queue, Queue) and context.job.context == 2 and context.chat_data is None and context.user_data is None and isinstance(context.bot_data, dict) and context.job_queue is not context.job.job_queue ): self.result += 1 def error_handler(self, bot, update, error): self.received_error = str(error) def error_handler_context(self, update, context): self.received_error = str(context.error) def error_handler_raise_error(self, *args): raise Exception('Failing bigly') def test_run_once(self, job_queue): job_queue.run_once(self.job_run_once, 0.01) sleep(0.02) assert self.result == 1 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) sleep(0.001) assert self.result == 1 def test_job_with_context(self, job_queue): job_queue.run_once(self.job_run_once_with_context, 0.01, context=5) sleep(0.02) assert self.result == 5 def test_run_repeating(self, job_queue): job_queue.run_repeating(self.job_run_once, 0.02) sleep(0.05) assert self.result == 2 def test_run_repeating_first(self, job_queue): job_queue.run_repeating(self.job_run_once, 0.05, first=0.2) sleep(0.15) assert self.result == 0 sleep(0.07) assert self.result == 1 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.1, first=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.05) ) sleep(0.1) assert self.result == 1 def test_run_repeating_last(self, job_queue): job_queue.run_repeating(self.job_run_once, 0.05, last=0.06) sleep(0.1) assert self.result == 1 sleep(0.1) assert self.result == 1 def test_run_repeating_last_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.05, last=dtm.datetime.now(timezone) + dtm.timedelta(seconds=0.06) ) sleep(0.1) assert self.result == 1 sleep(0.1) assert self.result == 1 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.05, first=1, last=0.5) def test_run_repeating_timedelta(self, job_queue): job_queue.run_repeating(self.job_run_once, dtm.timedelta(minutes=3.3333e-4)) sleep(0.05) assert self.result == 2 def test_run_custom(self, job_queue): job_queue.run_custom(self.job_run_once, {'trigger': 'interval', 'seconds': 0.02}) sleep(0.05) assert self.result == 2 def test_multiple(self, job_queue): job_queue.run_once(self.job_run_once, 0.01) job_queue.run_once(self.job_run_once, 0.02) job_queue.run_repeating(self.job_run_once, 0.02) sleep(0.055) assert self.result == 4 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.05) j1.enabled = False j2.enabled = False sleep(0.06) assert self.result == 0 j1.enabled = True sleep(0.2) assert self.result == 1 def test_schedule_removal(self, job_queue): j1 = job_queue.run_once(self.job_run_once, 0.03) j2 = job_queue.run_repeating(self.job_run_once, 0.02) sleep(0.025) j1.schedule_removal() j2.schedule_removal() sleep(0.04) assert self.result == 1 def test_schedule_removal_from_within(self, job_queue): job_queue.run_repeating(self.job_remove_self, 0.01) sleep(0.05) assert self.result == 1 def test_longer_first(self, job_queue): job_queue.run_once(self.job_run_once, 0.02) job_queue.run_once(self.job_run_once, 0.01) sleep(0.015) assert self.result == 1 def test_error(self, job_queue): job_queue.run_repeating(self.job_with_exception, 0.01) job_queue.run_repeating(self.job_run_once, 0.02) sleep(0.03) assert self.result == 1 def test_in_updater(self, bot): u = Updater(bot=bot, use_context=False) u.job_queue.start() try: u.job_queue.run_repeating(self.job_run_once, 0.02) sleep(0.03) assert self.result == 1 u.stop() sleep(1) assert self.result == 1 finally: try: u.stop() except SchedulerNotRunningError: pass def test_time_unit_int(self, job_queue): # Testing seconds in int delta = 0.05 expected_time = time.time() + delta job_queue.run_once(self.job_datetime_tests, delta) sleep(0.06) assert pytest.approx(self.job_time) == expected_time 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.05) expected_time = time.time() + interval.total_seconds() job_queue.run_once(self.job_datetime_tests, interval) sleep(0.06) assert pytest.approx(self.job_time) == expected_time def test_time_unit_dt_datetime(self, job_queue): # Testing running at a specific datetime delta, now = dtm.timedelta(seconds=0.05), dtm.datetime.now(pytz.utc) when = now + delta expected_time = (now + delta).timestamp() job_queue.run_once(self.job_datetime_tests, when) sleep(0.06) assert self.job_time == pytest.approx(expected_time) def test_time_unit_dt_time_today(self, job_queue): # Testing running at a specific time today delta, now = 0.05, dtm.datetime.now(pytz.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) sleep(0.06) assert self.job_time == pytest.approx(expected_time) 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(pytz.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) def test_run_daily(self, job_queue): delta, now = 1, dtm.datetime.now(pytz.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) 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) 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) 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) 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, 31, day_is_strict=False) scheduled_time = job_queue.jobs()[0].next_t.timestamp() assert scheduled_time == pytest.approx(expected_reschedule_time) def test_default_tzinfo(self, _dp, 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 jq = JobQueue() original_bot = _dp.bot _dp.bot = tz_bot jq.set_dispatcher(_dp) try: jq.start() when = dtm.datetime.now(tz_bot.defaults.tzinfo) + dtm.timedelta(seconds=0.0005) jq.run_once(self.job_run_once, when.time()) sleep(0.001) assert self.result == 1 jq.stop() finally: _dp.bot = original_bot @pytest.mark.parametrize('use_context', [True, False]) def test_get_jobs(self, job_queue, use_context): job_queue._dispatcher.use_context = use_context if use_context: callback = self.job_context_based_callback else: callback = self.job_run_once job1 = job_queue.run_once(callback, 10, name='name1') job2 = job_queue.run_once(callback, 10, name='name1') job3 = job_queue.run_once(callback, 10, name='name2') 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,) def test_context_based_callback(self, job_queue): job_queue._dispatcher.use_context = True job_queue.run_once(self.job_context_based_callback, 0.01, context=2) sleep(0.03) assert self.result == 1 job_queue._dispatcher.use_context = False @pytest.mark.parametrize('use_context', [True, False]) def test_job_run(self, _dp, use_context): _dp.use_context = use_context job_queue = JobQueue() job_queue.set_dispatcher(_dp) if use_context: job = job_queue.run_repeating(self.job_context_based_callback, 0.02, context=2) else: job = job_queue.run_repeating(self.job_run_once, 0.02, context=2) assert self.result == 0 job.run(_dp) assert self.result == 1 def test_enable_disable_job(self, job_queue): job = job_queue.run_repeating(self.job_run_once, 0.02) sleep(0.05) assert self.result == 2 job.enabled = False assert not job.enabled sleep(0.05) assert self.result == 2 job.enabled = True assert job.enabled sleep(0.05) assert self.result == 4 def test_remove_job(self, job_queue): job = job_queue.run_repeating(self.job_run_once, 0.02) sleep(0.05) assert self.result == 2 assert not job.removed job.schedule_removal() assert job.removed sleep(0.05) assert self.result == 2 def test_job_lt_eq(self, job_queue): job = job_queue.run_repeating(self.job_run_once, 0.02) assert not job == job_queue assert not job < job def test_dispatch_error(self, job_queue, dp): dp.add_error_handler(self.error_handler) job = job_queue.run_once(self.job_with_exception, 0.05) sleep(0.1) assert self.received_error == 'Test Error' self.received_error = None job.run(dp) assert self.received_error == 'Test Error' # Remove handler dp.remove_error_handler(self.error_handler) self.received_error = None job = job_queue.run_once(self.job_with_exception, 0.05) sleep(0.1) assert self.received_error is None job.run(dp) assert self.received_error is None def test_dispatch_error_context(self, job_queue, cdp): cdp.add_error_handler(self.error_handler_context) job = job_queue.run_once(self.job_with_exception, 0.05) sleep(0.1) assert self.received_error == 'Test Error' self.received_error = None job.run(cdp) assert self.received_error == 'Test Error' # Remove handler cdp.remove_error_handler(self.error_handler_context) self.received_error = None job = job_queue.run_once(self.job_with_exception, 0.05) sleep(0.1) assert self.received_error is None job.run(cdp) assert self.received_error is None def test_dispatch_error_that_raises_errors(self, job_queue, dp, caplog): dp.add_error_handler(self.error_handler_raise_error) with caplog.at_level(logging.ERROR): job = job_queue.run_once(self.job_with_exception, 0.05) sleep(0.1) assert len(caplog.records) == 1 rec = caplog.records[-1] assert 'processing the job' in rec.getMessage() assert 'uncaught error was raised while handling' in rec.getMessage() caplog.clear() with caplog.at_level(logging.ERROR): job.run(dp) assert len(caplog.records) == 1 rec = caplog.records[-1] assert 'processing the job' in rec.getMessage() assert 'uncaught error was raised while handling' in rec.getMessage() caplog.clear() # Remove handler dp.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.05) sleep(0.1) 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): job.run(dp) assert len(caplog.records) == 1 rec = caplog.records[-1] assert 'No error handlers are registered' in rec.getMessage() def test_custom_context(self, bot, job_queue): dispatcher = Dispatcher( bot, Queue(), context_types=ContextTypes( context=CustomContext, bot_data=int, user_data=float, chat_data=complex ), ) job_queue.set_dispatcher(dispatcher) def callback(context): self.result = ( type(context), context.user_data, context.chat_data, type(context.bot_data), ) job_queue.run_once(callback, 0.1) sleep(0.15) assert self.result == (CustomContext, None, None, int) python-telegram-bot-13.11/tests/test_keyboardbutton.py000066400000000000000000000061551417656324400232520ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 KeyboardButton, InlineKeyboardButton from telegram.keyboardbuttonpolltype import KeyboardButtonPollType @pytest.fixture(scope='class') def keyboard_button(): return KeyboardButton( TestKeyboardButton.text, request_location=TestKeyboardButton.request_location, request_contact=TestKeyboardButton.request_contact, request_poll=TestKeyboardButton.request_poll, ) class TestKeyboardButton: text = 'text' request_location = True request_contact = True request_poll = KeyboardButtonPollType("quiz") def test_slot_behaviour(self, keyboard_button, recwarn, mro_slots): inst = keyboard_button for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.text = 'should give warning', self.text assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 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() def test_equality(self): a = KeyboardButton('test', request_contact=True) b = KeyboardButton('test', request_contact=True) c = KeyboardButton('Test', request_location=True) d = InlineKeyboardButton('test', callback_data='test') 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-13.11/tests/test_keyboardbuttonpolltype.py000066400000000000000000000042201417656324400250320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def keyboard_button_poll_type(): return KeyboardButtonPollType(TestKeyboardButtonPollType.type) class TestKeyboardButtonPollType: type = Poll.QUIZ def test_slot_behaviour(self, keyboard_button_poll_type, recwarn, mro_slots): inst = keyboard_button_poll_type for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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_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-13.11/tests/test_labeledprice.py000066400000000000000000000045061417656324400226270ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def labeled_price(): return LabeledPrice(TestLabeledPrice.label, TestLabeledPrice.amount) class TestLabeledPrice: label = 'label' amount = 100 def test_slot_behaviour(self, labeled_price, recwarn, mro_slots): inst = labeled_price for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.label = 'should give warning', self.label assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_location.py000066400000000000000000000226671417656324400220340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import Location from telegram.error import BadRequest @pytest.fixture(scope='class') def location(): return Location( latitude=TestLocation.latitude, longitude=TestLocation.longitude, horizontal_accuracy=TestLocation.horizontal_accuracy, live_period=TestLocation.live_period, heading=TestLocation.live_period, proximity_alert_radius=TestLocation.proximity_alert_radius, ) class TestLocation: latitude = -23.691288 longitude = -46.788279 horizontal_accuracy = 999 live_period = 60 heading = 90 proximity_alert_radius = 50 def test_slot_behaviour(self, location, recwarn, mro_slots): for attr in location.__slots__: assert getattr(location, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not location.__dict__, f"got missing slot(s): {location.__dict__}" assert len(mro_slots(location)) == len(set(mro_slots(location))), "duplicate slot" location.custom, location.heading = 'should give warning', self.heading assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self, bot): json_dict = { 'latitude': TestLocation.latitude, 'longitude': TestLocation.longitude, 'horizontal_accuracy': TestLocation.horizontal_accuracy, 'live_period': TestLocation.live_period, 'heading': TestLocation.heading, 'proximity_alert_radius': TestLocation.proximity_alert_radius, } location = Location.de_json(json_dict, bot) 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 @flaky(3, 1) @pytest.mark.xfail def test_send_live_location(self, bot, chat_id): message = 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(52.223880, message.location.latitude) assert pytest.approx(5.166146, message.location.longitude) 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 = 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(52.223098, message2.location.latitude) assert pytest.approx(5.164306, message2.location.longitude) assert message2.location.horizontal_accuracy == 30 assert message2.location.heading == 10 assert message2.location.proximity_alert_radius == 500 bot.stop_message_live_location(message.chat_id, message.message_id) with pytest.raises(BadRequest, match="Message can't be edited"): bot.edit_message_live_location( message.chat_id, message.message_id, latitude=52.223880, longitude=5.164306 ) # TODO: Needs improvement with in inline sent live location. def test_edit_live_inline_message(self, monkeypatch, bot, location): def make_assertion(url, data, **kwargs): lat = data['latitude'] == location.latitude lon = data['longitude'] == 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 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. def test_stop_live_inline_message(self, monkeypatch, bot): def test(url, data, **kwargs): id_ = data['inline_message_id'] == 1234 return id_ monkeypatch.setattr(bot.request, 'post', test) assert bot.stop_message_live_location(inline_message_id=1234) def test_send_with_location(self, monkeypatch, bot, chat_id, location): def test(url, data, **kwargs): lat = data['latitude'] == location.latitude lon = data['longitude'] == location.longitude return lat and lon monkeypatch.setattr(bot.request, 'post', test) assert bot.send_location(location=location, chat_id=chat_id) @flaky(3, 1) @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'], ) def test_send_location_default_allow_sending_without_reply( self, default_bot, chat_id, location, custom ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_location( chat_id, location=location, reply_to_message_id=reply_to_message.message_id ) def test_edit_live_location_with_location(self, monkeypatch, bot, location): def test(url, data, **kwargs): lat = data['latitude'] == location.latitude lon = data['longitude'] == location.longitude return lat and lon monkeypatch.setattr(bot.request, 'post', test) assert bot.edit_message_live_location(None, None, location=location) def test_send_location_without_required(self, bot, chat_id): with pytest.raises(ValueError, match='Either location or latitude and longitude'): bot.send_location(chat_id=chat_id) def test_edit_location_without_required(self, bot): with pytest.raises(ValueError, match='Either location or latitude and longitude'): bot.edit_message_live_location(chat_id=2, message_id=3) def test_send_location_with_all_args(self, bot, location): with pytest.raises(ValueError, match='Not both'): bot.send_location(chat_id=1, latitude=2.5, longitude=4.6, location=location) def test_edit_location_with_all_args(self, bot, location): with pytest.raises(ValueError, match='Not both'): bot.edit_message_live_location( chat_id=1, message_id=7, latitude=2.5, longitude=4.6, location=location ) 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) python-telegram-bot-13.11/tests/test_loginurl.py000066400000000000000000000053061417656324400220460ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def login_url(): return LoginUrl( url=TestLoginUrl.url, forward_text=TestLoginUrl.forward_text, bot_username=TestLoginUrl.bot_username, request_write_access=TestLoginUrl.request_write_access, ) class TestLoginUrl: url = "http://www.google.com" forward_text = "Send me forward!" bot_username = "botname" request_write_access = True def test_slot_behaviour(self, login_url, recwarn, mro_slots): for attr in login_url.__slots__: assert getattr(login_url, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not login_url.__dict__, f"got missing slot(s): {login_url.__dict__}" assert len(mro_slots(login_url)) == len(set(mro_slots(login_url))), "duplicate slot" login_url.custom, login_url.url = 'should give warning', self.url assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_message.py000066400000000000000000002005251417656324400216370ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ( Update, Message, User, MessageEntity, Chat, Audio, Document, Animation, Game, PhotoSize, Sticker, Video, Voice, VideoNote, Contact, Location, Venue, Invoice, SuccessfulPayment, PassportData, ParseMode, Poll, PollOption, ProximityAlertTriggered, Dice, Bot, ChatAction, VoiceChatStarted, VoiceChatEnded, VoiceChatParticipantsInvited, MessageAutoDeleteTimerChanged, VoiceChatScheduled, ) from telegram.ext import Defaults from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling from tests.test_passport import RAW_PASSPORT_DATA @pytest.fixture(scope='class') def message(bot): return Message( TestMessage.id_, TestMessage.date, TestMessage.chat, from_user=TestMessage.from_user, bot=bot, ) @pytest.fixture( scope='function', params=[ {'forward_from': User(99, 'forward_user', False), 'forward_date': datetime.utcnow()}, { 'forward_from_chat': Chat(-23, 'channel'), 'forward_from_message_id': 101, 'forward_date': datetime.utcnow(), }, {'reply_to_message': Message(50, None, None, None)}, {'edit_date': datetime.utcnow()}, { 'text': 'a text message', 'enitites': [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)}, {'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, None, None, None)}, {'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/'}, {'forward_signature': 'some_forward_sign'}, {'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 ) }, {'voice_chat_scheduled': VoiceChatScheduled(datetime.utcnow())}, {'voice_chat_started': VoiceChatStarted()}, {'voice_chat_ended': VoiceChatEnded(100)}, { 'voice_chat_participants_invited': VoiceChatParticipantsInvited( [User(1, 'Rem', False), User(2, 'Emilia', False)] ) }, {'sender_chat': Chat(-123, 'discussion_channel')}, {'is_automatic_forward': True}, {'has_protected_content': True}, ], ids=[ 'forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text', 'caption_entities', 'audio', 'document', 'animation', 'game', 'photo', 'sticker', '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', 'forward_signature', 'author_signature', 'photo_from_media_group', 'passport_data', 'poll', 'reply_markup', 'dice', 'via_bot', 'proximity_alert_triggered', 'voice_chat_scheduled', 'voice_chat_started', 'voice_chat_ended', 'voice_chat_participants_invited', 'sender_chat', 'is_automatic_forward', 'has_protected_content', ], ) def message_params(bot, request): return Message( message_id=TestMessage.id_, from_user=TestMessage.from_user, date=TestMessage.date, chat=TestMessage.chat, bot=bot, **request.param, ) class TestMessage: 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.' ) 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], ) def test_slot_behaviour(self, message, recwarn, mro_slots): for attr in message.__slots__: assert getattr(message, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not message.__dict__, f"got missing slot(s): {message.__dict__}" assert len(mro_slots(message)) == len(set(mro_slots(message))), "duplicate slot" message.custom, message.message_id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_all_possibilities_de_json_and_to_dict(self, bot, message_params): new = Message.de_json(message_params.to_dict(), bot) assert new.to_dict() == message_params.to_dict() def test_dict_approach(self, message): assert message['text'] == message.text assert message['chat_id'] == message.chat_id assert message['no_key'] is None 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, self.from_user, self.date, self.chat, text=text, entities=[entity]) assert message.parse_entity(entity) == 'http://google.com' 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, self.from_user, self.date, self.chat, caption=caption, caption_entities=[entity] ) assert message.parse_caption_entity(entity) == 'http://google.com' 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, self.from_user, self.date, 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'} 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, self.from_user, self.date, 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.' ) 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.' ) 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||\\.' ) text_markdown = self.test_message_v2.text_markdown_v2 assert text_markdown == test_md_string def test_text_markdown_new_in_v2(self, message): message.text = 'test' message.entities = [ MessageEntity(MessageEntity.BOLD, offset=0, length=4), MessageEntity(MessageEntity.ITALIC, offset=0, length=4), ] with pytest.raises(ValueError): assert message.text_markdown message.entities = [MessageEntity(MessageEntity.UNDERLINE, offset=0, length=4)] with pytest.raises(ValueError): message.text_markdown message.entities = [MessageEntity(MessageEntity.STRIKETHROUGH, offset=0, length=4)] with pytest.raises(ValueError): message.text_markdown message.entities = [MessageEntity(MessageEntity.SPOILER, offset=0, length=4)] with pytest.raises(ValueError): 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||\\.' ) 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, self.from_user, self.date, 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.from_user, self.date, self.chat, text=text, entities=[bold_entity] ) assert expected == message.text_markdown 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.' ) 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.' ) 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||\\.' ) 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||\\.' ) 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, self.from_user, self.date, 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, self.from_user, self.date, self.chat, caption=caption, caption_entities=[bold_entity], ) assert expected == message.caption_markdown 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 @pytest.mark.parametrize('_type', argvalues=[Chat.SUPERGROUP, Chat.CHANNEL]) def test_link_with_username(self, message, _type): message.chat.username = 'username' message.chat.type = _type assert message.link == f'https://t.me/{message.chat.username}/{message.message_id}' @pytest.mark.parametrize( '_type, _id', argvalues=[(Chat.CHANNEL, -1003), (Chat.SUPERGROUP, -1003)] ) def test_link_with_id(self, message, _type, _id): message.chat.username = None message.chat.id = _id message.chat.type = _type # The leading - for group ids/ -100 for supergroup ids isn't supposed to be in the link assert message.link == f'https://t.me/c/{3}/{message.message_id}' @pytest.mark.parametrize('_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): for i in ( 'audio', 'game', 'document', 'animation', 'photo', 'sticker', 'video', 'voice', 'video_note', 'contact', 'location', 'venue', 'invoice', 'successful_payment', ): item = getattr(message_params, i, None) if item: break else: item = None assert message_params.effective_attachment == item def test_reply_text(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id text = kwargs['text'] == 'test' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and text and reply assert check_shortcut_signature( Message.reply_text, Bot.send_message, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_text, message.bot, 'send_message') assert check_defaults_handling(message.reply_text, message.bot) monkeypatch.setattr(message.bot, 'send_message', make_assertion) assert message.reply_text('test') assert message.reply_text('test', quote=True) assert message.reply_text('test', reply_to_message_id=message.message_id, quote=True) 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\_' ) 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 if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return all([cid, markdown_text, reply, markdown_enabled]) assert check_shortcut_signature( Message.reply_markdown, Bot.send_message, ['chat_id', 'parse_mode'], ['quote'] ) assert check_shortcut_call(message.reply_text, message.bot, 'send_message') assert check_defaults_handling(message.reply_text, message.bot) text_markdown = self.test_message.text_markdown assert text_markdown == test_md_string monkeypatch.setattr(message.bot, 'send_message', make_assertion) assert message.reply_markdown(self.test_message.text_markdown) assert message.reply_markdown(self.test_message.text_markdown, quote=True) assert message.reply_markdown( self.test_message.text_markdown, reply_to_message_id=message.message_id, quote=True ) 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||\\.' ) 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 if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return all([cid, markdown_text, reply, markdown_enabled]) assert check_shortcut_signature( Message.reply_markdown_v2, Bot.send_message, ['chat_id', 'parse_mode'], ['quote'] ) assert check_shortcut_call(message.reply_text, message.bot, 'send_message') assert check_defaults_handling(message.reply_text, message.bot) text_markdown = self.test_message_v2.text_markdown_v2 assert text_markdown == test_md_string monkeypatch.setattr(message.bot, 'send_message', make_assertion) assert message.reply_markdown_v2(self.test_message_v2.text_markdown_v2) assert message.reply_markdown_v2(self.test_message_v2.text_markdown_v2, quote=True) assert message.reply_markdown_v2( self.test_message_v2.text_markdown_v2, reply_to_message_id=message.message_id, quote=True, ) 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.' ) 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 if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return all([cid, html_text, reply, html_enabled]) assert check_shortcut_signature( Message.reply_html, Bot.send_message, ['chat_id', 'parse_mode'], ['quote'] ) assert check_shortcut_call(message.reply_text, message.bot, 'send_message') assert check_defaults_handling(message.reply_text, message.bot) text_html = self.test_message_v2.text_html assert text_html == test_html_string monkeypatch.setattr(message.bot, 'send_message', make_assertion) assert message.reply_html(self.test_message_v2.text_html) assert message.reply_html(self.test_message_v2.text_html, quote=True) assert message.reply_html( self.test_message_v2.text_html, reply_to_message_id=message.message_id, quote=True ) def test_reply_media_group(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id media = kwargs['media'] == 'reply_media_group' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and media and reply assert check_shortcut_signature( Message.reply_media_group, Bot.send_media_group, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_media_group, message.bot, 'send_media_group') assert check_defaults_handling(message.reply_media_group, message.bot) monkeypatch.setattr(message.bot, 'send_media_group', make_assertion) assert message.reply_media_group(media='reply_media_group') assert message.reply_media_group(media='reply_media_group', quote=True) def test_reply_photo(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id photo = kwargs['photo'] == 'test_photo' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and photo and reply assert check_shortcut_signature( Message.reply_photo, Bot.send_photo, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_photo, message.bot, 'send_photo') assert check_defaults_handling(message.reply_photo, message.bot) monkeypatch.setattr(message.bot, 'send_photo', make_assertion) assert message.reply_photo(photo='test_photo') assert message.reply_photo(photo='test_photo', quote=True) def test_reply_audio(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id audio = kwargs['audio'] == 'test_audio' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and audio and reply assert check_shortcut_signature( Message.reply_audio, Bot.send_audio, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_audio, message.bot, 'send_audio') assert check_defaults_handling(message.reply_audio, message.bot) monkeypatch.setattr(message.bot, 'send_audio', make_assertion) assert message.reply_audio(audio='test_audio') assert message.reply_audio(audio='test_audio', quote=True) def test_reply_document(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id document = kwargs['document'] == 'test_document' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and document and reply assert check_shortcut_signature( Message.reply_document, Bot.send_document, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_document, message.bot, 'send_document') assert check_defaults_handling(message.reply_document, message.bot) monkeypatch.setattr(message.bot, 'send_document', make_assertion) assert message.reply_document(document='test_document') assert message.reply_document(document='test_document', quote=True) def test_reply_animation(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id animation = kwargs['animation'] == 'test_animation' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and animation and reply assert check_shortcut_signature( Message.reply_animation, Bot.send_animation, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_animation, message.bot, 'send_animation') assert check_defaults_handling(message.reply_animation, message.bot) monkeypatch.setattr(message.bot, 'send_animation', make_assertion) assert message.reply_animation(animation='test_animation') assert message.reply_animation(animation='test_animation', quote=True) def test_reply_sticker(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id sticker = kwargs['sticker'] == 'test_sticker' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and sticker and reply assert check_shortcut_signature( Message.reply_sticker, Bot.send_sticker, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_sticker, message.bot, 'send_sticker') assert check_defaults_handling(message.reply_sticker, message.bot) monkeypatch.setattr(message.bot, 'send_sticker', make_assertion) assert message.reply_sticker(sticker='test_sticker') assert message.reply_sticker(sticker='test_sticker', quote=True) def test_reply_video(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id video = kwargs['video'] == 'test_video' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and video and reply assert check_shortcut_signature( Message.reply_video, Bot.send_video, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_video, message.bot, 'send_video') assert check_defaults_handling(message.reply_video, message.bot) monkeypatch.setattr(message.bot, 'send_video', make_assertion) assert message.reply_video(video='test_video') assert message.reply_video(video='test_video', quote=True) def test_reply_video_note(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id video_note = kwargs['video_note'] == 'test_video_note' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and video_note and reply assert check_shortcut_signature( Message.reply_video_note, Bot.send_video_note, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_video_note, message.bot, 'send_video_note') assert check_defaults_handling(message.reply_video_note, message.bot) monkeypatch.setattr(message.bot, 'send_video_note', make_assertion) assert message.reply_video_note(video_note='test_video_note') assert message.reply_video_note(video_note='test_video_note', quote=True) def test_reply_voice(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id voice = kwargs['voice'] == 'test_voice' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and voice and reply assert check_shortcut_signature( Message.reply_voice, Bot.send_voice, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_voice, message.bot, 'send_voice') assert check_defaults_handling(message.reply_voice, message.bot) monkeypatch.setattr(message.bot, 'send_voice', make_assertion) assert message.reply_voice(voice='test_voice') assert message.reply_voice(voice='test_voice', quote=True) def test_reply_location(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id location = kwargs['location'] == 'test_location' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and location and reply assert check_shortcut_signature( Message.reply_location, Bot.send_location, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_location, message.bot, 'send_location') assert check_defaults_handling(message.reply_location, message.bot) monkeypatch.setattr(message.bot, 'send_location', make_assertion) assert message.reply_location(location='test_location') assert message.reply_location(location='test_location', quote=True) def test_reply_venue(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id venue = kwargs['venue'] == 'test_venue' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and venue and reply assert check_shortcut_signature( Message.reply_venue, Bot.send_venue, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_venue, message.bot, 'send_venue') assert check_defaults_handling(message.reply_venue, message.bot) monkeypatch.setattr(message.bot, 'send_venue', make_assertion) assert message.reply_venue(venue='test_venue') assert message.reply_venue(venue='test_venue', quote=True) def test_reply_contact(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id contact = kwargs['contact'] == 'test_contact' if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and contact and reply assert check_shortcut_signature( Message.reply_contact, Bot.send_contact, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.reply_contact, message.bot, 'send_contact') assert check_defaults_handling(message.reply_contact, message.bot) monkeypatch.setattr(message.bot, 'send_contact', make_assertion) assert message.reply_contact(contact='test_contact') assert message.reply_contact(contact='test_contact', quote=True) def test_reply_poll(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id question = kwargs['question'] == 'test_poll' options = kwargs['options'] == ['1', '2', '3'] if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and question and options and reply assert check_shortcut_signature(Message.reply_poll, Bot.send_poll, ['chat_id'], ['quote']) assert check_shortcut_call(message.reply_poll, message.bot, 'send_poll') assert check_defaults_handling(message.reply_poll, message.bot) monkeypatch.setattr(message.bot, 'send_poll', make_assertion) assert message.reply_poll(question='test_poll', options=['1', '2', '3']) assert message.reply_poll(question='test_poll', quote=True, options=['1', '2', '3']) def test_reply_dice(self, monkeypatch, message): def make_assertion(*_, **kwargs): id_ = kwargs['chat_id'] == message.chat_id contact = kwargs['disable_notification'] is True if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id_ and contact and reply assert check_shortcut_signature(Message.reply_dice, Bot.send_dice, ['chat_id'], ['quote']) assert check_shortcut_call(message.reply_dice, message.bot, 'send_dice') assert check_defaults_handling(message.reply_dice, message.bot) monkeypatch.setattr(message.bot, 'send_dice', make_assertion) assert message.reply_dice(disable_notification=True) assert message.reply_dice(disable_notification=True, quote=True) def test_reply_action(self, monkeypatch, message: Message): 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'], [] ) assert check_shortcut_call(message.reply_chat_action, message.bot, 'send_chat_action') assert check_defaults_handling(message.reply_chat_action, message.bot) monkeypatch.setattr(message.bot, 'send_chat_action', make_assertion) assert message.reply_chat_action(action=ChatAction.TYPING) def test_reply_game(self, monkeypatch, message: Message): 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'], ['quote']) assert check_shortcut_call(message.reply_game, message.bot, 'send_game') assert check_defaults_handling(message.reply_game, message.bot) monkeypatch.setattr(message.bot, 'send_game', make_assertion) assert message.reply_game(game_short_name='test_game') assert message.reply_game(game_short_name='test_game', quote=True) def test_reply_invoice(self, monkeypatch, message: Message): 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'], ['quote'] ) assert check_shortcut_call(message.reply_invoice, message.bot, 'send_invoice') assert check_defaults_handling(message.reply_invoice, message.bot) monkeypatch.setattr(message.bot, 'send_invoice', make_assertion) assert message.reply_invoice( 'title', 'description', 'payload', 'provider_token', 'currency', 'prices', ) assert message.reply_invoice( 'title', 'description', 'payload', 'provider_token', 'currency', 'prices', quote=True, ) @pytest.mark.parametrize('disable_notification,protected', [(False, True), (True, False)]) def test_forward(self, monkeypatch, message, disable_notification, protected): 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 check_shortcut_call(message.forward, message.bot, 'forward_message') assert check_defaults_handling(message.forward, message.bot) monkeypatch.setattr(message.bot, 'forward_message', make_assertion) assert message.forward( 123456, disable_notification=disable_notification, protect_content=protected ) assert not message.forward(635241) @pytest.mark.parametrize('disable_notification,protected', [(True, False), (False, True)]) def test_copy(self, monkeypatch, message, disable_notification, protected): keyboard = [[1, 2]] 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 check_shortcut_call(message.copy, message.bot, 'copy_message') assert check_defaults_handling(message.copy, message.bot) monkeypatch.setattr(message.bot, 'copy_message', make_assertion) assert message.copy( 123456, disable_notification=disable_notification, protect_content=protected ) assert message.copy( 123456, reply_markup=keyboard, disable_notification=disable_notification, protect_content=protected, ) assert not message.copy(635241) @pytest.mark.parametrize('disable_notification,protected', [(True, False), (False, True)]) def test_reply_copy(self, monkeypatch, message, disable_notification, protected): keyboard = [[1, 2]] 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 if kwargs.get('reply_to_message_id') is not None: reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return ( chat_id and from_chat and message_id and notification and reply_markup and reply and is_protected ) assert check_shortcut_signature( Message.reply_copy, Bot.copy_message, ['chat_id'], ['quote'] ) assert check_shortcut_call(message.copy, message.bot, 'copy_message') assert check_defaults_handling(message.copy, message.bot) monkeypatch.setattr(message.bot, 'copy_message', make_assertion) assert message.reply_copy( 123456, 456789, disable_notification=disable_notification, protect_content=protected ) assert message.reply_copy( 123456, 456789, reply_markup=keyboard, disable_notification=disable_notification, protect_content=protected, ) assert message.reply_copy( 123456, 456789, quote=True, disable_notification=disable_notification, protect_content=protected, ) assert message.reply_copy( 123456, 456789, quote=True, reply_to_message_id=message.message_id, disable_notification=disable_notification, protect_content=protected, ) def test_edit_text(self, monkeypatch, message): 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 check_shortcut_call( message.edit_text, message.bot, 'edit_message_text', skip_params=['inline_message_id'], shortcut_kwargs=['message_id', 'chat_id'], ) assert check_defaults_handling(message.edit_text, message.bot) monkeypatch.setattr(message.bot, 'edit_message_text', make_assertion) assert message.edit_text(text='test') def test_edit_caption(self, monkeypatch, message): 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 check_shortcut_call( message.edit_caption, message.bot, 'edit_message_caption', skip_params=['inline_message_id'], shortcut_kwargs=['message_id', 'chat_id'], ) assert check_defaults_handling(message.edit_caption, message.bot) monkeypatch.setattr(message.bot, 'edit_message_caption', make_assertion) assert message.edit_caption(caption='new caption') def test_edit_media(self, monkeypatch, message): 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 check_shortcut_call( message.edit_media, message.bot, 'edit_message_media', skip_params=['inline_message_id'], shortcut_kwargs=['message_id', 'chat_id'], ) assert check_defaults_handling(message.edit_media, message.bot) monkeypatch.setattr(message.bot, 'edit_message_media', make_assertion) assert message.edit_media('my_media') def test_edit_reply_markup(self, monkeypatch, message): 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 check_shortcut_call( message.edit_reply_markup, message.bot, 'edit_message_reply_markup', skip_params=['inline_message_id'], shortcut_kwargs=['message_id', 'chat_id'], ) assert check_defaults_handling(message.edit_reply_markup, message.bot) monkeypatch.setattr(message.bot, 'edit_message_reply_markup', make_assertion) assert message.edit_reply_markup(reply_markup=[['1', '2']]) def test_edit_live_location(self, monkeypatch, message): 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 check_shortcut_call( message.edit_live_location, message.bot, 'edit_message_live_location', skip_params=['inline_message_id'], shortcut_kwargs=['message_id', 'chat_id'], ) assert check_defaults_handling(message.edit_live_location, message.bot) monkeypatch.setattr(message.bot, 'edit_message_live_location', make_assertion) assert message.edit_live_location(latitude=1, longitude=2) def test_stop_live_location(self, monkeypatch, message): 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 check_shortcut_call( message.stop_live_location, message.bot, 'stop_message_live_location', skip_params=['inline_message_id'], shortcut_kwargs=['message_id', 'chat_id'], ) assert check_defaults_handling(message.stop_live_location, message.bot) monkeypatch.setattr(message.bot, 'stop_message_live_location', make_assertion) assert message.stop_live_location() def test_set_game_score(self, monkeypatch, message): 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 check_shortcut_call( message.set_game_score, message.bot, 'set_game_score', skip_params=['inline_message_id'], shortcut_kwargs=['message_id', 'chat_id'], ) assert check_defaults_handling(message.set_game_score, message.bot) monkeypatch.setattr(message.bot, 'set_game_score', make_assertion) assert message.set_game_score(user_id=1, score=2) def test_get_game_high_scores(self, monkeypatch, message): 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 check_shortcut_call( message.get_game_high_scores, message.bot, 'get_game_high_scores', skip_params=['inline_message_id'], shortcut_kwargs=['message_id', 'chat_id'], ) assert check_defaults_handling(message.get_game_high_scores, message.bot) monkeypatch.setattr(message.bot, 'get_game_high_scores', make_assertion) assert message.get_game_high_scores(user_id=1) def test_delete(self, monkeypatch, message): 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 check_shortcut_call(message.delete, message.bot, 'delete_message') assert check_defaults_handling(message.delete, message.bot) monkeypatch.setattr(message.bot, 'delete_message', make_assertion) assert message.delete() def test_stop_poll(self, monkeypatch, message): 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 check_shortcut_call(message.stop_poll, message.bot, 'stop_poll') assert check_defaults_handling(message.stop_poll, message.bot) monkeypatch.setattr(message.bot, 'stop_poll', make_assertion) assert message.stop_poll() def test_pin(self, monkeypatch, message): 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 check_shortcut_call(message.pin, message.bot, 'pin_chat_message') assert check_defaults_handling(message.pin, message.bot) monkeypatch.setattr(message.bot, 'pin_chat_message', make_assertion) assert message.pin() def test_unpin(self, monkeypatch, message): 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 check_shortcut_call( message.unpin, message.bot, 'unpin_chat_message', shortcut_kwargs=['chat_id', 'message_id'], ) assert check_defaults_handling(message.unpin, message.bot) monkeypatch.setattr(message.bot, 'unpin_chat_message', make_assertion) assert message.unpin() def test_default_quote(self, message): message.bot.defaults = Defaults() try: message.bot.defaults._quote = False assert message._quote(None, None) is None message.bot.defaults._quote = True assert message._quote(None, None) == message.message_id message.bot.defaults._quote = None message.chat.type = Chat.PRIVATE assert message._quote(None, None) is None message.chat.type = Chat.GROUP assert message._quote(None, None) finally: message.bot.defaults = None 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) python-telegram-bot-13.11/tests/test_messageautodeletetimerchanged.py000066400000000000000000000045571417656324400262750ustar00rootroot00000000000000#!/usr/bin/env python # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, VoiceChatEnded class TestMessageAutoDeleteTimerChanged: message_auto_delete_time = 100 def test_slot_behaviour(self, recwarn, mro_slots): action = MessageAutoDeleteTimerChanged(self.message_auto_delete_time) for attr in action.__slots__: assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not action.__dict__, f"got missing slot(s): {action.__dict__}" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" action.custom = 'should give warning' assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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 = VoiceChatEnded(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-13.11/tests/test_messageentity.py000066400000000000000000000063561417656324400231020ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope="class", 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 TestMessageEntity: type_ = 'url' offset = 1 length = 2 url = 'url' def test_slot_behaviour(self, message_entity, recwarn, mro_slots): inst = message_entity for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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_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-13.11/tests/test_messagehandler.py000066400000000000000000000307721417656324400232020ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram.utils.deprecate import TelegramDeprecationWarning from telegram import ( Message, Update, Chat, Bot, User, CallbackQuery, InlineQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, ) from telegram.ext import Filters, MessageHandler, CallbackContext, JobQueue, UpdateFilter 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): return Message(1, None, Chat(1, ''), from_user=User(1, '', False), bot=bot) class TestMessageHandler: test_flag = False SRE_TYPE = type(re.match("", "")) def test_slot_behaviour(self, recwarn, mro_slots): handler = MessageHandler(Filters.all, self.callback_basic) for attr in handler.__slots__: assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" handler.custom, handler.callback = 'should give warning', self.callback_basic assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(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_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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_context_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_context_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_basic(self, dp, message): handler = MessageHandler(None, self.callback_basic) dp.add_handler(handler) assert handler.check_update(Update(0, message)) dp.process_update(Update(0, message)) assert self.test_flag def test_deprecation_warning(self): with pytest.warns(TelegramDeprecationWarning, match='See https://git.io/fxJuV'): MessageHandler(None, self.callback_basic, edited_updates=True) with pytest.warns(TelegramDeprecationWarning, match='See https://git.io/fxJuV'): MessageHandler(None, self.callback_basic, message_updates=False) with pytest.warns(TelegramDeprecationWarning, match='See https://git.io/fxJuV'): MessageHandler(None, self.callback_basic, channel_post_updates=True) def test_edited_deprecated(self, message): handler = MessageHandler( None, self.callback_basic, edited_updates=True, message_updates=False, channel_post_updates=False, ) assert 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_channel_post_deprecated(self, message): handler = MessageHandler( None, self.callback_basic, edited_updates=False, message_updates=False, channel_post_updates=True, ) assert not handler.check_update(Update(0, edited_message=message)) assert not handler.check_update(Update(0, message=message)) assert handler.check_update(Update(0, channel_post=message)) assert not handler.check_update(Update(0, edited_channel_post=message)) def test_multiple_flags_deprecated(self, message): handler = MessageHandler( None, self.callback_basic, edited_updates=True, message_updates=True, channel_post_updates=True, ) assert handler.check_update(Update(0, edited_message=message)) assert handler.check_update(Update(0, message=message)) assert handler.check_update(Update(0, channel_post=message)) assert handler.check_update(Update(0, edited_channel_post=message)) def test_none_allowed_deprecated(self): with pytest.raises(ValueError, match='are all False'): MessageHandler( None, self.callback_basic, message_updates=False, channel_post_updates=False, edited_updates=False, ) def test_with_filter(self, message): handler = MessageHandler(Filters.group, self.callback_basic) 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(UpdateFilter): flag = False def filter(self, u): self.flag = True test_filter = TestFilter() handler = MessageHandler(test_filter, self.callback_basic) 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.update.messages & ~Filters.update.channel_post & Filters.update.edited_channel_post ) handler = MessageHandler(f, self.callback_basic) 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_pass_user_or_chat_data(self, dp, message): handler = MessageHandler(None, self.callback_data_1, pass_user_data=True) dp.add_handler(handler) dp.process_update(Update(0, message=message)) assert self.test_flag dp.remove_handler(handler) handler = MessageHandler(None, self.callback_data_1, pass_chat_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(Update(0, message=message)) assert self.test_flag dp.remove_handler(handler) handler = MessageHandler( None, self.callback_data_2, pass_chat_data=True, pass_user_data=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(Update(0, message=message)) assert self.test_flag def test_pass_job_or_update_queue(self, dp, message): handler = MessageHandler(None, self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update(Update(0, message=message)) assert self.test_flag dp.remove_handler(handler) handler = MessageHandler(None, self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(Update(0, message=message)) assert self.test_flag dp.remove_handler(handler) handler = MessageHandler( None, self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(Update(0, message=message)) assert self.test_flag def test_other_update_types(self, false_update): handler = MessageHandler(None, self.callback_basic, edited_updates=True) assert not handler.check_update(false_update) def test_context(self, cdp, message): handler = MessageHandler( None, self.callback_context, edited_updates=True, channel_post_updates=True ) cdp.add_handler(handler) cdp.process_update(Update(0, message=message)) assert self.test_flag self.test_flag = False cdp.process_update(Update(0, edited_message=message)) assert self.test_flag self.test_flag = False cdp.process_update(Update(0, channel_post=message)) assert self.test_flag self.test_flag = False cdp.process_update(Update(0, edited_channel_post=message)) assert self.test_flag def test_context_regex(self, cdp, message): handler = MessageHandler(Filters.regex('one two'), self.callback_context_regex1) cdp.add_handler(handler) message.text = 'not it' cdp.process_update(Update(0, message)) assert not self.test_flag message.text += ' one two now it is' cdp.process_update(Update(0, message)) assert self.test_flag def test_context_multiple_regex(self, cdp, message): handler = MessageHandler( Filters.regex('one') & Filters.regex('two'), self.callback_context_regex2 ) cdp.add_handler(handler) message.text = 'not it' cdp.process_update(Update(0, message)) assert not self.test_flag message.text += ' one two now it is' cdp.process_update(Update(0, message)) assert self.test_flag python-telegram-bot-13.11/tests/test_messageid.py000066400000000000000000000044021417656324400221500ustar00rootroot00000000000000#!/usr/bin/env python # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope="class") def message_id(): return MessageId(message_id=TestMessageId.m_id) class TestMessageId: m_id = 1234 def test_slot_behaviour(self, message_id, recwarn, mro_slots): for attr in message_id.__slots__: assert getattr(message_id, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not message_id.__dict__, f"got missing slot(s): {message_id.__dict__}" assert len(mro_slots(message_id)) == len(set(mro_slots(message_id))), "duplicate slot" message_id.custom, message_id.message_id = 'should give warning', self.m_id assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self): json_dict = {'message_id': self.m_id} message_id = MessageId.de_json(json_dict, None) 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-13.11/tests/test_messagequeue.py000066400000000000000000000044421417656324400227040ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 time import sleep, perf_counter import pytest import telegram.ext.messagequeue as mq @pytest.mark.skipif( os.getenv('GITHUB_ACTIONS', False) and os.name == 'nt', reason="On windows precise timings are not accurate.", ) class TestDelayQueue: N = 128 burst_limit = 30 time_limit_ms = 1000 margin_ms = 0 testtimes = [] def call(self): self.testtimes.append(perf_counter()) def test_delayqueue_limits(self): dsp = mq.DelayQueue( burst_limit=self.burst_limit, time_limit_ms=self.time_limit_ms, autostart=True ) assert dsp.is_alive() is True for _ in range(self.N): dsp(self.call) starttime = perf_counter() # wait up to 20 sec more than needed app_endtime = (self.N * self.burst_limit / (1000 * self.time_limit_ms)) + starttime + 20 while not dsp._queue.empty() and perf_counter() < app_endtime: sleep(1) assert dsp._queue.empty() is True # check loop exit condition dsp.stop() assert dsp.is_alive() is False assert self.testtimes or self.N == 0 passes, fails = [], [] delta = (self.time_limit_ms - self.margin_ms) / 1000 for start, stop in enumerate(range(self.burst_limit + 1, len(self.testtimes))): part = self.testtimes[start:stop] if (part[-1] - part[0]) >= delta: passes.append(part) else: fails.append(part) assert not fails python-telegram-bot-13.11/tests/test_meta.py000066400000000000000000000024231417656324400211360ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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.conftest import env_var_2_bool @pytest.mark.skipif( not env_var_2_bool(os.getenv('TEST_BUILD', False)), reason='TEST_BUILD not enabled' ) def test_build(): assert os.system('python setup.py bdist_dumb') == 0 # pragma: no cover @pytest.mark.skipif( not env_var_2_bool(os.getenv('TEST_BUILD', False)), reason='TEST_BUILD not enabled' ) def test_build_raw(): assert os.system('python setup-raw.py bdist_dumb') == 0 # pragma: no cover python-telegram-bot-13.11/tests/test_no_passport.py000066400000000000000000000062711417656324400225640ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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_NO_PASSPORT environment variable set in addition to the regular test suite. Because actually uninstalling the optional dependencies would lead to errors in the test suite we just mock the import to raise the expected exception. Note that a fixture that just does this for every test that needs it is a nice idea, but for some reason makes test_updater.py hang indefinitely on GitHub Actions (at least when Hinrich tried that) """ import os from importlib import reload from unittest import mock import pytest from telegram import bot from telegram.passport import credentials from tests.conftest import env_var_2_bool TEST_NO_PASSPORT = env_var_2_bool(os.getenv('TEST_NO_PASSPORT', False)) if TEST_NO_PASSPORT: orig_import = __import__ def import_mock(module_name, *args, **kwargs): if module_name.startswith('cryptography'): raise ModuleNotFoundError('We are testing without cryptography here') return orig_import(module_name, *args, **kwargs) with mock.patch('builtins.__import__', side_effect=import_mock): reload(bot) reload(credentials) class TestNoPassport: """ The monkeypatches simulate cryptography not being installed even when TEST_NO_PASSPORT is False, though that doesn't test the actual imports """ def test_bot_init(self, bot_info, monkeypatch): if not TEST_NO_PASSPORT: monkeypatch.setattr(bot, 'CRYPTO_INSTALLED', False) with pytest.raises(RuntimeError, match='passport'): bot.Bot(bot_info['token'], private_key=1, private_key_password=2) def test_credentials_decrypt(self, monkeypatch): if not TEST_NO_PASSPORT: monkeypatch.setattr(credentials, 'CRYPTO_INSTALLED', False) with pytest.raises(RuntimeError, match='passport'): credentials.decrypt(1, 1, 1) def test_encrypted_credentials_decrypted_secret(self, monkeypatch): if not TEST_NO_PASSPORT: monkeypatch.setattr(credentials, 'CRYPTO_INSTALLED', False) ec = credentials.EncryptedCredentials('data', 'hash', 'secret') with pytest.raises(RuntimeError, match='passport'): ec.decrypted_secret python-telegram-bot-13.11/tests/test_official.py000066400000000000000000000152711417656324400217710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 inspect import certifi import pytest from bs4 import BeautifulSoup from telegram.vendor.ptb_urllib3 import urllib3 import telegram from tests.conftest import env_var_2_bool IGNORED_OBJECTS = ('ResponseParameters', 'CallbackGame') IGNORED_PARAMETERS = { 'self', 'args', '_kwargs', 'read_latency', 'network_delay', 'timeout', 'bot', 'api_kwargs', } def find_next_sibling_until(tag, name, until): for sibling in tag.next_siblings: if sibling is until: return if sibling.name == name: return sibling def parse_table(h4): table = find_next_sibling_until(h4, 'table', h4.find_next_sibling('h4')) if not table: return [] t = [] for tr in table.find_all('tr')[1:]: t.append([td.text for td in tr.find_all('td')]) return t def check_method(h4): name = h4.text method = getattr(telegram.Bot, name) table = parse_table(h4) # Check arguments based on source sig = inspect.signature(method, follow_wrapped=True) checked = [] for parameter in table: param = sig.parameters.get(parameter[0]) assert param is not None, f"Parameter {parameter[0]} not found in {method.__name__}" # TODO: Check type via docstring # TODO: Check if optional or required checked.append(parameter[0]) ignored = IGNORED_PARAMETERS.copy() if name == 'getUpdates': ignored -= {'timeout'} # Has it's own timeout parameter that we do wanna check for elif name in ( f'send{media_type}' for media_type in [ 'Animation', 'Audio', 'Document', 'Photo', 'Video', 'VideoNote', 'Voice', ] ): ignored |= {'filename'} # Convenience parameter elif name == 'setGameScore': ignored |= {'edit_message'} # TODO: Now deprecated, so no longer in telegrams docs elif name == 'sendContact': ignored |= {'contact'} # Added for ease of use elif name in ['sendLocation', 'editMessageLiveLocation']: ignored |= {'location'} # Added for ease of use elif name == 'sendVenue': ignored |= {'venue'} # Added for ease of use elif name == 'answerInlineQuery': ignored |= {'current_offset'} # Added for ease of use assert (sig.parameters.keys() ^ checked) - ignored == set() def check_object(h4): name = h4.text obj = getattr(telegram, name) table = parse_table(h4) # Check arguments based on source sig = inspect.signature(obj, follow_wrapped=True) checked = [] for parameter in table: field = parameter[0] if field == 'from': field = 'from_user' elif ( name.startswith('InlineQueryResult') or name.startswith('InputMedia') or name.startswith('BotCommandScope') ) and field == 'type': continue elif (name.startswith('ChatMember')) and field == 'status': continue elif ( name.startswith('PassportElementError') and field == 'source' ) or field == 'remove_keyboard': continue param = sig.parameters.get(field) assert param is not None, f"Attribute {field} not found in {obj.__name__}" # TODO: Check type via docstring # TODO: Check if optional or required checked.append(field) ignored = IGNORED_PARAMETERS.copy() if name == 'InputFile': return if name == 'InlineQueryResult': ignored |= {'id', 'type'} # attributes common to all subclasses if name == 'ChatMember': ignored |= {'user', 'status'} # attributes common to all subclasses if name == 'ChatMember': ignored |= { 'can_add_web_page_previews', # for backwards compatibility 'can_be_edited', 'can_change_info', 'can_delete_messages', 'can_edit_messages', 'can_invite_users', 'can_manage_chat', 'can_manage_voice_chats', 'can_pin_messages', 'can_post_messages', 'can_promote_members', 'can_restrict_members', 'can_send_media_messages', 'can_send_messages', 'can_send_other_messages', 'can_send_polls', 'custom_title', 'is_anonymous', 'is_member', 'until_date', } if name == 'BotCommandScope': ignored |= {'type'} # attributes common to all subclasses elif name == 'User': ignored |= {'type'} # TODO: Deprecation elif name in ('PassportFile', 'EncryptedPassportElement'): ignored |= {'credentials'} elif name == 'PassportElementError': ignored |= {'message', 'type', 'source'} elif name.startswith('InputMedia'): ignored |= {'filename'} # Convenience parameter assert (sig.parameters.keys() ^ checked) - ignored == set() argvalues = [] names = [] http = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where()) request = http.request('GET', 'https://core.telegram.org/bots/api') soup = BeautifulSoup(request.data.decode('utf-8'), 'html.parser') for thing in soup.select('h4 > a.anchor'): # Methods and types don't have spaces in them, luckily all other sections of the docs do # TODO: don't depend on that if '-' not in thing['name']: h4 = thing.parent # Is it a method if h4.text[0].lower() == h4.text[0]: argvalues.append((check_method, h4)) names.append(h4.text) elif h4.text not in IGNORED_OBJECTS: # Or a type/object argvalues.append((check_object, h4)) names.append(h4.text) @pytest.mark.parametrize(('method', 'data'), argvalues=argvalues, ids=names) @pytest.mark.skipif( not env_var_2_bool(os.getenv('TEST_OFFICIAL')), reason='test_official is not enabled' ) def test_official(method, data): method(data) python-telegram-bot-13.11/tests/test_orderinfo.py000066400000000000000000000073771417656324400222140ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, OrderInfo @pytest.fixture(scope='class') def order_info(): return OrderInfo( TestOrderInfo.name, TestOrderInfo.phone_number, TestOrderInfo.email, TestOrderInfo.shipping_address, ) class TestOrderInfo: name = 'name' phone_number = 'phone_number' email = 'email' shipping_address = ShippingAddress('GB', '', 'London', '12 Grimmauld Place', '', 'WC1') def test_slot_behaviour(self, order_info, mro_slots, recwarn): for attr in order_info.__slots__: assert getattr(order_info, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not order_info.__dict__, f"got missing slot(s): {order_info.__dict__}" assert len(mro_slots(order_info)) == len(set(mro_slots(order_info))), "duplicate slot" order_info.custom, order_info.name = 'should give warning', self.name assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self, bot): json_dict = { 'name': TestOrderInfo.name, 'phone_number': TestOrderInfo.phone_number, 'email': TestOrderInfo.email, 'shipping_address': TestOrderInfo.shipping_address.to_dict(), } order_info = OrderInfo.de_json(json_dict, bot) 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-13.11/tests/test_parsemode.py000066400000000000000000000042431417656324400221710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import ParseMode class TestParseMode: markdown_text = '*bold* _italic_ [link](http://google.com) [name](tg://user?id=123456789).' html_text = ( 'bold italic link ' 'name.' ) formatted_text_formatted = 'bold italic link name.' def test_slot_behaviour(self, recwarn, mro_slots): inst = ParseMode() for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom = 'should give warning' assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @flaky(3, 1) def test_send_message_with_parse_mode_markdown(self, bot, chat_id): message = bot.send_message( chat_id=chat_id, text=self.markdown_text, parse_mode=ParseMode.MARKDOWN ) assert message.text == self.formatted_text_formatted @flaky(3, 1) def test_send_message_with_parse_mode_html(self, bot, chat_id): message = bot.send_message(chat_id=chat_id, text=self.html_text, parse_mode=ParseMode.HTML) assert message.text == self.formatted_text_formatted python-telegram-bot-13.11/tests/test_passport.py000066400000000000000000000651151417656324400220720ustar00rootroot00000000000000#!/usr/bin/env python # flake8: noqa: E501 # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ( PassportData, PassportFile, Bot, File, PassportElementErrorSelfie, PassportElementErrorDataField, Credentials, TelegramDecryptionError, ) # 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. 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', }, { 'reverse_side': { '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==', }, { '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', }, { 'data': 'j9SksVkSj128DBtZA+3aNjSFNirzv+R97guZaMgae4Gi0oDVNAF7twPR7j9VSmPedfJrEwL3O889Ei+a5F1xyLLyEI/qEBljvL70GFIhYGitS0JmNabHPHSZrjOl8b4s/0Z0Px2GpLO5siusTLQonimdUvu4UPjKquYISmlKEKhtmGATy+h+JDjNCYuOkhakeNw0Rk0BHgj0C3fCb7WZNQSyVb+2GTu6caR6eXf/AFwFp0TV3sRz3h0WIVPW8bna', 'type': 'address', }, {'email': 'fb3e3i47zt@dispostable.com', 'type': 'email'}, ], } @pytest.fixture(scope='function') def all_passport_data(): return [ {'type': 'personal_details', 'data': RAW_PASSPORT_DATA['data'][0]['data']}, { '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'], }, { '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'], }, { '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'], }, { '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'], }, { 'type': 'utility_bill', 'files': RAW_PASSPORT_DATA['data'][2]['files'], 'translation': RAW_PASSPORT_DATA['data'][2]['translation'], }, { 'type': 'bank_statement', 'files': RAW_PASSPORT_DATA['data'][2]['files'], 'translation': RAW_PASSPORT_DATA['data'][2]['translation'], }, { 'type': 'rental_agreement', 'files': RAW_PASSPORT_DATA['data'][2]['files'], 'translation': RAW_PASSPORT_DATA['data'][2]['translation'], }, { 'type': 'passport_registration', 'files': RAW_PASSPORT_DATA['data'][2]['files'], 'translation': RAW_PASSPORT_DATA['data'][2]['translation'], }, { 'type': 'temporary_registration', 'files': RAW_PASSPORT_DATA['data'][2]['files'], 'translation': RAW_PASSPORT_DATA['data'][2]['translation'], }, {'type': 'address', 'data': RAW_PASSPORT_DATA['data'][3]['data']}, {'type': 'email', 'email': 'fb3e3i47zt@dispostable.com'}, {'type': 'phone_number', 'phone_number': 'fb3e3i47zt@dispostable.com'}, ] @pytest.fixture(scope='function') def passport_data(bot): return PassportData.de_json(RAW_PASSPORT_DATA, bot=bot) class TestPassport: 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=' def test_slot_behaviour(self, passport_data, mro_slots, recwarn): inst = passport_data for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.data = 'should give warning', passport_data.data assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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_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, ) new.credentials._decrypted_data = Credentials.de_json(credentials, bot) assert isinstance(new, PassportData) assert new.decrypted_data def test_bot_init_invalid_key(self, bot): with pytest.raises(TypeError): Bot(bot.token, private_key='Invalid key!') with pytest.raises(ValueError): Bot(bot.token, private_key=b'Invalid key!') def test_passport_data_okay_with_non_crypto_bot(self, bot): b = Bot(bot.token) 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(TelegramDecryptionError): assert passport_data.decrypted_data def test_wrong_key(self, bot): short_key = b"-----BEGIN RSA PRIVATE 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 RSA PRIVATE KEY-----" b = Bot(bot.token, private_key=short_key) passport_data = PassportData.de_json(RAW_PASSPORT_DATA, bot=b) with pytest.raises(TelegramDecryptionError): assert passport_data.decrypted_data wrong_key = b"-----BEGIN RSA PRIVATE KEY-----\r\nMIIEogIBAAKCAQB4qCFltuvHakZze86TUweU7E/SB3VLGEHAe7GJlBmrou9SSWsL\r\nH7E++157X6UqWFl54LOE9MeHZnoW7rZ+DxLKhk6NwAHTxXPnvw4CZlvUPC3OFxg3\r\nhEmNen6ojSM4sl4kYUIa7F+Q5uMEYaboxoBen9mbj4zzMGsG4aY/xBOb2ewrXQyL\r\nRh//tk1Px4ago+lUPisAvQVecz7/6KU4Xj4Lpv2z20f3cHlZX6bb7HlE1vixCMOf\r\nxvfC5SkWEGZMR/ZoWQUsoDkrDSITF/S3GtLfg083TgtCKaOF3mCT27sJ1og77npP\r\n0cH/qdlbdoFtdrRj3PvBpaj/TtXRhmdGcJBxAgMBAAECggEAYSq1Sp6XHo8dkV8B\r\nK2/QSURNu8y5zvIH8aUrgqo8Shb7OH9bryekrB3vJtgNwR5JYHdu2wHttcL3S4SO\r\nftJQxbyHgmxAjHUVNGqOM6yPA0o7cR70J7FnMoKVgdO3q68pVY7ll50IET9/T0X9\r\nDrTdKFb+/eILFsXFS1NpeSzExdsKq3zM0sP/vlJHHYVTmZDGaGEvny/eLAS+KAfG\r\nrKP96DeO4C/peXEJzALZ/mG1ReBB05Qp9Dx1xEC20yreRk5MnnBA5oiHVG5ZLOl9\r\nEEHINidqN+TMNSkxv67xMfQ6utNu5IpbklKv/4wqQOJOO50HZ+qBtSurTN573dky\r\nzslbCQKBgQDHDUBYyKN/v69VLmvNVcxTgrOcrdbqAfefJXb9C3dVXhS8/oRkCRU/\r\ndzxYWNT7hmQyWUKor/izh68rZ/M+bsTnlaa7IdAgyChzTfcZL/2pxG9pq05GF1Q4\r\nBSJ896ZEe3jEhbpJXRlWYvz7455svlxR0H8FooCTddTmkU3nsQSx0wKBgQCbLSa4\r\nyZs2QVstQQerNjxAtLi0IvV8cJkuvFoNC2Q21oqQc7BYU7NJL7uwriprZr5nwkCQ\r\nOFQXi4N3uqimNxuSng31ETfjFZPp+pjb8jf7Sce7cqU66xxR+anUzVZqBG1CJShx\r\nVxN7cWN33UZvIH34gA2Ax6AXNnJG42B5Gn1GKwKBgQCZ/oh/p4nGNXfiAK3qB6yy\r\nFvX6CwuvsqHt/8AUeKBz7PtCU+38roI/vXF0MBVmGky+HwxREQLpcdl1TVCERpIT\r\nUFXThI9OLUwOGI1IcTZf9tby+1LtKvM++8n4wGdjp9qAv6ylQV9u09pAzZItMwCd\r\nUx5SL6wlaQ2y60tIKk0lfQKBgBJS+56YmA6JGzY11qz+I5FUhfcnpauDNGOTdGLT\r\n9IqRPR2fu7RCdgpva4+KkZHLOTLReoRNUojRPb4WubGfEk93AJju5pWXR7c6k3Bt\r\novS2mrJk8GQLvXVksQxjDxBH44sLDkKMEM3j7uYJqDaZNKbyoCWT7TCwikAau5qx\r\naRevAoGAAKZV705dvrpJuyoHFZ66luANlrAwG/vNf6Q4mBEXB7guqMkokCsSkjqR\r\nhsD79E6q06zA0QzkLCavbCn5kMmDS/AbA80+B7El92iIN6d3jRdiNZiewkhlWhEG\r\nm4N0gQRfIu+rUjsS/4xk8UuQUT/Ossjn/hExi7ejpKdCc7N++bc=\r\n-----END RSA PRIVATE KEY-----" b = Bot(bot.token, private_key=wrong_key) passport_data = PassportData.de_json(RAW_PASSPORT_DATA, bot=b) with pytest.raises(TelegramDecryptionError): assert passport_data.decrypted_data 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 def get_file(*_, **kwargs): return File(kwargs['file_id'], selfie.file_unique_id) monkeypatch.setattr(passport_data.bot, 'get_file', get_file) file = 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 def test_mocked_set_passport_data_errors(self, monkeypatch, bot, chat_id, passport_data): def test(url, data, **kwargs): return ( data['user_id'] == 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', test) message = 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 def test_de_json_and_to_dict(self, bot): passport_data = PassportData.de_json(RAW_PASSPORT_DATA, bot) 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 passport_data.credentials.hash = 'NOTAPROPERHASH' c = PassportData(passport_data.data, passport_data.credentials) assert a != c assert hash(a) != hash(c) python-telegram-bot-13.11/tests/test_passportelementerrordatafield.py000066400000000000000000000104171417656324400263470ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def passport_element_error_data_field(): return PassportElementErrorDataField( TestPassportElementErrorDataField.type_, TestPassportElementErrorDataField.field_name, TestPassportElementErrorDataField.data_hash, TestPassportElementErrorDataField.message, ) class TestPassportElementErrorDataField: source = 'data' type_ = 'test_type' field_name = 'test_field' data_hash = 'data_hash' message = 'Error message' def test_slot_behaviour(self, passport_element_error_data_field, recwarn, mro_slots): inst = passport_element_error_data_field for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_passportelementerrorfile.py000066400000000000000000000067371417656324400253630ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def passport_element_error_file(): return PassportElementErrorFile( TestPassportElementErrorFile.type_, TestPassportElementErrorFile.file_hash, TestPassportElementErrorFile.message, ) class TestPassportElementErrorFile: source = 'file' type_ = 'test_type' file_hash = 'file_hash' message = 'Error message' def test_slot_behaviour(self, passport_element_error_file, recwarn, mro_slots): inst = passport_element_error_file for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_passportelementerrorfiles.py000066400000000000000000000071421417656324400255350ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def passport_element_error_files(): return PassportElementErrorFiles( TestPassportElementErrorFiles.type_, TestPassportElementErrorFiles.file_hashes, TestPassportElementErrorFiles.message, ) class TestPassportElementErrorFiles: source = 'files' type_ = 'test_type' file_hashes = ['hash1', 'hash2'] message = 'Error message' def test_slot_behaviour(self, passport_element_error_files, mro_slots, recwarn): inst = passport_element_error_files for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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['file_hashes'] == passport_element_error_files.file_hashes ) assert passport_element_error_files_dict['message'] == passport_element_error_files.message 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) python-telegram-bot-13.11/tests/test_passportelementerrorfrontside.py000066400000000000000000000074141417656324400264320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def passport_element_error_front_side(): return PassportElementErrorFrontSide( TestPassportElementErrorFrontSide.type_, TestPassportElementErrorFrontSide.file_hash, TestPassportElementErrorFrontSide.message, ) class TestPassportElementErrorFrontSide: source = 'front_side' type_ = 'test_type' file_hash = 'file_hash' message = 'Error message' def test_slot_behaviour(self, passport_element_error_front_side, recwarn, mro_slots): inst = passport_element_error_front_side for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_passportelementerrorreverseside.py000066400000000000000000000075141417656324400267560ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def passport_element_error_reverse_side(): return PassportElementErrorReverseSide( TestPassportElementErrorReverseSide.type_, TestPassportElementErrorReverseSide.file_hash, TestPassportElementErrorReverseSide.message, ) class TestPassportElementErrorReverseSide: source = 'reverse_side' type_ = 'test_type' file_hash = 'file_hash' message = 'Error message' def test_slot_behaviour(self, passport_element_error_reverse_side, mro_slots, recwarn): inst = passport_element_error_reverse_side for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_passportelementerrorselfie.py000066400000000000000000000071011417656324400256750ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, PassportElementErrorDataField @pytest.fixture(scope='class') def passport_element_error_selfie(): return PassportElementErrorSelfie( TestPassportElementErrorSelfie.type_, TestPassportElementErrorSelfie.file_hash, TestPassportElementErrorSelfie.message, ) class TestPassportElementErrorSelfie: source = 'selfie' type_ = 'test_type' file_hash = 'file_hash' message = 'Error message' def test_slot_behaviour(self, passport_element_error_selfie, recwarn, mro_slots): inst = passport_element_error_selfie for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_passportelementerrortranslationfile.py000066400000000000000000000077421417656324400276370ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 PassportElementErrorTranslationFile, PassportElementErrorDataField @pytest.fixture(scope='class') def passport_element_error_translation_file(): return PassportElementErrorTranslationFile( TestPassportElementErrorTranslationFile.type_, TestPassportElementErrorTranslationFile.file_hash, TestPassportElementErrorTranslationFile.message, ) class TestPassportElementErrorTranslationFile: source = 'translation_file' type_ = 'test_type' file_hash = 'file_hash' message = 'Error message' def test_slot_behaviour(self, passport_element_error_translation_file, recwarn, mro_slots): inst = passport_element_error_translation_file for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_passportelementerrortranslationfiles.py000066400000000000000000000101471417656324400300130ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 PassportElementErrorTranslationFiles, PassportElementErrorSelfie @pytest.fixture(scope='class') def passport_element_error_translation_files(): return PassportElementErrorTranslationFiles( TestPassportElementErrorTranslationFiles.type_, TestPassportElementErrorTranslationFiles.file_hashes, TestPassportElementErrorTranslationFiles.message, ) class TestPassportElementErrorTranslationFiles: source = 'translation_files' type_ = 'test_type' file_hashes = ['hash1', 'hash2'] message = 'Error message' def test_slot_behaviour(self, passport_element_error_translation_files, mro_slots, recwarn): inst = passport_element_error_translation_files for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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['file_hashes'] == passport_element_error_translation_files.file_hashes ) assert ( passport_element_error_translation_files_dict['message'] == passport_element_error_translation_files.message ) 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) python-telegram-bot-13.11/tests/test_passportelementerrorunspecified.py000066400000000000000000000075231417656324400267340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 PassportElementErrorUnspecified, PassportElementErrorDataField @pytest.fixture(scope='class') def passport_element_error_unspecified(): return PassportElementErrorUnspecified( TestPassportElementErrorUnspecified.type_, TestPassportElementErrorUnspecified.element_hash, TestPassportElementErrorUnspecified.message, ) class TestPassportElementErrorUnspecified: source = 'unspecified' type_ = 'test_type' element_hash = 'element_hash' message = 'Error message' def test_slot_behaviour(self, passport_element_error_unspecified, recwarn, mro_slots): inst = passport_element_error_unspecified for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.type = 'should give warning', self.type_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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-13.11/tests/test_passportfile.py000066400000000000000000000100051417656324400227160ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 PassportFile, PassportElementError, Bot, File from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling @pytest.fixture(scope='class') def passport_file(bot): return PassportFile( file_id=TestPassportFile.file_id, file_unique_id=TestPassportFile.file_unique_id, file_size=TestPassportFile.file_size, file_date=TestPassportFile.file_date, bot=bot, ) class TestPassportFile: file_id = 'data' file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' file_size = 50 file_date = 1532879128 def test_slot_behaviour(self, passport_file, mro_slots, recwarn): inst = passport_file for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.file_id = 'should give warning', self.file_id assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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_get_file_instance_method(self, monkeypatch, passport_file): 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 check_shortcut_call(passport_file.get_file, passport_file.bot, 'get_file') assert check_defaults_handling(passport_file.get_file, passport_file.bot) monkeypatch.setattr(passport_file.bot, 'get_file', make_assertion) assert passport_file.get_file().file_id == 'True' 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) python-telegram-bot-13.11/tests/test_persistence.py000066400000000000000000002736151417656324400225510ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 gzip import signal import uuid from threading import Lock from telegram.ext.callbackdatacache import CallbackDataCache from telegram.utils.helpers import encode_conversations_to_json try: import ujson as json except ImportError: import json import logging import os import pickle from collections import defaultdict from collections.abc import Container from time import sleep from sys import version_info as py_ver import pytest from telegram import Update, Message, User, Chat, MessageEntity, Bot from telegram.ext import ( BasePersistence, Updater, ConversationHandler, MessageHandler, Filters, PicklePersistence, CommandHandler, DictPersistence, TypeHandler, JobQueue, ContextTypes, ) @pytest.fixture(autouse=True) def change_directory(tmp_path): orig_dir = os.getcwd() # Switch to a temporary directory so we don't have to worry about cleaning up files # (str() for py<3.6) os.chdir(str(tmp_path)) yield # Go back to original directory os.chdir(orig_dir) @pytest.fixture(autouse=True) def reset_callback_data_cache(bot): yield bot.callback_data_cache.clear_callback_data() bot.callback_data_cache.clear_callback_queries() bot.arbitrary_callback_data = False class OwnPersistence(BasePersistence): def get_bot_data(self): raise NotImplementedError def get_chat_data(self): raise NotImplementedError def get_user_data(self): raise NotImplementedError def get_conversations(self, name): raise NotImplementedError def update_bot_data(self, data): raise NotImplementedError def update_chat_data(self, chat_id, data): raise NotImplementedError def update_conversation(self, name, key, new_state): raise NotImplementedError def update_user_data(self, user_id, data): raise NotImplementedError @pytest.fixture(scope="function") def base_persistence(): return OwnPersistence( store_chat_data=True, store_user_data=True, store_bot_data=True, store_callback_data=True ) @pytest.fixture(scope="function") def bot_persistence(): class BotPersistence(BasePersistence): __slots__ = () def __init__(self): super().__init__() self.bot_data = None self.chat_data = defaultdict(dict) self.user_data = defaultdict(dict) self.callback_data = None def get_bot_data(self): return self.bot_data def get_chat_data(self): return self.chat_data def get_user_data(self): return self.user_data def get_callback_data(self): return self.callback_data def get_conversations(self, name): raise NotImplementedError def update_bot_data(self, data): self.bot_data = data def update_chat_data(self, chat_id, data): self.chat_data[chat_id] = data def update_user_data(self, user_id, data): self.user_data[user_id] = data def update_callback_data(self, data): self.callback_data = data def update_conversation(self, name, key, new_state): raise NotImplementedError return BotPersistence() @pytest.fixture(scope="function") def bot_data(): return {'test1': 'test2', 'test3': {'test4': 'test5'}} @pytest.fixture(scope="function") def chat_data(): return defaultdict( dict, {-12345: {'test1': 'test2', 'test3': {'test4': 'test5'}}, -67890: {3: 'test4'}} ) @pytest.fixture(scope="function") def user_data(): return defaultdict( dict, {12345: {'test1': 'test2', 'test3': {'test4': 'test5'}}, 67890: {3: 'test4'}} ) @pytest.fixture(scope="function") def callback_data(): return [('test1', 1000, {'button1': 'test0', 'button2': 'test1'})], {'test1': 'test2'} @pytest.fixture(scope='function') 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(scope="function") def updater(bot, base_persistence): base_persistence.store_chat_data = False base_persistence.store_bot_data = False base_persistence.store_user_data = False base_persistence.store_callback_data = False u = Updater(bot=bot, persistence=base_persistence) base_persistence.store_bot_data = True base_persistence.store_chat_data = True base_persistence.store_user_data = True base_persistence.store_callback_data = True return u @pytest.fixture(scope='function') def job_queue(bot): jq = JobQueue() yield jq jq.stop() def assert_data_in_cache(callback_data_cache: CallbackDataCache, data): for val in callback_data_cache._keyboard_data.values(): if data in val.button_data.values(): return data return False class TestBasePersistence: test_flag = False @pytest.fixture(scope='function', autouse=True) def reset(self): self.test_flag = False def test_slot_behaviour(self, bot_persistence, mro_slots, recwarn): inst = bot_persistence for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" # assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" # The below test fails if the child class doesn't define __slots__ (not a cause of concern) assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.store_user_data, inst.custom = {}, "custom persistence shouldn't warn" assert len(recwarn) == 0, recwarn.list assert '__dict__' not in BasePersistence.__slots__ if py_ver < (3, 7) else True, 'has dict' def test_creation(self, base_persistence): assert base_persistence.store_chat_data assert base_persistence.store_user_data assert base_persistence.store_bot_data def test_abstract_methods(self, base_persistence): with pytest.raises( TypeError, match=( 'get_bot_data, get_chat_data, get_conversations, ' 'get_user_data, update_bot_data, update_chat_data, ' 'update_conversation, update_user_data' ), ): BasePersistence() with pytest.raises(NotImplementedError): base_persistence.get_callback_data() with pytest.raises(NotImplementedError): base_persistence.update_callback_data((None, {'foo': 'bar'})) def test_implementation(self, updater, base_persistence): dp = updater.dispatcher assert dp.persistence == base_persistence def test_conversationhandler_addition(self, dp, base_persistence): with pytest.raises(ValueError, match="when handler is unnamed"): ConversationHandler([], [], [], persistent=True) with pytest.raises(ValueError, match="if dispatcher has no persistence"): dp.add_handler(ConversationHandler([], {}, [], persistent=True, name="My Handler")) dp.persistence = base_persistence def test_dispatcher_integration_init( self, bot, base_persistence, chat_data, user_data, bot_data, callback_data ): def get_user_data(): return "test" def get_chat_data(): return "test" def get_bot_data(): return "test" def get_callback_data(): return "test" base_persistence.get_user_data = get_user_data base_persistence.get_chat_data = get_chat_data base_persistence.get_bot_data = get_bot_data base_persistence.get_callback_data = get_callback_data with pytest.raises(ValueError, match="user_data must be of type defaultdict"): u = Updater(bot=bot, persistence=base_persistence) def get_user_data(): return user_data base_persistence.get_user_data = get_user_data with pytest.raises(ValueError, match="chat_data must be of type defaultdict"): Updater(bot=bot, persistence=base_persistence) def get_chat_data(): return chat_data base_persistence.get_chat_data = get_chat_data with pytest.raises(ValueError, match="bot_data must be of type dict"): Updater(bot=bot, persistence=base_persistence) def get_bot_data(): return bot_data base_persistence.get_bot_data = get_bot_data with pytest.raises(ValueError, match="callback_data must be a 2-tuple"): Updater(bot=bot, persistence=base_persistence) def get_callback_data(): return callback_data base_persistence.get_callback_data = get_callback_data u = Updater(bot=bot, persistence=base_persistence) assert u.dispatcher.bot_data == bot_data assert u.dispatcher.chat_data == chat_data assert u.dispatcher.user_data == user_data assert u.dispatcher.bot.callback_data_cache.persistence_data == callback_data u.dispatcher.chat_data[442233]['test5'] = 'test6' assert u.dispatcher.chat_data[442233]['test5'] == 'test6' @pytest.mark.parametrize('run_async', [True, False], ids=['synchronous', 'run_async']) def test_dispatcher_integration_handlers( self, cdp, caplog, bot, base_persistence, chat_data, user_data, bot_data, callback_data, run_async, ): def get_user_data(): return user_data def get_chat_data(): return chat_data def get_bot_data(): return bot_data def get_callback_data(): return callback_data base_persistence.get_user_data = get_user_data base_persistence.get_chat_data = get_chat_data base_persistence.get_bot_data = get_bot_data base_persistence.get_callback_data = get_callback_data # base_persistence.update_chat_data = lambda x: x # base_persistence.update_user_data = lambda x: x base_persistence.refresh_bot_data = lambda x: x base_persistence.refresh_chat_data = lambda x, y: x base_persistence.refresh_user_data = lambda x, y: x updater = Updater(bot=bot, persistence=base_persistence, use_context=True) dp = updater.dispatcher def callback_known_user(update, context): if not context.user_data['test1'] == 'test2': pytest.fail('user_data corrupt') if not context.bot_data == bot_data: pytest.fail('bot_data corrupt') def callback_known_chat(update, context): if not context.chat_data['test3'] == 'test4': pytest.fail('chat_data corrupt') if not context.bot_data == bot_data: pytest.fail('bot_data corrupt') def callback_unknown_user_or_chat(update, context): if not context.user_data == {}: pytest.fail('user_data corrupt') if not context.chat_data == {}: pytest.fail('chat_data corrupt') if not context.bot_data == bot_data: pytest.fail('bot_data corrupt') context.user_data[1] = 'test7' context.chat_data[2] = 'test8' context.bot_data['test0'] = 'test0' context.bot.callback_data_cache.put('test0') known_user = MessageHandler( Filters.user(user_id=12345), callback_known_user, pass_chat_data=True, pass_user_data=True, ) known_chat = MessageHandler( Filters.chat(chat_id=-67890), callback_known_chat, pass_chat_data=True, pass_user_data=True, ) unknown = MessageHandler( Filters.all, callback_unknown_user_or_chat, pass_chat_data=True, pass_user_data=True ) dp.add_handler(known_user) dp.add_handler(known_chat) dp.add_handler(unknown) user1 = User(id=12345, first_name='test user', is_bot=False) user2 = User(id=54321, first_name='test user', is_bot=False) chat1 = Chat(id=-67890, type='group') chat2 = Chat(id=-987654, type='group') m = Message(1, None, chat2, from_user=user1) u = Update(0, m) with caplog.at_level(logging.ERROR): dp.process_update(u) rec = caplog.records[-1] assert rec.getMessage() == 'No error handlers are registered, logging exception.' assert rec.levelname == 'ERROR' rec = caplog.records[-2] assert rec.getMessage() == 'No error handlers are registered, logging exception.' assert rec.levelname == 'ERROR' rec = caplog.records[-3] assert rec.getMessage() == 'No error handlers are registered, logging exception.' assert rec.levelname == 'ERROR' m.from_user = user2 m.chat = chat1 u = Update(1, m) dp.process_update(u) m.chat = chat2 u = Update(2, m) def save_bot_data(data): if 'test0' not in data: pytest.fail() def save_chat_data(data): if -987654 not in data: pytest.fail() def save_user_data(data): if 54321 not in data: pytest.fail() def save_callback_data(data): if not assert_data_in_cache(dp.bot.callback_data, 'test0'): pytest.fail() base_persistence.update_chat_data = save_chat_data base_persistence.update_user_data = save_user_data base_persistence.update_bot_data = save_bot_data base_persistence.update_callback_data = save_callback_data dp.process_update(u) assert dp.user_data[54321][1] == 'test7' assert dp.chat_data[-987654][2] == 'test8' assert dp.bot_data['test0'] == 'test0' assert assert_data_in_cache(dp.bot.callback_data_cache, 'test0') @pytest.mark.parametrize( 'store_user_data', [True, False], ids=['store_user_data-True', 'store_user_data-False'] ) @pytest.mark.parametrize( 'store_chat_data', [True, False], ids=['store_chat_data-True', 'store_chat_data-False'] ) @pytest.mark.parametrize( 'store_bot_data', [True, False], ids=['store_bot_data-True', 'store_bot_data-False'] ) @pytest.mark.parametrize('run_async', [True, False], ids=['synchronous', 'run_async']) def test_persistence_dispatcher_integration_refresh_data( self, cdp, base_persistence, chat_data, bot_data, user_data, store_bot_data, store_chat_data, store_user_data, run_async, ): base_persistence.refresh_bot_data = lambda x: x.setdefault( 'refreshed', x.get('refreshed', 0) + 1 ) # x is the user/chat_id base_persistence.refresh_chat_data = lambda x, y: y.setdefault('refreshed', x) base_persistence.refresh_user_data = lambda x, y: y.setdefault('refreshed', x) base_persistence.store_bot_data = store_bot_data base_persistence.store_chat_data = store_chat_data base_persistence.store_user_data = store_user_data cdp.persistence = base_persistence self.test_flag = True def callback_with_user_and_chat(update, context): if store_user_data: if context.user_data.get('refreshed') != update.effective_user.id: self.test_flag = 'user_data was not refreshed' else: if 'refreshed' in context.user_data: self.test_flag = 'user_data was wrongly refreshed' if store_chat_data: if context.chat_data.get('refreshed') != update.effective_chat.id: self.test_flag = 'chat_data was not refreshed' else: if 'refreshed' in context.chat_data: self.test_flag = 'chat_data was wrongly refreshed' if store_bot_data: if context.bot_data.get('refreshed') != 1: self.test_flag = 'bot_data was not refreshed' else: if 'refreshed' in context.bot_data: self.test_flag = 'bot_data was wrongly refreshed' def callback_without_user_and_chat(_, context): if store_bot_data: if context.bot_data.get('refreshed') != 1: self.test_flag = 'bot_data was not refreshed' else: if 'refreshed' in context.bot_data: self.test_flag = 'bot_data was wrongly refreshed' with_user_and_chat = MessageHandler( Filters.user(user_id=12345), callback_with_user_and_chat, pass_chat_data=True, pass_user_data=True, run_async=run_async, ) without_user_and_chat = MessageHandler( Filters.all, callback_without_user_and_chat, pass_chat_data=True, pass_user_data=True, run_async=run_async, ) cdp.add_handler(with_user_and_chat) cdp.add_handler(without_user_and_chat) user = User(id=12345, first_name='test user', is_bot=False) chat = Chat(id=-987654, type='group') m = Message(1, None, chat, from_user=user) # has user and chat u = Update(0, m) cdp.process_update(u) assert self.test_flag is True # has neither user nor hat m.from_user = None m.chat = None u = Update(1, m) cdp.process_update(u) assert self.test_flag is True sleep(0.1) def test_persistence_dispatcher_arbitrary_update_types(self, dp, base_persistence, caplog): # Updates used with TypeHandler doesn't necessarily have the proper attributes for # persistence, makes sure it works anyways dp.persistence = base_persistence class MyUpdate: pass dp.add_handler(TypeHandler(MyUpdate, lambda *_: None)) with caplog.at_level(logging.ERROR): dp.process_update(MyUpdate()) assert 'An uncaught error was raised while processing the update' not in caplog.text def test_bot_replace_insert_bot(self, bot, bot_persistence): class CustomSlottedClass: __slots__ = ('bot', '__dict__') def __init__(self): self.bot = bot self.not_in_slots = bot def __eq__(self, other): if isinstance(other, CustomSlottedClass): return self.bot is other.bot and self.not_in_slots is other.not_in_slots return False class DictNotInSlots(Container): """This classes parent has slots, but __dict__ is not in those slots.""" def __init__(self): self.bot = bot def __contains__(self, item): return True def __eq__(self, other): if isinstance(other, DictNotInSlots): return self.bot is other.bot return False class CustomClass: def __init__(self): self.bot = bot self.slotted_object = CustomSlottedClass() self.dict_not_in_slots_object = DictNotInSlots() self.list_ = [1, 2, bot] self.tuple_ = tuple(self.list_) self.set_ = set(self.list_) self.frozenset_ = frozenset(self.list_) self.dict_ = {item: item for item in self.list_} self.defaultdict_ = defaultdict(dict, self.dict_) @staticmethod def replace_bot(): cc = CustomClass() cc.bot = BasePersistence.REPLACED_BOT cc.slotted_object.bot = BasePersistence.REPLACED_BOT cc.slotted_object.not_in_slots = BasePersistence.REPLACED_BOT cc.dict_not_in_slots_object.bot = BasePersistence.REPLACED_BOT cc.list_ = [1, 2, BasePersistence.REPLACED_BOT] cc.tuple_ = tuple(cc.list_) cc.set_ = set(cc.list_) cc.frozenset_ = frozenset(cc.list_) cc.dict_ = {item: item for item in cc.list_} cc.defaultdict_ = defaultdict(dict, cc.dict_) return cc def __eq__(self, other): if isinstance(other, CustomClass): return ( self.bot is other.bot and self.slotted_object == other.slotted_object and self.dict_not_in_slots_object == other.dict_not_in_slots_object and self.list_ == other.list_ and self.tuple_ == other.tuple_ and self.set_ == other.set_ and self.frozenset_ == other.frozenset_ and self.dict_ == other.dict_ and self.defaultdict_ == other.defaultdict_ ) return False persistence = bot_persistence persistence.set_bot(bot) cc = CustomClass() persistence.update_bot_data({1: cc}) assert persistence.bot_data[1].bot == BasePersistence.REPLACED_BOT assert persistence.bot_data[1] == cc.replace_bot() persistence.update_chat_data(123, {1: cc}) assert persistence.chat_data[123][1].bot == BasePersistence.REPLACED_BOT assert persistence.chat_data[123][1] == cc.replace_bot() persistence.update_user_data(123, {1: cc}) assert persistence.user_data[123][1].bot == BasePersistence.REPLACED_BOT assert persistence.user_data[123][1] == cc.replace_bot() persistence.update_callback_data(([('1', 2, {0: cc})], {'1': '2'})) assert persistence.callback_data[0][0][2][0].bot == BasePersistence.REPLACED_BOT assert persistence.callback_data[0][0][2][0] == cc.replace_bot() assert persistence.get_bot_data()[1] == cc assert persistence.get_bot_data()[1].bot is bot assert persistence.get_chat_data()[123][1] == cc assert persistence.get_chat_data()[123][1].bot is bot assert persistence.get_user_data()[123][1] == cc assert persistence.get_user_data()[123][1].bot is bot assert persistence.get_callback_data()[0][0][2][0].bot is bot assert persistence.get_callback_data()[0][0][2][0] == cc def test_bot_replace_insert_bot_unpickable_objects(self, bot, bot_persistence, recwarn): """Here check that unpickable objects are just returned verbatim.""" persistence = bot_persistence persistence.set_bot(bot) class CustomClass: def __copy__(self): raise TypeError('UnhandledException') lock = Lock() persistence.update_bot_data({1: lock}) assert persistence.bot_data[1] is lock persistence.update_chat_data(123, {1: lock}) assert persistence.chat_data[123][1] is lock persistence.update_user_data(123, {1: lock}) assert persistence.user_data[123][1] is lock persistence.update_callback_data(([('1', 2, {0: lock})], {'1': '2'})) assert persistence.callback_data[0][0][2][0] is lock assert persistence.get_bot_data()[1] is lock assert persistence.get_chat_data()[123][1] is lock assert persistence.get_user_data()[123][1] is lock assert persistence.get_callback_data()[0][0][2][0] is lock cc = CustomClass() persistence.update_bot_data({1: cc}) assert persistence.bot_data[1] is cc persistence.update_chat_data(123, {1: cc}) assert persistence.chat_data[123][1] is cc persistence.update_user_data(123, {1: cc}) assert persistence.user_data[123][1] is cc persistence.update_callback_data(([('1', 2, {0: cc})], {'1': '2'})) assert persistence.callback_data[0][0][2][0] is cc assert persistence.get_bot_data()[1] is cc assert persistence.get_chat_data()[123][1] is cc assert persistence.get_user_data()[123][1] is cc assert persistence.get_callback_data()[0][0][2][0] is cc assert len(recwarn) == 2 assert str(recwarn[0].message).startswith( "BasePersistence.replace_bot does not handle objects that can not be copied." ) assert str(recwarn[1].message).startswith( "BasePersistence.insert_bot does not handle objects that can not be copied." ) def test_bot_replace_insert_bot_unparsable_objects(self, bot, bot_persistence, recwarn): """Here check that objects in __dict__ or __slots__ that can't be parsed are just returned verbatim.""" persistence = bot_persistence persistence.set_bot(bot) uuid_obj = uuid.uuid4() persistence.update_bot_data({1: uuid_obj}) assert persistence.bot_data[1] is uuid_obj persistence.update_chat_data(123, {1: uuid_obj}) assert persistence.chat_data[123][1] is uuid_obj persistence.update_user_data(123, {1: uuid_obj}) assert persistence.user_data[123][1] is uuid_obj persistence.update_callback_data(([('1', 2, {0: uuid_obj})], {'1': '2'})) assert persistence.callback_data[0][0][2][0] is uuid_obj assert persistence.get_bot_data()[1] is uuid_obj assert persistence.get_chat_data()[123][1] is uuid_obj assert persistence.get_user_data()[123][1] is uuid_obj assert persistence.get_callback_data()[0][0][2][0] is uuid_obj assert len(recwarn) == 2 assert str(recwarn[0].message).startswith( "Parsing of an object failed with the following exception: " ) assert str(recwarn[1].message).startswith( "Parsing of an object failed with the following exception: " ) def test_bot_replace_insert_bot_classes(self, bot, bot_persistence, recwarn): """Here check that classes are just returned verbatim.""" persistence = bot_persistence persistence.set_bot(bot) class CustomClass: pass persistence.update_bot_data({1: CustomClass}) assert persistence.bot_data[1] is CustomClass persistence.update_chat_data(123, {1: CustomClass}) assert persistence.chat_data[123][1] is CustomClass persistence.update_user_data(123, {1: CustomClass}) assert persistence.user_data[123][1] is CustomClass assert persistence.get_bot_data()[1] is CustomClass assert persistence.get_chat_data()[123][1] is CustomClass assert persistence.get_user_data()[123][1] is CustomClass assert len(recwarn) == 2 assert str(recwarn[0].message).startswith( "BasePersistence.replace_bot does not handle classes." ) assert str(recwarn[1].message).startswith( "BasePersistence.insert_bot does not handle classes." ) def test_bot_replace_insert_bot_objects_with_faulty_equality(self, bot, bot_persistence): """Here check that trying to compare obj == self.REPLACED_BOT doesn't lead to problems.""" persistence = bot_persistence persistence.set_bot(bot) class CustomClass: def __init__(self, data): self.data = data def __eq__(self, other): raise RuntimeError("Can't be compared") cc = CustomClass({1: bot, 2: 'foo'}) expected = {1: BasePersistence.REPLACED_BOT, 2: 'foo'} persistence.update_bot_data({1: cc}) assert persistence.bot_data[1].data == expected persistence.update_chat_data(123, {1: cc}) assert persistence.chat_data[123][1].data == expected persistence.update_user_data(123, {1: cc}) assert persistence.user_data[123][1].data == expected persistence.update_callback_data(([('1', 2, {0: cc})], {'1': '2'})) assert persistence.callback_data[0][0][2][0].data == expected expected = {1: bot, 2: 'foo'} assert persistence.get_bot_data()[1].data == expected assert persistence.get_chat_data()[123][1].data == expected assert persistence.get_user_data()[123][1].data == expected assert persistence.get_callback_data()[0][0][2][0].data == expected @pytest.mark.filterwarnings('ignore:BasePersistence') def test_replace_insert_bot_item_identity(self, bot, bot_persistence): persistence = bot_persistence persistence.set_bot(bot) class CustomSlottedClass: __slots__ = ('value',) def __init__(self): self.value = 5 class CustomClass: pass slot_object = CustomSlottedClass() dict_object = CustomClass() lock = Lock() list_ = [slot_object, dict_object, lock] tuple_ = (1, 2, 3) dict_ = {1: slot_object, 2: dict_object} data = { 'bot_1': bot, 'bot_2': bot, 'list_1': list_, 'list_2': list_, 'tuple_1': tuple_, 'tuple_2': tuple_, 'dict_1': dict_, 'dict_2': dict_, } def make_assertion(data_): return ( data_['bot_1'] is data_['bot_2'] and data_['list_1'] is data_['list_2'] and data_['list_1'][0] is data_['list_2'][0] and data_['list_1'][1] is data_['list_2'][1] and data_['list_1'][2] is data_['list_2'][2] and data_['tuple_1'] is data_['tuple_2'] and data_['dict_1'] is data_['dict_2'] and data_['dict_1'][1] is data_['dict_2'][1] and data_['dict_1'][1] is data_['list_1'][0] and data_['dict_1'][2] is data_['list_1'][1] and data_['dict_1'][2] is data_['dict_2'][2] ) persistence.update_bot_data(data) assert make_assertion(persistence.bot_data) assert make_assertion(persistence.get_bot_data()) def test_set_bot_exception(self, bot): non_ext_bot = Bot(bot.token) persistence = OwnPersistence(store_callback_data=True) with pytest.raises(TypeError, match='store_callback_data can only be used'): persistence.set_bot(non_ext_bot) @pytest.fixture(scope='function') def pickle_persistence(): return PicklePersistence( filename='pickletest', store_user_data=True, store_chat_data=True, store_bot_data=True, store_callback_data=True, single_file=False, on_flush=False, ) @pytest.fixture(scope='function') def pickle_persistence_only_bot(): return PicklePersistence( filename='pickletest', store_user_data=False, store_chat_data=False, store_bot_data=True, store_callback_data=False, single_file=False, on_flush=False, ) @pytest.fixture(scope='function') def pickle_persistence_only_chat(): return PicklePersistence( filename='pickletest', store_user_data=False, store_chat_data=True, store_bot_data=False, store_callback_data=False, single_file=False, on_flush=False, ) @pytest.fixture(scope='function') def pickle_persistence_only_user(): return PicklePersistence( filename='pickletest', store_user_data=True, store_chat_data=False, store_bot_data=False, store_callback_data=False, single_file=False, on_flush=False, ) @pytest.fixture(scope='function') def pickle_persistence_only_callback(): return PicklePersistence( filename='pickletest', store_user_data=False, store_chat_data=False, store_bot_data=False, store_callback_data=True, single_file=False, on_flush=False, ) @pytest.fixture(scope='function') def bad_pickle_files(): for name in [ 'pickletest_user_data', 'pickletest_chat_data', 'pickletest_bot_data', 'pickletest_callback_data', 'pickletest_conversations', 'pickletest', ]: with open(name, 'w') as f: f.write('(())') yield True @pytest.fixture(scope='function') 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) yield True @pytest.fixture(scope='function') 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 open('pickletest_user_data', 'wb') as f: pickle.dump(user_data, f) with open('pickletest_chat_data', 'wb') as f: pickle.dump(chat_data, f) with open('pickletest_bot_data', 'wb') as f: pickle.dump(bot_data, f) with open('pickletest_callback_data', 'wb') as f: pickle.dump(callback_data, f) with open('pickletest_conversations', 'wb') as f: pickle.dump(conversations, f) with open('pickletest', 'wb') as f: pickle.dump(data, f) yield True @pytest.fixture(scope='function') 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 open('pickletest_user_data', 'wb') as f: pickle.dump(user_data, f) with open('pickletest_chat_data', 'wb') as f: pickle.dump(chat_data, f) with open('pickletest_callback_data', 'wb') as f: pickle.dump(callback_data, f) with open('pickletest_conversations', 'wb') as f: pickle.dump(conversations, f) with open('pickletest', 'wb') as f: pickle.dump(data, f) yield True @pytest.fixture(scope='function') 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 open('pickletest_user_data', 'wb') as f: pickle.dump(user_data, f) with open('pickletest_chat_data', 'wb') as f: pickle.dump(chat_data, f) with open('pickletest_bot_data', 'wb') as f: pickle.dump(bot_data, f) with open('pickletest_conversations', 'wb') as f: pickle.dump(conversations, f) with open('pickletest', 'wb') as f: pickle.dump(data, f) yield True @pytest.fixture(scope='function') def update(bot): user = User(id=321, first_name='test_user', is_bot=False) chat = Chat(id=123, type='group') message = Message(1, None, chat, from_user=user, text="Hi there", bot=bot) return Update(0, message=message) class CustomMapping(defaultdict): pass class TestPicklePersistence: def test_slot_behaviour(self, mro_slots, recwarn, pickle_persistence): inst = pickle_persistence for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" # assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.store_user_data = 'should give warning', {} assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_pickle_behaviour_with_slots(self, pickle_persistence): bot_data = pickle_persistence.get_bot_data() bot_data['message'] = Message(3, None, Chat(2, type='supergroup')) pickle_persistence.update_bot_data(bot_data) retrieved = pickle_persistence.get_bot_data() assert retrieved == bot_data def test_no_files_present_multi_file(self, pickle_persistence): assert pickle_persistence.get_user_data() == defaultdict(dict) assert pickle_persistence.get_user_data() == defaultdict(dict) assert pickle_persistence.get_chat_data() == defaultdict(dict) assert pickle_persistence.get_chat_data() == defaultdict(dict) assert pickle_persistence.get_bot_data() == {} assert pickle_persistence.get_bot_data() == {} assert pickle_persistence.get_callback_data() is None assert pickle_persistence.get_conversations('noname') == {} assert pickle_persistence.get_conversations('noname') == {} def test_no_files_present_single_file(self, pickle_persistence): pickle_persistence.single_file = True assert pickle_persistence.get_user_data() == defaultdict(dict) assert pickle_persistence.get_chat_data() == defaultdict(dict) assert pickle_persistence.get_bot_data() == {} assert pickle_persistence.get_callback_data() is None assert pickle_persistence.get_conversations('noname') == {} def test_with_bad_multi_file(self, pickle_persistence, bad_pickle_files): with pytest.raises(TypeError, match='pickletest_user_data'): pickle_persistence.get_user_data() with pytest.raises(TypeError, match='pickletest_chat_data'): pickle_persistence.get_chat_data() with pytest.raises(TypeError, match='pickletest_bot_data'): pickle_persistence.get_bot_data() with pytest.raises(TypeError, match='pickletest_callback_data'): pickle_persistence.get_callback_data() with pytest.raises(TypeError, match='pickletest_conversations'): pickle_persistence.get_conversations('name') def test_with_invalid_multi_file(self, pickle_persistence, invalid_pickle_files): with pytest.raises(TypeError, match='pickletest_user_data does not contain'): pickle_persistence.get_user_data() with pytest.raises(TypeError, match='pickletest_chat_data does not contain'): pickle_persistence.get_chat_data() with pytest.raises(TypeError, match='pickletest_bot_data does not contain'): pickle_persistence.get_bot_data() with pytest.raises(TypeError, match='pickletest_callback_data does not contain'): pickle_persistence.get_callback_data() with pytest.raises(TypeError, match='pickletest_conversations does not contain'): pickle_persistence.get_conversations('name') def test_with_bad_single_file(self, pickle_persistence, bad_pickle_files): pickle_persistence.single_file = True with pytest.raises(TypeError, match='pickletest'): pickle_persistence.get_user_data() with pytest.raises(TypeError, match='pickletest'): pickle_persistence.get_chat_data() with pytest.raises(TypeError, match='pickletest'): pickle_persistence.get_bot_data() with pytest.raises(TypeError, match='pickletest'): pickle_persistence.get_callback_data() with pytest.raises(TypeError, match='pickletest'): pickle_persistence.get_conversations('name') 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'): pickle_persistence.get_user_data() with pytest.raises(TypeError, match='pickletest does not contain'): pickle_persistence.get_chat_data() with pytest.raises(TypeError, match='pickletest does not contain'): pickle_persistence.get_bot_data() with pytest.raises(TypeError, match='pickletest does not contain'): pickle_persistence.get_callback_data() with pytest.raises(TypeError, match='pickletest does not contain'): pickle_persistence.get_conversations('name') def test_with_good_multi_file(self, pickle_persistence, good_pickle_files): user_data = pickle_persistence.get_user_data() assert isinstance(user_data, defaultdict) assert user_data[12345]['test1'] == 'test2' assert user_data[67890][3] == 'test4' assert user_data[54321] == {} chat_data = pickle_persistence.get_chat_data() assert isinstance(chat_data, defaultdict) assert chat_data[-12345]['test1'] == 'test2' assert chat_data[-67890][3] == 'test4' assert chat_data[-54321] == {} bot_data = 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 = 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 = 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 = 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)] def test_with_good_single_file(self, pickle_persistence, good_pickle_files): pickle_persistence.single_file = True user_data = pickle_persistence.get_user_data() assert isinstance(user_data, defaultdict) assert user_data[12345]['test1'] == 'test2' assert user_data[67890][3] == 'test4' assert user_data[54321] == {} chat_data = pickle_persistence.get_chat_data() assert isinstance(chat_data, defaultdict) assert chat_data[-12345]['test1'] == 'test2' assert chat_data[-67890][3] == 'test4' assert chat_data[-54321] == {} bot_data = 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 = 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 = 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 = 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)] def test_with_multi_file_wo_bot_data(self, pickle_persistence, pickle_files_wo_bot_data): user_data = pickle_persistence.get_user_data() assert isinstance(user_data, defaultdict) assert user_data[12345]['test1'] == 'test2' assert user_data[67890][3] == 'test4' assert user_data[54321] == {} chat_data = pickle_persistence.get_chat_data() assert isinstance(chat_data, defaultdict) assert chat_data[-12345]['test1'] == 'test2' assert chat_data[-67890][3] == 'test4' assert chat_data[-54321] == {} bot_data = pickle_persistence.get_bot_data() assert isinstance(bot_data, dict) assert not bot_data.keys() callback_data = 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 = 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 = 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)] def test_with_multi_file_wo_callback_data( self, pickle_persistence, pickle_files_wo_callback_data ): user_data = pickle_persistence.get_user_data() assert isinstance(user_data, defaultdict) assert user_data[12345]['test1'] == 'test2' assert user_data[67890][3] == 'test4' assert user_data[54321] == {} chat_data = pickle_persistence.get_chat_data() assert isinstance(chat_data, defaultdict) assert chat_data[-12345]['test1'] == 'test2' assert chat_data[-67890][3] == 'test4' assert chat_data[-54321] == {} bot_data = 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 = pickle_persistence.get_callback_data() assert callback_data is None conversation1 = 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 = 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)] def test_with_single_file_wo_bot_data(self, pickle_persistence, pickle_files_wo_bot_data): pickle_persistence.single_file = True user_data = pickle_persistence.get_user_data() assert isinstance(user_data, defaultdict) assert user_data[12345]['test1'] == 'test2' assert user_data[67890][3] == 'test4' assert user_data[54321] == {} chat_data = pickle_persistence.get_chat_data() assert isinstance(chat_data, defaultdict) assert chat_data[-12345]['test1'] == 'test2' assert chat_data[-67890][3] == 'test4' assert chat_data[-54321] == {} bot_data = pickle_persistence.get_bot_data() assert isinstance(bot_data, dict) assert not bot_data.keys() callback_data = 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 = 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 = 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)] def test_with_single_file_wo_callback_data( self, pickle_persistence, pickle_files_wo_callback_data ): user_data = pickle_persistence.get_user_data() assert isinstance(user_data, defaultdict) assert user_data[12345]['test1'] == 'test2' assert user_data[67890][3] == 'test4' assert user_data[54321] == {} chat_data = pickle_persistence.get_chat_data() assert isinstance(chat_data, defaultdict) assert chat_data[-12345]['test1'] == 'test2' assert chat_data[-67890][3] == 'test4' assert chat_data[-54321] == {} bot_data = 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 = pickle_persistence.get_callback_data() assert callback_data is None conversation1 = 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 = 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)] def test_updating_multi_file(self, pickle_persistence, good_pickle_files): user_data = pickle_persistence.get_user_data() user_data[12345]['test3']['test4'] = 'test6' assert not pickle_persistence.user_data == user_data pickle_persistence.update_user_data(12345, user_data[12345]) user_data[12345]['test3']['test4'] = 'test7' assert not pickle_persistence.user_data == user_data pickle_persistence.update_user_data(12345, user_data[12345]) assert pickle_persistence.user_data == user_data with open('pickletest_user_data', 'rb') as f: user_data_test = defaultdict(dict, pickle.load(f)) assert user_data_test == user_data chat_data = pickle_persistence.get_chat_data() chat_data[-12345]['test3']['test4'] = 'test6' assert not pickle_persistence.chat_data == chat_data pickle_persistence.update_chat_data(-12345, chat_data[-12345]) chat_data[-12345]['test3']['test4'] = 'test7' assert not pickle_persistence.chat_data == chat_data pickle_persistence.update_chat_data(-12345, chat_data[-12345]) assert pickle_persistence.chat_data == chat_data with open('pickletest_chat_data', 'rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)) assert chat_data_test == chat_data bot_data = pickle_persistence.get_bot_data() bot_data['test3']['test4'] = 'test6' assert not pickle_persistence.bot_data == bot_data pickle_persistence.update_bot_data(bot_data) bot_data['test3']['test4'] = 'test7' assert not pickle_persistence.bot_data == bot_data pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data with open('pickletest_bot_data', 'rb') as f: bot_data_test = pickle.load(f) assert bot_data_test == bot_data callback_data = pickle_persistence.get_callback_data() callback_data[1]['test3'] = 'test4' assert not pickle_persistence.callback_data == callback_data pickle_persistence.update_callback_data(callback_data) callback_data[1]['test3'] = 'test5' assert not pickle_persistence.callback_data == callback_data pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data with open('pickletest_callback_data', 'rb') as f: callback_data_test = pickle.load(f) assert callback_data_test == callback_data conversation1 = pickle_persistence.get_conversations('name1') conversation1[(123, 123)] = 5 assert not pickle_persistence.conversations['name1'] == conversation1 pickle_persistence.update_conversation('name1', (123, 123), 5) assert pickle_persistence.conversations['name1'] == conversation1 assert pickle_persistence.get_conversations('name1') == conversation1 with open('pickletest_conversations', 'rb') as f: conversations_test = defaultdict(dict, pickle.load(f)) assert conversations_test['name1'] == conversation1 pickle_persistence.conversations = None pickle_persistence.update_conversation('name1', (123, 123), 5) assert pickle_persistence.conversations['name1'] == {(123, 123): 5} assert pickle_persistence.get_conversations('name1') == {(123, 123): 5} def test_updating_single_file(self, pickle_persistence, good_pickle_files): pickle_persistence.single_file = True user_data = pickle_persistence.get_user_data() user_data[12345]['test3']['test4'] = 'test6' assert not pickle_persistence.user_data == user_data pickle_persistence.update_user_data(12345, user_data[12345]) user_data[12345]['test3']['test4'] = 'test7' assert not pickle_persistence.user_data == user_data pickle_persistence.update_user_data(12345, user_data[12345]) assert pickle_persistence.user_data == user_data with open('pickletest', 'rb') as f: user_data_test = defaultdict(dict, pickle.load(f)['user_data']) assert user_data_test == user_data chat_data = pickle_persistence.get_chat_data() chat_data[-12345]['test3']['test4'] = 'test6' assert not pickle_persistence.chat_data == chat_data pickle_persistence.update_chat_data(-12345, chat_data[-12345]) chat_data[-12345]['test3']['test4'] = 'test7' assert not pickle_persistence.chat_data == chat_data pickle_persistence.update_chat_data(-12345, chat_data[-12345]) assert pickle_persistence.chat_data == chat_data with open('pickletest', 'rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)['chat_data']) assert chat_data_test == chat_data bot_data = pickle_persistence.get_bot_data() bot_data['test3']['test4'] = 'test6' assert not pickle_persistence.bot_data == bot_data pickle_persistence.update_bot_data(bot_data) bot_data['test3']['test4'] = 'test7' assert not pickle_persistence.bot_data == bot_data pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data with open('pickletest', 'rb') as f: bot_data_test = pickle.load(f)['bot_data'] assert bot_data_test == bot_data callback_data = pickle_persistence.get_callback_data() callback_data[1]['test3'] = 'test4' assert not pickle_persistence.callback_data == callback_data pickle_persistence.update_callback_data(callback_data) callback_data[1]['test3'] = 'test5' assert not pickle_persistence.callback_data == callback_data pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data with open('pickletest', 'rb') as f: callback_data_test = pickle.load(f)['callback_data'] assert callback_data_test == callback_data conversation1 = pickle_persistence.get_conversations('name1') conversation1[(123, 123)] = 5 assert not pickle_persistence.conversations['name1'] == conversation1 pickle_persistence.update_conversation('name1', (123, 123), 5) assert pickle_persistence.conversations['name1'] == conversation1 assert pickle_persistence.get_conversations('name1') == conversation1 with open('pickletest', 'rb') as f: conversations_test = defaultdict(dict, pickle.load(f)['conversations']) assert conversations_test['name1'] == conversation1 pickle_persistence.conversations = None pickle_persistence.update_conversation('name1', (123, 123), 5) assert pickle_persistence.conversations['name1'] == {(123, 123): 5} assert pickle_persistence.get_conversations('name1') == {(123, 123): 5} 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, ] ) pickle_persistence.flush() with pytest.raises(FileNotFoundError, match='pickletest'): open('pickletest', 'rb') def test_save_on_flush_multi_files(self, pickle_persistence, good_pickle_files): # Should run without error pickle_persistence.flush() pickle_persistence.on_flush = True user_data = pickle_persistence.get_user_data() user_data[54321]['test9'] = 'test 10' assert not pickle_persistence.user_data == user_data pickle_persistence.update_user_data(54321, user_data[54321]) assert pickle_persistence.user_data == user_data with open('pickletest_user_data', 'rb') as f: user_data_test = defaultdict(dict, pickle.load(f)) assert not user_data_test == user_data chat_data = pickle_persistence.get_chat_data() chat_data[54321]['test9'] = 'test 10' assert not pickle_persistence.chat_data == chat_data pickle_persistence.update_chat_data(54321, chat_data[54321]) assert pickle_persistence.chat_data == chat_data with open('pickletest_chat_data', 'rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)) assert not chat_data_test == chat_data bot_data = pickle_persistence.get_bot_data() bot_data['test6'] = 'test 7' assert not pickle_persistence.bot_data == bot_data pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data with open('pickletest_bot_data', 'rb') as f: bot_data_test = pickle.load(f) assert not bot_data_test == bot_data callback_data = pickle_persistence.get_callback_data() callback_data[1]['test3'] = 'test4' assert not pickle_persistence.callback_data == callback_data pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data with open('pickletest_callback_data', 'rb') as f: callback_data_test = pickle.load(f) assert not callback_data_test == callback_data conversation1 = pickle_persistence.get_conversations('name1') conversation1[(123, 123)] = 5 assert not pickle_persistence.conversations['name1'] == conversation1 pickle_persistence.update_conversation('name1', (123, 123), 5) assert pickle_persistence.conversations['name1'] == conversation1 with open('pickletest_conversations', 'rb') as f: conversations_test = defaultdict(dict, pickle.load(f)) assert not conversations_test['name1'] == conversation1 pickle_persistence.flush() with open('pickletest_user_data', 'rb') as f: user_data_test = defaultdict(dict, pickle.load(f)) assert user_data_test == user_data with open('pickletest_chat_data', 'rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)) assert chat_data_test == chat_data with open('pickletest_bot_data', 'rb') as f: bot_data_test = pickle.load(f) assert bot_data_test == bot_data with open('pickletest_conversations', 'rb') as f: conversations_test = defaultdict(dict, pickle.load(f)) assert conversations_test['name1'] == conversation1 def test_save_on_flush_single_files(self, pickle_persistence, good_pickle_files): # Should run without error pickle_persistence.flush() pickle_persistence.on_flush = True pickle_persistence.single_file = True user_data = pickle_persistence.get_user_data() user_data[54321]['test9'] = 'test 10' assert not pickle_persistence.user_data == user_data pickle_persistence.update_user_data(54321, user_data[54321]) assert pickle_persistence.user_data == user_data with open('pickletest', 'rb') as f: user_data_test = defaultdict(dict, pickle.load(f)['user_data']) assert not user_data_test == user_data chat_data = pickle_persistence.get_chat_data() chat_data[54321]['test9'] = 'test 10' assert not pickle_persistence.chat_data == chat_data pickle_persistence.update_chat_data(54321, chat_data[54321]) assert pickle_persistence.chat_data == chat_data with open('pickletest', 'rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)['chat_data']) assert not chat_data_test == chat_data bot_data = pickle_persistence.get_bot_data() bot_data['test6'] = 'test 7' assert not pickle_persistence.bot_data == bot_data pickle_persistence.update_bot_data(bot_data) assert pickle_persistence.bot_data == bot_data with open('pickletest', 'rb') as f: bot_data_test = pickle.load(f)['bot_data'] assert not bot_data_test == bot_data callback_data = pickle_persistence.get_callback_data() callback_data[1]['test3'] = 'test4' assert not pickle_persistence.callback_data == callback_data pickle_persistence.update_callback_data(callback_data) assert pickle_persistence.callback_data == callback_data with open('pickletest', 'rb') as f: callback_data_test = pickle.load(f)['callback_data'] assert not callback_data_test == callback_data conversation1 = pickle_persistence.get_conversations('name1') conversation1[(123, 123)] = 5 assert not pickle_persistence.conversations['name1'] == conversation1 pickle_persistence.update_conversation('name1', (123, 123), 5) assert pickle_persistence.conversations['name1'] == conversation1 with open('pickletest', 'rb') as f: conversations_test = defaultdict(dict, pickle.load(f)['conversations']) assert not conversations_test['name1'] == conversation1 pickle_persistence.flush() with open('pickletest', 'rb') as f: user_data_test = defaultdict(dict, pickle.load(f)['user_data']) assert user_data_test == user_data with open('pickletest', 'rb') as f: chat_data_test = defaultdict(dict, pickle.load(f)['chat_data']) assert chat_data_test == chat_data with open('pickletest', 'rb') as f: bot_data_test = pickle.load(f)['bot_data'] assert bot_data_test == bot_data with open('pickletest', 'rb') as f: conversations_test = defaultdict(dict, pickle.load(f)['conversations']) assert conversations_test['name1'] == conversation1 def test_with_handler(self, bot, update, bot_data, pickle_persistence, good_pickle_files): u = Updater(bot=bot, persistence=pickle_persistence, use_context=True) dp = u.dispatcher bot.callback_data_cache.clear_callback_data() bot.callback_data_cache.clear_callback_queries() def first(update, context): if not context.user_data == {}: pytest.fail() if not context.chat_data == {}: pytest.fail() if not context.bot_data == bot_data: pytest.fail() if not context.bot.callback_data_cache.persistence_data == ([], {}): pytest.fail() context.user_data['test1'] = 'test2' context.chat_data['test3'] = 'test4' context.bot_data['test1'] = 'test0' context.bot.callback_data_cache._callback_queries['test1'] = 'test0' def second(update, context): if not context.user_data['test1'] == 'test2': pytest.fail() if not context.chat_data['test3'] == 'test4': pytest.fail() if not context.bot_data['test1'] == 'test0': pytest.fail() if not context.bot.callback_data_cache.persistence_data == ([], {'test1': 'test0'}): pytest.fail() h1 = MessageHandler(None, first, pass_user_data=True, pass_chat_data=True) h2 = MessageHandler(None, second, pass_user_data=True, pass_chat_data=True) dp.add_handler(h1) dp.process_update(update) pickle_persistence_2 = PicklePersistence( filename='pickletest', store_user_data=True, store_chat_data=True, store_bot_data=True, store_callback_data=True, single_file=False, on_flush=False, ) u = Updater(bot=bot, persistence=pickle_persistence_2) dp = u.dispatcher dp.add_handler(h2) dp.process_update(update) def test_flush_on_stop(self, bot, update, pickle_persistence): u = Updater(bot=bot, persistence=pickle_persistence) dp = u.dispatcher u.running = True dp.user_data[4242424242]['my_test'] = 'Working!' dp.chat_data[-4242424242]['my_test2'] = 'Working2!' dp.bot_data['test'] = 'Working3!' dp.bot.callback_data_cache._callback_queries['test'] = 'Working4!' u._signal_handler(signal.SIGINT, None) pickle_persistence_2 = PicklePersistence( filename='pickletest', store_bot_data=True, store_user_data=True, store_chat_data=True, store_callback_data=True, single_file=False, on_flush=False, ) assert pickle_persistence_2.get_user_data()[4242424242]['my_test'] == 'Working!' assert pickle_persistence_2.get_chat_data()[-4242424242]['my_test2'] == 'Working2!' assert pickle_persistence_2.get_bot_data()['test'] == 'Working3!' data = pickle_persistence_2.get_callback_data()[1] assert data['test'] == 'Working4!' def test_flush_on_stop_only_bot(self, bot, update, pickle_persistence_only_bot): u = Updater(bot=bot, persistence=pickle_persistence_only_bot) dp = u.dispatcher u.running = True dp.user_data[4242424242]['my_test'] = 'Working!' dp.chat_data[-4242424242]['my_test2'] = 'Working2!' dp.bot_data['my_test3'] = 'Working3!' dp.bot.callback_data_cache._callback_queries['test'] = 'Working4!' u._signal_handler(signal.SIGINT, None) pickle_persistence_2 = PicklePersistence( filename='pickletest', store_user_data=False, store_chat_data=False, store_bot_data=True, store_callback_data=False, single_file=False, on_flush=False, ) assert pickle_persistence_2.get_user_data() == {} assert pickle_persistence_2.get_chat_data() == {} assert pickle_persistence_2.get_bot_data()['my_test3'] == 'Working3!' assert pickle_persistence_2.get_callback_data() is None def test_flush_on_stop_only_chat(self, bot, update, pickle_persistence_only_chat): u = Updater(bot=bot, persistence=pickle_persistence_only_chat) dp = u.dispatcher u.running = True dp.user_data[4242424242]['my_test'] = 'Working!' dp.chat_data[-4242424242]['my_test2'] = 'Working2!' dp.bot_data['my_test3'] = 'Working3!' dp.bot.callback_data_cache._callback_queries['test'] = 'Working4!' u._signal_handler(signal.SIGINT, None) pickle_persistence_2 = PicklePersistence( filename='pickletest', store_user_data=False, store_chat_data=True, store_bot_data=False, store_callback_data=False, single_file=False, on_flush=False, ) assert pickle_persistence_2.get_user_data() == {} assert pickle_persistence_2.get_chat_data()[-4242424242]['my_test2'] == 'Working2!' assert pickle_persistence_2.get_bot_data() == {} assert pickle_persistence_2.get_callback_data() is None def test_flush_on_stop_only_user(self, bot, update, pickle_persistence_only_user): u = Updater(bot=bot, persistence=pickle_persistence_only_user) dp = u.dispatcher u.running = True dp.user_data[4242424242]['my_test'] = 'Working!' dp.chat_data[-4242424242]['my_test2'] = 'Working2!' dp.bot_data['my_test3'] = 'Working3!' dp.bot.callback_data_cache._callback_queries['test'] = 'Working4!' u._signal_handler(signal.SIGINT, None) pickle_persistence_2 = PicklePersistence( filename='pickletest', store_user_data=True, store_chat_data=False, store_bot_data=False, store_callback_data=False, single_file=False, on_flush=False, ) assert pickle_persistence_2.get_user_data()[4242424242]['my_test'] == 'Working!' assert pickle_persistence_2.get_chat_data() == {} assert pickle_persistence_2.get_bot_data() == {} assert pickle_persistence_2.get_callback_data() is None def test_flush_on_stop_only_callback(self, bot, update, pickle_persistence_only_callback): u = Updater(bot=bot, persistence=pickle_persistence_only_callback) dp = u.dispatcher u.running = True dp.user_data[4242424242]['my_test'] = 'Working!' dp.chat_data[-4242424242]['my_test2'] = 'Working2!' dp.bot_data['my_test3'] = 'Working3!' dp.bot.callback_data_cache._callback_queries['test'] = 'Working4!' u._signal_handler(signal.SIGINT, None) del dp del u del pickle_persistence_only_callback pickle_persistence_2 = PicklePersistence( filename='pickletest', store_user_data=False, store_chat_data=False, store_bot_data=False, store_callback_data=True, single_file=False, on_flush=False, ) assert pickle_persistence_2.get_user_data() == {} assert pickle_persistence_2.get_chat_data() == {} assert pickle_persistence_2.get_bot_data() == {} data = pickle_persistence_2.get_callback_data()[1] assert data['test'] == 'Working4!' def test_with_conversation_handler(self, dp, update, good_pickle_files, pickle_persistence): dp.persistence = pickle_persistence dp.use_context = True NEXT, NEXT2 = range(2) def start(update, context): return NEXT start = CommandHandler('start', start) def next_callback(update, context): return NEXT2 next_handler = MessageHandler(None, next_callback) def next2(update, context): return ConversationHandler.END next2 = MessageHandler(None, next2) ch = ConversationHandler( [start], {NEXT: [next_handler], NEXT2: [next2]}, [], name='name2', persistent=True ) dp.add_handler(ch) assert ch.conversations[ch._get_key(update)] == 1 dp.process_update(update) assert ch._get_key(update) not in ch.conversations update.message.text = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] dp.process_update(update) assert ch.conversations[ch._get_key(update)] == 0 assert ch.conversations == pickle_persistence.conversations['name2'] def test_with_nested_conversationHandler( self, dp, update, good_pickle_files, pickle_persistence ): dp.persistence = pickle_persistence dp.use_context = True NEXT2, NEXT3 = range(1, 3) def start(update, context): return NEXT2 start = CommandHandler('start', start) def next_callback(update, context): return NEXT2 next_handler = MessageHandler(None, next_callback) def next2(update, context): return ConversationHandler.END next2 = MessageHandler(None, next2) nested_ch = ConversationHandler( [next_handler], {NEXT2: [next2]}, [], name='name3', persistent=True, map_to_parent={ConversationHandler.END: ConversationHandler.END}, ) ch = ConversationHandler( [start], {NEXT2: [nested_ch], NEXT3: []}, [], name='name2', persistent=True ) dp.add_handler(ch) assert ch.conversations[ch._get_key(update)] == 1 assert nested_ch.conversations[nested_ch._get_key(update)] == 1 dp.process_update(update) assert ch._get_key(update) not in ch.conversations assert nested_ch._get_key(update) not in nested_ch.conversations update.message.text = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] dp.process_update(update) assert ch.conversations[ch._get_key(update)] == 1 assert ch.conversations == pickle_persistence.conversations['name2'] assert nested_ch._get_key(update) not in nested_ch.conversations dp.process_update(update) assert ch.conversations[ch._get_key(update)] == 1 assert ch.conversations == pickle_persistence.conversations['name2'] assert nested_ch.conversations[nested_ch._get_key(update)] == 1 assert nested_ch.conversations == pickle_persistence.conversations['name3'] def test_with_job(self, job_queue, cdp, pickle_persistence): cdp.bot.arbitrary_callback_data = True def job_callback(context): context.bot_data['test1'] = '456' context.dispatcher.chat_data[123]['test2'] = '789' context.dispatcher.user_data[789]['test3'] = '123' context.bot.callback_data_cache._callback_queries['test'] = 'Working4!' cdp.persistence = pickle_persistence job_queue.set_dispatcher(cdp) job_queue.start() job_queue.run_once(job_callback, 0.01) sleep(0.5) bot_data = pickle_persistence.get_bot_data() assert bot_data == {'test1': '456'} chat_data = pickle_persistence.get_chat_data() assert chat_data[123] == {'test2': '789'} user_data = pickle_persistence.get_user_data() assert user_data[789] == {'test3': '123'} data = pickle_persistence.get_callback_data()[1] assert data['test'] == 'Working4!' @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]) 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(persistence.get_user_data()[1], ud) assert persistence.get_user_data()[1] == 0 assert isinstance(persistence.get_chat_data()[1], cd) assert persistence.get_chat_data()[1] == 0 assert isinstance(persistence.get_bot_data(), bd) assert persistence.get_bot_data() == 0 persistence.user_data = None persistence.chat_data = None persistence.update_user_data(1, ud(1)) persistence.update_chat_data(1, cd(1)) persistence.update_bot_data(bd(1)) assert persistence.get_user_data()[1] == 1 assert persistence.get_chat_data()[1] == 1 assert persistence.get_bot_data() == 1 persistence.flush() persistence = PicklePersistence('pickletest', single_file=singlefile, context_types=cc) assert isinstance(persistence.get_user_data()[1], ud) assert persistence.get_user_data()[1] == 1 assert isinstance(persistence.get_chat_data()[1], cd) assert persistence.get_chat_data()[1] == 1 assert isinstance(persistence.get_bot_data(), bd) assert persistence.get_bot_data() == 1 @pytest.fixture(scope='function') def user_data_json(user_data): return json.dumps(user_data) @pytest.fixture(scope='function') def chat_data_json(chat_data): return json.dumps(chat_data) @pytest.fixture(scope='function') def bot_data_json(bot_data): return json.dumps(bot_data) @pytest.fixture(scope='function') def callback_data_json(callback_data): return json.dumps(callback_data) @pytest.fixture(scope='function') 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: def test_slot_behaviour(self, mro_slots, recwarn): inst = DictPersistence() for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" # assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.store_user_data = 'should give warning', {} assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_no_json_given(self): dict_persistence = DictPersistence() assert dict_persistence.get_user_data() == defaultdict(dict) assert dict_persistence.get_chat_data() == defaultdict(dict) assert dict_persistence.get_bot_data() == {} assert dict_persistence.get_callback_data() is None assert dict_persistence.get_conversations('noname') == {} 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) def test_invalid_json_string_given(self, pickle_persistence, bad_pickle_files): 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) 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 = dict_persistence.get_user_data() assert isinstance(user_data, defaultdict) assert user_data[12345]['test1'] == 'test2' assert user_data[67890][3] == 'test4' assert user_data[54321] == {} chat_data = dict_persistence.get_chat_data() assert isinstance(chat_data, defaultdict) assert chat_data[-12345]['test1'] == 'test2' assert chat_data[-67890][3] == 'test4' assert chat_data[-54321] == {} bot_data = 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 = 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 = 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 = 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)] 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' 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 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 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, store_callback_data=True, ) user_data = dict_persistence.get_user_data() user_data[12345]['test3']['test4'] = 'test6' assert not dict_persistence.user_data == user_data assert not dict_persistence.user_data_json == json.dumps(user_data) dict_persistence.update_user_data(12345, user_data[12345]) user_data[12345]['test3']['test4'] = 'test7' assert not dict_persistence.user_data == user_data assert not dict_persistence.user_data_json == json.dumps(user_data) 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) chat_data = dict_persistence.get_chat_data() chat_data[-12345]['test3']['test4'] = 'test6' assert not dict_persistence.chat_data == chat_data assert not dict_persistence.chat_data_json == json.dumps(chat_data) dict_persistence.update_chat_data(-12345, chat_data[-12345]) chat_data[-12345]['test3']['test4'] = 'test7' assert not dict_persistence.chat_data == chat_data assert not dict_persistence.chat_data_json == json.dumps(chat_data) 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) bot_data = dict_persistence.get_bot_data() bot_data['test3']['test4'] = 'test6' assert not dict_persistence.bot_data == bot_data assert not dict_persistence.bot_data_json == json.dumps(bot_data) dict_persistence.update_bot_data(bot_data) bot_data['test3']['test4'] = 'test7' assert not dict_persistence.bot_data == bot_data assert not dict_persistence.bot_data_json == json.dumps(bot_data) 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 = dict_persistence.get_callback_data() callback_data[1]['test3'] = 'test4' callback_data[0][0][2]['button2'] = 'test41' assert not dict_persistence.callback_data == callback_data assert not dict_persistence.callback_data_json == json.dumps(callback_data) dict_persistence.update_callback_data(callback_data) callback_data[1]['test3'] = 'test5' callback_data[0][0][2]['button2'] = 'test42' assert not dict_persistence.callback_data == callback_data assert not dict_persistence.callback_data_json == json.dumps(callback_data) 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 = dict_persistence.get_conversations('name1') conversation1[(123, 123)] = 5 assert not dict_persistence.conversations['name1'] == conversation1 dict_persistence.update_conversation('name1', (123, 123), 5) assert dict_persistence.conversations['name1'] == conversation1 conversations['name1'][(123, 123)] = 5 assert dict_persistence.conversations_json == encode_conversations_to_json(conversations) assert dict_persistence.get_conversations('name1') == conversation1 dict_persistence._conversations = None dict_persistence.update_conversation('name1', (123, 123), 5) assert dict_persistence.conversations['name1'] == {(123, 123): 5} assert dict_persistence.get_conversations('name1') == {(123, 123): 5} assert dict_persistence.conversations_json == encode_conversations_to_json( {"name1": {(123, 123): 5}} ) def test_with_handler(self, bot, update): dict_persistence = DictPersistence(store_callback_data=True) u = Updater(bot=bot, persistence=dict_persistence, use_context=True) dp = u.dispatcher def first(update, context): if not context.user_data == {}: pytest.fail() if not context.chat_data == {}: pytest.fail() if not context.bot_data == {}: pytest.fail() if not context.bot.callback_data_cache.persistence_data == ([], {}): pytest.fail() context.user_data['test1'] = 'test2' context.chat_data[3] = 'test4' context.bot_data['test1'] = 'test0' context.bot.callback_data_cache._callback_queries['test1'] = 'test0' def second(update, context): if not context.user_data['test1'] == 'test2': pytest.fail() if not context.chat_data[3] == 'test4': pytest.fail() if not context.bot_data['test1'] == 'test0': pytest.fail() if not context.bot.callback_data_cache.persistence_data == ([], {'test1': 'test0'}): pytest.fail() h1 = MessageHandler(Filters.all, first) h2 = MessageHandler(Filters.all, second) dp.add_handler(h1) dp.process_update(update) user_data = dict_persistence.user_data_json chat_data = dict_persistence.chat_data_json bot_data = dict_persistence.bot_data_json callback_data = dict_persistence.callback_data_json dict_persistence_2 = DictPersistence( user_data_json=user_data, chat_data_json=chat_data, bot_data_json=bot_data, callback_data_json=callback_data, store_callback_data=True, ) u = Updater(bot=bot, persistence=dict_persistence_2) dp = u.dispatcher dp.add_handler(h2) dp.process_update(update) def test_with_conversationHandler(self, dp, update, conversations_json): dict_persistence = DictPersistence(conversations_json=conversations_json) dp.persistence = dict_persistence dp.use_context = True NEXT, NEXT2 = range(2) def start(update, context): return NEXT start = CommandHandler('start', start) def next_callback(update, context): return NEXT2 next_handler = MessageHandler(None, next_callback) def next2(update, context): return ConversationHandler.END next2 = MessageHandler(None, next2) ch = ConversationHandler( [start], {NEXT: [next_handler], NEXT2: [next2]}, [], name='name2', persistent=True ) dp.add_handler(ch) assert ch.conversations[ch._get_key(update)] == 1 dp.process_update(update) assert ch._get_key(update) not in ch.conversations update.message.text = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] dp.process_update(update) assert ch.conversations[ch._get_key(update)] == 0 assert ch.conversations == dict_persistence.conversations['name2'] def test_with_nested_conversationHandler(self, dp, update, conversations_json): dict_persistence = DictPersistence(conversations_json=conversations_json) dp.persistence = dict_persistence dp.use_context = True NEXT2, NEXT3 = range(1, 3) def start(update, context): return NEXT2 start = CommandHandler('start', start) def next_callback(update, context): return NEXT2 next_handler = MessageHandler(None, next_callback) def next2(update, context): return ConversationHandler.END next2 = MessageHandler(None, next2) nested_ch = ConversationHandler( [next_handler], {NEXT2: [next2]}, [], name='name3', persistent=True, map_to_parent={ConversationHandler.END: ConversationHandler.END}, ) ch = ConversationHandler( [start], {NEXT2: [nested_ch], NEXT3: []}, [], name='name2', persistent=True ) dp.add_handler(ch) assert ch.conversations[ch._get_key(update)] == 1 assert nested_ch.conversations[nested_ch._get_key(update)] == 1 dp.process_update(update) assert ch._get_key(update) not in ch.conversations assert nested_ch._get_key(update) not in nested_ch.conversations update.message.text = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] dp.process_update(update) assert ch.conversations[ch._get_key(update)] == 1 assert ch.conversations == dict_persistence.conversations['name2'] assert nested_ch._get_key(update) not in nested_ch.conversations dp.process_update(update) assert ch.conversations[ch._get_key(update)] == 1 assert ch.conversations == dict_persistence.conversations['name2'] assert nested_ch.conversations[nested_ch._get_key(update)] == 1 assert nested_ch.conversations == dict_persistence.conversations['name3'] def test_with_job(self, job_queue, cdp): cdp.bot.arbitrary_callback_data = True def job_callback(context): context.bot_data['test1'] = '456' context.dispatcher.chat_data[123]['test2'] = '789' context.dispatcher.user_data[789]['test3'] = '123' context.bot.callback_data_cache._callback_queries['test'] = 'Working4!' dict_persistence = DictPersistence(store_callback_data=True) cdp.persistence = dict_persistence job_queue.set_dispatcher(cdp) job_queue.start() job_queue.run_once(job_callback, 0.01) sleep(0.8) bot_data = dict_persistence.get_bot_data() assert bot_data == {'test1': '456'} chat_data = dict_persistence.get_chat_data() assert chat_data[123] == {'test2': '789'} user_data = dict_persistence.get_user_data() assert user_data[789] == {'test3': '123'} data = dict_persistence.get_callback_data()[1] assert data['test'] == 'Working4!' python-telegram-bot-13.11/tests/test_photo.py000066400000000000000000000464311417656324400213500ustar00rootroot00000000000000#!/usr/bin/env python # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 io import BytesIO from pathlib import Path import pytest from flaky import flaky from telegram import Sticker, TelegramError, PhotoSize, InputFile, MessageEntity, Bot from telegram.error import BadRequest from telegram.utils.helpers import escape_markdown from tests.conftest import ( expect_bad_request, check_shortcut_call, check_shortcut_signature, check_defaults_handling, ) @pytest.fixture(scope='function') def photo_file(): f = open('tests/data/telegram.jpg', 'rb') yield f f.close() @pytest.fixture(scope='class') def _photo(bot, chat_id): def func(): with open('tests/data/telegram.jpg', 'rb') as f: return bot.send_photo(chat_id, photo=f, timeout=50).photo return expect_bad_request(func, 'Type of file mismatch', 'Telegram did not accept the file.') @pytest.fixture(scope='class') def thumb(_photo): return _photo[0] @pytest.fixture(scope='class') def photo(_photo): return _photo[1] class TestPhoto: width = 320 height = 320 caption = 'PhotoTest - *Caption*' photo_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.jpg' file_size = 29176 def test_slot_behaviour(self, photo, recwarn, mro_slots): for attr in photo.__slots__: assert getattr(photo, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not photo.__dict__, f"got missing slot(s): {photo.__dict__}" assert len(mro_slots(photo)) == len(set(mro_slots(photo))), "duplicate slot" photo.custom, photo.width = 'should give warning', self.width assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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): # We used to test for file_size as well, but TG apparently at some point apparently changed # the compression method and it's not really our job anyway ... assert photo.width == self.width assert photo.height == self.height assert thumb.width == 90 assert thumb.height == 90 @flaky(3, 1) def test_send_photo_all_args(self, bot, chat_id, photo_file, thumb, photo): message = bot.send_photo( chat_id, photo_file, caption=self.caption, disable_notification=False, protect_content=True, parse_mode='Markdown', ) assert isinstance(message.photo[0], PhotoSize) assert isinstance(message.photo[0].file_id, str) assert isinstance(message.photo[0].file_unique_id, str) assert message.photo[0].file_id != '' assert message.photo[0].file_unique_id != '' assert message.photo[0].width == thumb.width assert message.photo[0].height == thumb.height assert message.photo[0].file_size == thumb.file_size 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.photo[1].width == photo.width assert message.photo[1].height == photo.height assert message.photo[1].file_size == photo.file_size assert message.caption == TestPhoto.caption.replace('*', '') assert message.has_protected_content @flaky(3, 1) def test_send_photo_custom_filename(self, bot, chat_id, photo_file, monkeypatch): def make_assertion(url, data, **kwargs): return data['photo'].filename == 'custom_filename' monkeypatch.setattr(bot.request, 'post', make_assertion) assert bot.send_photo(chat_id, photo_file, filename='custom_filename') @flaky(3, 1) def test_send_photo_parse_mode_markdown(self, bot, chat_id, photo_file, thumb, photo): message = bot.send_photo(chat_id, photo_file, caption=self.caption, parse_mode='Markdown') assert isinstance(message.photo[0], PhotoSize) assert isinstance(message.photo[0].file_id, str) assert isinstance(message.photo[0].file_unique_id, str) assert message.photo[0].file_id != '' assert message.photo[0].file_unique_id != '' assert message.photo[0].width == thumb.width assert message.photo[0].height == thumb.height assert message.photo[0].file_size == thumb.file_size 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.photo[1].width == photo.width assert message.photo[1].height == photo.height assert message.photo[1].file_size == photo.file_size assert message.caption == TestPhoto.caption.replace('*', '') assert len(message.caption_entities) == 1 @flaky(3, 1) def test_send_photo_parse_mode_html(self, bot, chat_id, photo_file, thumb, photo): message = bot.send_photo(chat_id, photo_file, caption=self.caption, parse_mode='HTML') assert isinstance(message.photo[0], PhotoSize) assert isinstance(message.photo[0].file_id, str) assert isinstance(message.photo[0].file_unique_id, str) assert message.photo[0].file_id != '' assert message.photo[0].file_unique_id != '' assert message.photo[0].width == thumb.width assert message.photo[0].height == thumb.height assert message.photo[0].file_size == thumb.file_size 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.photo[1].width == photo.width assert message.photo[1].height == photo.height assert message.photo[1].file_size == photo.file_size assert message.caption == TestPhoto.caption.replace('', '').replace('', '') assert len(message.caption_entities) == 1 @flaky(3, 1) def test_send_photo_caption_entities(self, bot, chat_id, photo_file, thumb, photo): test_string = 'Italic Bold Code' entities = [ MessageEntity(MessageEntity.ITALIC, 0, 6), MessageEntity(MessageEntity.ITALIC, 7, 4), MessageEntity(MessageEntity.ITALIC, 12, 4), ] message = bot.send_photo( chat_id, photo_file, caption=test_string, caption_entities=entities ) assert message.caption == test_string assert message.caption_entities == entities @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_photo_default_parse_mode_1(self, default_bot, chat_id, photo_file, thumb, photo): test_string = 'Italic Bold Code' test_markdown_string = '_Italic_ *Bold* `Code`' message = default_bot.send_photo(chat_id, photo_file, caption=test_markdown_string) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_photo_default_parse_mode_2(self, default_bot, chat_id, photo_file, thumb, photo): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_photo_default_parse_mode_3(self, default_bot, chat_id, photo_file, thumb, photo): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) def test_send_photo_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('photo') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.send_photo(chat_id, file) assert test_flag monkeypatch.delattr(bot, '_post') @flaky(3, 1) @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'], ) def test_send_photo_default_allow_sending_without_reply( self, default_bot, chat_id, photo_file, thumb, photo, custom ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_photo( chat_id, photo_file, reply_to_message_id=reply_to_message.message_id ) @flaky(3, 1) def test_get_and_download(self, bot, photo): new_file = 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 new_file.download('telegram.jpg') assert os.path.isfile('telegram.jpg') is True @flaky(3, 1) def test_send_url_jpg_file(self, bot, chat_id, thumb, photo): message = bot.send_photo(chat_id, photo=self.photo_file_url) assert isinstance(message.photo[0], PhotoSize) assert isinstance(message.photo[0].file_id, str) assert isinstance(message.photo[0].file_unique_id, str) assert message.photo[0].file_id != '' assert message.photo[0].file_unique_id != '' # We used to test for width, height and file_size, but TG apparently started to treat # sending by URL and sending by upload differently and it's not really our job anyway ... 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 != '' # We used to test for width, height and file_size, but TG apparently started to treat # sending by URL and sending by upload differently and it's not really our job anyway ... @flaky(3, 1) def test_send_url_png_file(self, bot, chat_id): message = 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 != '' @flaky(3, 1) def test_send_url_gif_file(self, bot, chat_id): message = 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 != '' @flaky(3, 1) 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 open('tests/data/测试.png', 'rb') as f: message = 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 != '' @flaky(3, 1) def test_send_bytesio_jpg_file(self, bot, chat_id): file_name = 'tests/data/telegram_no_standard_header.jpg' # raw image bytes raw_bytes = BytesIO(open(file_name, 'rb').read()) input_file = InputFile(raw_bytes) assert input_file.mimetype == 'application/octet-stream' # raw image bytes with name info raw_bytes = BytesIO(open(file_name, 'rb').read()) raw_bytes.name = file_name input_file = InputFile(raw_bytes) assert input_file.mimetype == 'image/jpeg' # send raw photo raw_bytes = BytesIO(open(file_name, 'rb').read()) message = 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 def test_send_with_photosize(self, monkeypatch, bot, chat_id, photo): def test(url, data, **kwargs): return data['photo'] == photo.file_id monkeypatch.setattr(bot.request, 'post', test) message = bot.send_photo(photo=photo, chat_id=chat_id) assert message @flaky(3, 1) def test_resend(self, bot, chat_id, photo): message = bot.send_photo(chat_id=chat_id, photo=photo.file_id) thumb, photo, _ = message.photo assert isinstance(message.photo[0], PhotoSize) assert isinstance(message.photo[0].file_id, str) assert isinstance(message.photo[0].file_unique_id, str) assert message.photo[0].file_id != '' assert message.photo[0].file_unique_id != '' assert message.photo[0].width == thumb.width assert message.photo[0].height == thumb.height assert message.photo[0].file_size == thumb.file_size 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.photo[1].width == photo.width assert message.photo[1].height == photo.height assert message.photo[1].file_size == photo.file_size 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.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 @flaky(3, 1) def test_error_send_empty_file(self, bot, chat_id): with pytest.raises(TelegramError): bot.send_photo(chat_id=chat_id, photo=open(os.devnull, 'rb')) @flaky(3, 1) def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): bot.send_photo(chat_id=chat_id, photo='') def test_error_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): bot.send_photo(chat_id=chat_id) def test_get_file_instance_method(self, monkeypatch, photo): def make_assertion(*_, **kwargs): return kwargs['file_id'] == photo.file_id assert check_shortcut_signature(PhotoSize.get_file, Bot.get_file, ['file_id'], []) assert check_shortcut_call(photo.get_file, photo.bot, 'get_file') assert check_defaults_handling(photo.get_file, photo.bot) monkeypatch.setattr(photo.bot, 'get_file', make_assertion) assert photo.get_file() 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) 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-13.11/tests/test_poll.py000066400000000000000000000215411417656324400211600ustar00rootroot00000000000000#!/usr/bin/env python # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 datetime import datetime from telegram import Poll, PollOption, PollAnswer, User, MessageEntity from telegram.utils.helpers import to_timestamp @pytest.fixture(scope="class") def poll_option(): return PollOption(text=TestPollOption.text, voter_count=TestPollOption.voter_count) class TestPollOption: text = "test option" voter_count = 3 def test_slot_behaviour(self, poll_option, mro_slots, recwarn): for attr in poll_option.__slots__: assert getattr(poll_option, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not poll_option.__dict__, f"got missing slot(s): {poll_option.__dict__}" assert len(mro_slots(poll_option)) == len(set(mro_slots(poll_option))), "duplicate slot" poll_option.custom, poll_option.text = 'should give warning', self.text assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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="class") def poll_answer(): return PollAnswer( poll_id=TestPollAnswer.poll_id, user=TestPollAnswer.user, option_ids=TestPollAnswer.poll_id ) class TestPollAnswer: poll_id = 'id' user = User(1, '', False) option_ids = [2] def test_de_json(self): json_dict = { 'poll_id': self.poll_id, 'user': self.user.to_dict(), 'option_ids': self.option_ids, } poll_answer = PollAnswer.de_json(json_dict, None) assert poll_answer.poll_id == self.poll_id assert poll_answer.user == self.user assert poll_answer.option_ids == self.option_ids 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['user'] == poll_answer.user.to_dict() assert poll_answer_dict['option_ids'] == poll_answer.option_ids def test_equality(self): a = PollAnswer(123, self.user, [2]) b = PollAnswer(123, User(1, 'first', False), [2]) c = PollAnswer(123, self.user, [1, 2]) d = PollAnswer(456, self.user, [2]) e = 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) @pytest.fixture(scope='class') def poll(): return Poll( TestPoll.id_, TestPoll.question, TestPoll.options, TestPoll.total_voter_count, TestPoll.is_closed, TestPoll.is_anonymous, TestPoll.type, TestPoll.allows_multiple_answers, explanation=TestPoll.explanation, explanation_entities=TestPoll.explanation_entities, open_period=TestPoll.open_period, close_date=TestPoll.close_date, ) class TestPoll: 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.utcnow() 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.id == self.id_ assert poll.question == self.question assert poll.options == 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 == self.explanation_entities assert poll.open_period == self.open_period assert pytest.approx(poll.close_date == self.close_date) assert to_timestamp(poll.close_date) == to_timestamp(self.close_date) 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_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' 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'} 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) python-telegram-bot-13.11/tests/test_pollanswerhandler.py000066400000000000000000000145131417656324400237370ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram import ( Update, CallbackQuery, Bot, Message, User, Chat, PollAnswer, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, ) from telegram.ext import PollAnswerHandler, CallbackContext, JobQueue 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='function') def poll_answer(bot): return Update(0, poll_answer=PollAnswer(1, User(2, 'test user', False), [0, 1])) class TestPollAnswerHandler: test_flag = False def test_slot_behaviour(self, recwarn, mro_slots): handler = PollAnswerHandler(self.callback_basic) for attr in handler.__slots__: assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not handler.__dict__, f"got missing slot(s): {handler.__dict__}" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" handler.custom, handler.callback = 'should give warning', self.callback_basic assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(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_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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_basic(self, dp, poll_answer): handler = PollAnswerHandler(self.callback_basic) dp.add_handler(handler) assert handler.check_update(poll_answer) dp.process_update(poll_answer) assert self.test_flag def test_pass_user_or_chat_data(self, dp, poll_answer): handler = PollAnswerHandler(self.callback_data_1, pass_user_data=True) dp.add_handler(handler) dp.process_update(poll_answer) assert self.test_flag dp.remove_handler(handler) handler = PollAnswerHandler(self.callback_data_1, pass_chat_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(poll_answer) assert self.test_flag dp.remove_handler(handler) handler = PollAnswerHandler(self.callback_data_2, pass_chat_data=True, pass_user_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(poll_answer) assert self.test_flag def test_pass_job_or_update_queue(self, dp, poll_answer): handler = PollAnswerHandler(self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update(poll_answer) assert self.test_flag dp.remove_handler(handler) handler = PollAnswerHandler(self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(poll_answer) assert self.test_flag dp.remove_handler(handler) handler = PollAnswerHandler( self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(poll_answer) assert self.test_flag def test_other_update_types(self, false_update): handler = PollAnswerHandler(self.callback_basic) assert not handler.check_update(false_update) def test_context(self, cdp, poll_answer): handler = PollAnswerHandler(self.callback_context) cdp.add_handler(handler) cdp.process_update(poll_answer) assert self.test_flag python-telegram-bot-13.11/tests/test_pollhandler.py000066400000000000000000000144161417656324400225210ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram import ( Update, Poll, PollOption, Bot, Message, User, Chat, CallbackQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, ) from telegram.ext import PollHandler, CallbackContext, JobQueue 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='function') 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, recwarn, mro_slots): inst = PollHandler(self.callback_basic) for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.callback = 'should give warning', self.callback_basic assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(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_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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_basic(self, dp, poll): handler = PollHandler(self.callback_basic) dp.add_handler(handler) assert handler.check_update(poll) dp.process_update(poll) assert self.test_flag def test_pass_user_or_chat_data(self, dp, poll): handler = PollHandler(self.callback_data_1, pass_user_data=True) dp.add_handler(handler) dp.process_update(poll) assert self.test_flag dp.remove_handler(handler) handler = PollHandler(self.callback_data_1, pass_chat_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(poll) assert self.test_flag dp.remove_handler(handler) handler = PollHandler(self.callback_data_2, pass_chat_data=True, pass_user_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(poll) assert self.test_flag def test_pass_job_or_update_queue(self, dp, poll): handler = PollHandler(self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update(poll) assert self.test_flag dp.remove_handler(handler) handler = PollHandler(self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(poll) assert self.test_flag dp.remove_handler(handler) handler = PollHandler(self.callback_queue_2, pass_job_queue=True, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(poll) assert self.test_flag def test_other_update_types(self, false_update): handler = PollHandler(self.callback_basic) assert not handler.check_update(false_update) def test_context(self, cdp, poll): handler = PollHandler(self.callback_context) cdp.add_handler(handler) cdp.process_update(poll) assert self.test_flag python-telegram-bot-13.11/tests/test_precheckoutquery.py000066400000000000000000000123011417656324400236060ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 Update, User, PreCheckoutQuery, OrderInfo, Bot from tests.conftest import check_shortcut_call, check_shortcut_signature, check_defaults_handling @pytest.fixture(scope='class') def pre_checkout_query(bot): return PreCheckoutQuery( TestPreCheckoutQuery.id_, TestPreCheckoutQuery.from_user, TestPreCheckoutQuery.currency, TestPreCheckoutQuery.total_amount, TestPreCheckoutQuery.invoice_payload, shipping_option_id=TestPreCheckoutQuery.shipping_option_id, order_info=TestPreCheckoutQuery.order_info, bot=bot, ) class TestPreCheckoutQuery: 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() def test_slot_behaviour(self, pre_checkout_query, recwarn, mro_slots): inst = pre_checkout_query for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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_answer(self, monkeypatch, pre_checkout_query): 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 check_shortcut_call( pre_checkout_query.answer, pre_checkout_query.bot, 'answer_pre_checkout_query', ) assert check_defaults_handling(pre_checkout_query.answer, pre_checkout_query.bot) monkeypatch.setattr(pre_checkout_query.bot, 'answer_pre_checkout_query', make_assertion) assert pre_checkout_query.answer(ok=True) 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) python-telegram-bot-13.11/tests/test_precheckoutqueryhandler.py000066400000000000000000000150641417656324400251550ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram import ( Update, Chat, Bot, ChosenInlineResult, User, Message, CallbackQuery, InlineQuery, ShippingQuery, PreCheckoutQuery, ) from telegram.ext import PreCheckoutQueryHandler, CallbackContext, JobQueue 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(): return Update( 1, pre_checkout_query=PreCheckoutQuery( 'id', User(1, 'test user', False), 'EUR', 223, 'invoice_payload' ), ) class TestPreCheckoutQueryHandler: test_flag = False def test_slot_behaviour(self, recwarn, mro_slots): inst = PreCheckoutQueryHandler(self.callback_basic) for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.callback = 'should give warning', self.callback_basic assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(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_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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_basic(self, dp, pre_checkout_query): handler = PreCheckoutQueryHandler(self.callback_basic) dp.add_handler(handler) assert handler.check_update(pre_checkout_query) dp.process_update(pre_checkout_query) assert self.test_flag def test_pass_user_or_chat_data(self, dp, pre_checkout_query): handler = PreCheckoutQueryHandler(self.callback_data_1, pass_user_data=True) dp.add_handler(handler) dp.process_update(pre_checkout_query) assert self.test_flag dp.remove_handler(handler) handler = PreCheckoutQueryHandler(self.callback_data_1, pass_chat_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(pre_checkout_query) assert self.test_flag dp.remove_handler(handler) handler = PreCheckoutQueryHandler( self.callback_data_2, pass_chat_data=True, pass_user_data=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(pre_checkout_query) assert self.test_flag def test_pass_job_or_update_queue(self, dp, pre_checkout_query): handler = PreCheckoutQueryHandler(self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update(pre_checkout_query) assert self.test_flag dp.remove_handler(handler) handler = PreCheckoutQueryHandler(self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(pre_checkout_query) assert self.test_flag dp.remove_handler(handler) handler = PreCheckoutQueryHandler( self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(pre_checkout_query) assert self.test_flag def test_other_update_types(self, false_update): handler = PreCheckoutQueryHandler(self.callback_basic) assert not handler.check_update(false_update) def test_context(self, cdp, pre_checkout_query): handler = PreCheckoutQueryHandler(self.callback_context) cdp.add_handler(handler) cdp.process_update(pre_checkout_query) assert self.test_flag python-telegram-bot-13.11/tests/test_promise.py000066400000000000000000000113121417656324400216630ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 logging import pytest from telegram import TelegramError from telegram.ext.utils.promise import Promise class TestPromise: """ Here we just test the things that are not covered by the other tests anyway """ test_flag = False def test_slot_behaviour(self, recwarn, mro_slots): inst = Promise(self.test_call, [], {}) for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.args = 'should give warning', [] assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def test_call(self): def callback(): self.test_flag = True promise = Promise(callback, [], {}) promise() assert promise.done assert self.test_flag def test_run_with_exception(self): def callback(): raise TelegramError('Error') promise = Promise(callback, [], {}) promise.run() assert promise.done assert not self.test_flag assert isinstance(promise.exception, TelegramError) def test_wait_for_exception(self): def callback(): raise TelegramError('Error') promise = Promise(callback, [], {}) promise.run() with pytest.raises(TelegramError, match='Error'): promise.result() def test_done_cb_after_run(self): def callback(): return "done!" def done_callback(_): self.test_flag = True promise = Promise(callback, [], {}) promise.run() promise.add_done_callback(done_callback) assert promise.result() == "done!" assert self.test_flag is True def test_done_cb_after_run_excp(self): def callback(): return "done!" def done_callback(_): raise Exception("Error!") promise = Promise(callback, [], {}) promise.run() assert promise.result() == "done!" with pytest.raises(Exception) as err: promise.add_done_callback(done_callback) assert str(err) == "Error!" def test_done_cb_before_run(self): def callback(): return "done!" def done_callback(_): self.test_flag = True promise = Promise(callback, [], {}) promise.add_done_callback(done_callback) assert promise.result(0) != "done!" assert self.test_flag is False promise.run() assert promise.result() == "done!" assert self.test_flag is True def test_done_cb_before_run_excp(self, caplog): def callback(): return "done!" def done_callback(_): raise Exception("Error!") promise = Promise(callback, [], {}) promise.add_done_callback(done_callback) assert promise.result(0) != "done!" caplog.clear() with caplog.at_level(logging.WARNING): promise.run() assert len(caplog.records) == 2 assert caplog.records[0].message == ( "`done_callback` of a Promise raised the following exception." " The exception won't be handled by error handlers." ) assert caplog.records[1].message.startswith("Full traceback:") assert promise.result() == "done!" def test_done_cb_not_run_on_excp(self): def callback(): raise TelegramError('Error') def done_callback(_): self.test_flag = True promise = Promise(callback, [], {}) promise.add_done_callback(done_callback) promise.run() assert isinstance(promise.exception, TelegramError) assert promise.done assert self.test_flag is False python-telegram-bot-13.11/tests/test_proximityalerttriggered.py000066400000000000000000000074231417656324400252060ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, User, ProximityAlertTriggered @pytest.fixture(scope="class") def proximity_alert_triggered(): return ProximityAlertTriggered( traveler=TestProximityAlertTriggered.traveler, watcher=TestProximityAlertTriggered.watcher, distance=TestProximityAlertTriggered.distance, ) class TestProximityAlertTriggered: traveler = User(1, 'foo', False) watcher = User(2, 'bar', False) distance = 42 def test_slot_behaviour(self, proximity_alert_triggered, mro_slots, recwarn): inst = proximity_alert_triggered for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.traveler = 'should give warning', self.traveler assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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-13.11/tests/test_regexhandler.py000066400000000000000000000245271417656324400226710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram.utils.deprecate import TelegramDeprecationWarning from telegram import ( Message, Update, Chat, Bot, User, CallbackQuery, InlineQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, ) from telegram.ext import RegexHandler, CallbackContext, JobQueue 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): return Message( 1, None, Chat(1, ''), from_user=User(1, '', False), text='test message', bot=bot ) class TestRegexHandler: test_flag = False def test_slot_behaviour(self, recwarn, mro_slots): inst = RegexHandler("", self.callback_basic) for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.callback = 'should give warning', self.callback_basic assert 'custom' in str(recwarn[-1].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(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', ' message') if groupdict is not None: self.test_flag = groupdict == {'begin': 't', 'end': ' message'} def callback_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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_context_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'} def test_deprecation_Warning(self): with pytest.warns(TelegramDeprecationWarning, match='RegexHandler is deprecated.'): RegexHandler('.*', self.callback_basic) def test_basic(self, dp, message): handler = RegexHandler('.*', self.callback_basic) dp.add_handler(handler) assert handler.check_update(Update(0, message)) dp.process_update(Update(0, message)) assert self.test_flag def test_pattern(self, message): handler = RegexHandler('.*est.*', self.callback_basic) assert handler.check_update(Update(0, message)) handler = RegexHandler('.*not in here.*', self.callback_basic) assert not handler.check_update(Update(0, message)) def test_with_passing_group_dict(self, dp, message): handler = RegexHandler( '(?P.*)est(?P.*)', self.callback_group, pass_groups=True ) dp.add_handler(handler) dp.process_update(Update(0, message)) assert self.test_flag dp.remove_handler(handler) handler = RegexHandler( '(?P.*)est(?P.*)', self.callback_group, pass_groupdict=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(Update(0, message)) assert self.test_flag def test_edited(self, message): handler = RegexHandler( '.*', self.callback_basic, edited_updates=True, message_updates=False, channel_post_updates=False, ) assert 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_channel_post(self, message): handler = RegexHandler( '.*', self.callback_basic, edited_updates=False, message_updates=False, channel_post_updates=True, ) assert not handler.check_update(Update(0, edited_message=message)) assert not handler.check_update(Update(0, message=message)) assert handler.check_update(Update(0, channel_post=message)) assert not handler.check_update(Update(0, edited_channel_post=message)) def test_multiple_flags(self, message): handler = RegexHandler( '.*', self.callback_basic, edited_updates=True, message_updates=True, channel_post_updates=True, ) assert handler.check_update(Update(0, edited_message=message)) assert handler.check_update(Update(0, message=message)) assert handler.check_update(Update(0, channel_post=message)) assert handler.check_update(Update(0, edited_channel_post=message)) def test_none_allowed(self): with pytest.raises(ValueError, match='are all False'): RegexHandler( '.*', self.callback_basic, message_updates=False, channel_post_updates=False, edited_updates=False, ) def test_pass_user_or_chat_data(self, dp, message): handler = RegexHandler('.*', self.callback_data_1, pass_user_data=True) dp.add_handler(handler) dp.process_update(Update(0, message=message)) assert self.test_flag dp.remove_handler(handler) handler = RegexHandler('.*', self.callback_data_1, pass_chat_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(Update(0, message=message)) assert self.test_flag dp.remove_handler(handler) handler = RegexHandler( '.*', self.callback_data_2, pass_chat_data=True, pass_user_data=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(Update(0, message=message)) assert self.test_flag def test_pass_job_or_update_queue(self, dp, message): handler = RegexHandler('.*', self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update(Update(0, message=message)) assert self.test_flag dp.remove_handler(handler) handler = RegexHandler('.*', self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(Update(0, message=message)) assert self.test_flag dp.remove_handler(handler) handler = RegexHandler( '.*', self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(Update(0, message=message)) assert self.test_flag def test_other_update_types(self, false_update): handler = RegexHandler('.*', self.callback_basic, edited_updates=True) assert not handler.check_update(false_update) def test_context(self, cdp, message): handler = RegexHandler(r'(t)est(.*)', self.callback_context) cdp.add_handler(handler) cdp.process_update(Update(0, message=message)) assert self.test_flag def test_context_pattern(self, cdp, message): handler = RegexHandler(r'(t)est(.*)', self.callback_context_pattern) cdp.add_handler(handler) cdp.process_update(Update(0, message=message)) assert self.test_flag cdp.remove_handler(handler) handler = RegexHandler(r'(t)est(.*)', self.callback_context_pattern) cdp.add_handler(handler) cdp.process_update(Update(0, message=message)) assert self.test_flag python-telegram-bot-13.11/tests/test_replykeyboardmarkup.py000066400000000000000000000145621417656324400243130ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup @pytest.fixture(scope='class') def reply_keyboard_markup(): return ReplyKeyboardMarkup( TestReplyKeyboardMarkup.keyboard, resize_keyboard=TestReplyKeyboardMarkup.resize_keyboard, one_time_keyboard=TestReplyKeyboardMarkup.one_time_keyboard, selective=TestReplyKeyboardMarkup.selective, input_field_placeholder=TestReplyKeyboardMarkup.input_field_placeholder, ) class TestReplyKeyboardMarkup: keyboard = [[KeyboardButton('button1'), KeyboardButton('button2')]] resize_keyboard = True one_time_keyboard = True selective = True input_field_placeholder = 'lol a keyboard' def test_slot_behaviour(self, reply_keyboard_markup, mro_slots, recwarn): inst = reply_keyboard_markup for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.selective = 'should give warning', self.selective assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @flaky(3, 1) def test_send_message_with_reply_keyboard_markup(self, bot, chat_id, reply_keyboard_markup): message = bot.send_message(chat_id, 'Text', reply_markup=reply_keyboard_markup) assert message.text == 'Text' @flaky(3, 1) def test_send_message_with_data_markup(self, bot, chat_id): message = bot.send_message(chat_id, 'text 2', reply_markup={'keyboard': [['1', '2']]}) assert message.text == 'text 2' 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 def test_expected_values(self, reply_keyboard_markup): assert isinstance(reply_keyboard_markup.keyboard, list) 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.input_field_placeholder == self.input_field_placeholder 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['input_field_placeholder'] == reply_keyboard_markup.input_field_placeholder ) 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) python-telegram-bot-13.11/tests/test_replykeyboardremove.py000066400000000000000000000046151417656324400243070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import ReplyKeyboardRemove @pytest.fixture(scope='class') def reply_keyboard_remove(): return ReplyKeyboardRemove(selective=TestReplyKeyboardRemove.selective) class TestReplyKeyboardRemove: remove_keyboard = True selective = True def test_slot_behaviour(self, reply_keyboard_remove, recwarn, mro_slots): inst = reply_keyboard_remove for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.selective = 'should give warning', self.selective assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @flaky(3, 1) def test_send_message_with_reply_keyboard_remove(self, bot, chat_id, reply_keyboard_remove): message = bot.send_message(chat_id, 'Text', reply_markup=reply_keyboard_remove) assert message.text == 'Text' 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 python-telegram-bot-13.11/tests/test_request.py000066400000000000000000000037321417656324400217040ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 TelegramError from telegram.utils.request import Request def test_slot_behaviour(recwarn, mro_slots): inst = Request() for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst._connect_timeout = 'should give warning', 10 assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_replaced_unprintable_char(): """ Clients can send arbitrary bytes in callback data. Make sure the correct error is raised in this case. """ server_response = b'{"invalid utf-8": "\x80", "result": "KUKU"}' assert Request._parse(server_response) == 'KUKU' def test_parse_illegal_json(): """ Clients can send arbitrary bytes in callback data. Make sure the correct error is raised in this case. """ server_response = b'{"invalid utf-8": "\x80", result: "KUKU"}' with pytest.raises(TelegramError, match='Invalid server response'): Request._parse(server_response) python-telegram-bot-13.11/tests/test_shippingaddress.py000066400000000000000000000115661417656324400234070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def shipping_address(): return ShippingAddress( TestShippingAddress.country_code, TestShippingAddress.state, TestShippingAddress.city, TestShippingAddress.street_line1, TestShippingAddress.street_line2, TestShippingAddress.post_code, ) class TestShippingAddress: country_code = 'GB' state = 'state' city = 'London' street_line1 = '12 Grimmauld Place' street_line2 = 'street_line2' post_code = 'WC1' def test_slot_behaviour(self, shipping_address, recwarn, mro_slots): inst = shipping_address for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.state = 'should give warning', self.state assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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-13.11/tests/test_shippingoption.py000066400000000000000000000056251417656324400232710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def shipping_option(): return ShippingOption( TestShippingOption.id_, TestShippingOption.title, TestShippingOption.prices ) class TestShippingOption: id_ = 'id' title = 'title' prices = [LabeledPrice('Fish Container', 100), LabeledPrice('Premium Fish Container', 1000)] def test_slot_behaviour(self, shipping_option, recwarn, mro_slots): inst = shipping_option for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_expected_values(self, shipping_option): assert shipping_option.id == self.id_ assert shipping_option.title == self.title assert shipping_option.prices == 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-13.11/tests/test_shippingquery.py000066400000000000000000000105201417656324400231140ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 Update, User, ShippingAddress, ShippingQuery, Bot from tests.conftest import check_shortcut_call, check_shortcut_signature, check_defaults_handling @pytest.fixture(scope='class') def shipping_query(bot): return ShippingQuery( TestShippingQuery.id_, TestShippingQuery.from_user, TestShippingQuery.invoice_payload, TestShippingQuery.shipping_address, bot=bot, ) class TestShippingQuery: id_ = 5 invoice_payload = 'invoice_payload' from_user = User(0, '', False) shipping_address = ShippingAddress('GB', '', 'London', '12 Grimmauld Place', '', 'WC1') def test_slot_behaviour(self, shipping_query, recwarn, mro_slots): inst = shipping_query for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self, bot): json_dict = { 'id': TestShippingQuery.id_, 'invoice_payload': TestShippingQuery.invoice_payload, 'from': TestShippingQuery.from_user.to_dict(), 'shipping_address': TestShippingQuery.shipping_address.to_dict(), } shipping_query = ShippingQuery.de_json(json_dict, bot) 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.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_answer(self, monkeypatch, shipping_query): 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 check_shortcut_call( shipping_query.answer, shipping_query.bot, 'answer_shipping_query' ) assert check_defaults_handling(shipping_query.answer, shipping_query.bot) monkeypatch.setattr(shipping_query.bot, 'answer_shipping_query', make_assertion) assert shipping_query.answer(ok=True) 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) python-telegram-bot-13.11/tests/test_shippingqueryhandler.py000066400000000000000000000150761417656324400244650ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram import ( Update, Chat, Bot, ChosenInlineResult, User, Message, CallbackQuery, InlineQuery, ShippingQuery, PreCheckoutQuery, ShippingAddress, ) from telegram.ext import ShippingQueryHandler, CallbackContext, JobQueue 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, recwarn, mro_slots): inst = ShippingQueryHandler(self.callback_basic) for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.callback = 'should give warning', self.callback_basic assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(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_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, Update) and isinstance(context.update_queue, 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_basic(self, dp, shiping_query): handler = ShippingQueryHandler(self.callback_basic) dp.add_handler(handler) assert handler.check_update(shiping_query) dp.process_update(shiping_query) assert self.test_flag def test_pass_user_or_chat_data(self, dp, shiping_query): handler = ShippingQueryHandler(self.callback_data_1, pass_user_data=True) dp.add_handler(handler) dp.process_update(shiping_query) assert self.test_flag dp.remove_handler(handler) handler = ShippingQueryHandler(self.callback_data_1, pass_chat_data=True) dp.add_handler(handler) self.test_flag = False dp.process_update(shiping_query) assert self.test_flag dp.remove_handler(handler) handler = ShippingQueryHandler( self.callback_data_2, pass_chat_data=True, pass_user_data=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(shiping_query) assert self.test_flag def test_pass_job_or_update_queue(self, dp, shiping_query): handler = ShippingQueryHandler(self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update(shiping_query) assert self.test_flag dp.remove_handler(handler) handler = ShippingQueryHandler(self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update(shiping_query) assert self.test_flag dp.remove_handler(handler) handler = ShippingQueryHandler( self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update(shiping_query) assert self.test_flag def test_other_update_types(self, false_update): handler = ShippingQueryHandler(self.callback_basic) assert not handler.check_update(false_update) def test_context(self, cdp, shiping_query): handler = ShippingQueryHandler(self.callback_context) cdp.add_handler(handler) cdp.process_update(shiping_query) assert self.test_flag python-telegram-bot-13.11/tests/test_slots.py000066400000000000000000000056531417656324400213640ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 importlib.util import os from glob import iglob import inspect excluded = { 'telegram.error', '_ConversationTimeoutContext', 'DispatcherHandlerStop', 'Days', 'telegram.deprecate', 'TelegramDecryptionError', 'ContextTypes', 'CallbackDataCache', 'InvalidCallbackData', '_KeyboardData', } # These modules/classes intentionally don't have __dict__. def test_class_has_slots_and_dict(mro_slots): tg_paths = [p for p in iglob("telegram/**/*.py", recursive=True) if 'vendor' not in p] for path in tg_paths: # windows uses backslashes: if os.name == 'nt': split_path = path.split('\\') else: split_path = path.split('/') mod_name = f"telegram{'.ext.' if split_path[1] == 'ext' else '.'}{split_path[-1][:-3]}" spec = importlib.util.spec_from_file_location(mod_name, path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # Exec 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 cls.__module__ in excluded or name in excluded: continue assert '__dict__' in get_slots(cls), f"class '{name}' in {path} doesn't have __dict__" def get_slots(_class): slots = [attr for cls in _class.__mro__ if hasattr(cls, '__slots__') for attr in cls.__slots__] # We're a bit hacky here to handle cases correctly, where we can't read the parents slots from # the mro if '__dict__' not in slots: try: class Subclass(_class): __slots__ = ('__dict__',) except TypeError as exc: if '__dict__ slot disallowed: we already got one' in str(exc): slots.append('__dict__') else: raise exc return slots python-telegram-bot-13.11/tests/test_sticker.py000066400000000000000000000663771417656324400216760ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 time import sleep import pytest from flaky import flaky from telegram import Sticker, PhotoSize, TelegramError, StickerSet, Audio, MaskPosition, Bot from telegram.error import BadRequest from tests.conftest import check_shortcut_call, check_shortcut_signature, check_defaults_handling @pytest.fixture(scope='function') def sticker_file(): f = open('tests/data/telegram.webp', 'rb') yield f f.close() @pytest.fixture(scope='class') def sticker(bot, chat_id): with open('tests/data/telegram.webp', 'rb') as f: return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker @pytest.fixture(scope='function') def animated_sticker_file(): f = open('tests/data/telegram_animated_sticker.tgs', 'rb') yield f f.close() @pytest.fixture(scope='class') def animated_sticker(bot, chat_id): with open('tests/data/telegram_animated_sticker.tgs', 'rb') as f: return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker @pytest.fixture(scope='function') def video_sticker_file(): with open('tests/data/telegram_video_sticker.webm', 'rb') as f: yield f @pytest.fixture(scope='class') def video_sticker(bot, chat_id): with open('tests/data/telegram_video_sticker.webm', 'rb') as f: return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker class TestSticker: # 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 = 21472 sticker_file_id = '5a3128a4d2a04750b5b58397f3b5e812' sticker_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' def test_slot_behaviour(self, sticker, mro_slots, recwarn): for attr in sticker.__slots__: assert getattr(sticker, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not sticker.__dict__, f"got missing slot(s): {sticker.__dict__}" assert len(mro_slots(sticker)) == len(set(mro_slots(sticker))), "duplicate slot" sticker.custom, sticker.emoji = 'should give warning', self.emoji assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb, PhotoSize) assert isinstance(sticker.thumb.file_id, str) assert isinstance(sticker.thumb.file_unique_id, str) assert sticker.thumb.file_id != '' assert sticker.thumb.file_unique_id != '' 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.thumb.width == self.thumb_width assert sticker.thumb.height == self.thumb_height assert sticker.thumb.file_size == self.thumb_file_size @flaky(3, 1) def test_send_all_args(self, bot, chat_id, sticker_file, sticker): message = 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 isinstance(message.sticker.thumb, PhotoSize) assert isinstance(message.sticker.thumb.file_id, str) assert isinstance(message.sticker.thumb.file_unique_id, str) assert message.sticker.thumb.file_id != '' assert message.sticker.thumb.file_unique_id != '' assert message.sticker.thumb.width == sticker.thumb.width assert message.sticker.thumb.height == sticker.thumb.height assert message.sticker.thumb.file_size == sticker.thumb.file_size assert message.has_protected_content @flaky(3, 1) def test_get_and_download(self, bot, sticker): new_file = bot.get_file(sticker.file_id) assert new_file.file_size == sticker.file_size assert new_file.file_id == sticker.file_id assert new_file.file_unique_id == sticker.file_unique_id assert new_file.file_path.startswith('https://') new_file.download('telegram.webp') assert os.path.isfile('telegram.webp') @flaky(3, 1) def test_resend(self, bot, chat_id, sticker): message = bot.send_sticker(chat_id=chat_id, sticker=sticker.file_id) assert message.sticker == sticker @flaky(3, 1) def test_send_on_server_emoji(self, bot, chat_id): server_file_id = 'CAADAQADHAADyIsGAAFZfq1bphjqlgI' message = bot.send_sticker(chat_id=chat_id, sticker=server_file_id) sticker = message.sticker assert sticker.emoji == self.emoji @flaky(3, 1) def test_send_from_url(self, bot, chat_id): message = 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 isinstance(message.sticker.thumb, PhotoSize) assert isinstance(message.sticker.thumb.file_id, str) assert isinstance(message.sticker.thumb.file_unique_id, str) assert message.sticker.thumb.file_id != '' assert message.sticker.thumb.file_unique_id != '' assert message.sticker.thumb.width == sticker.thumb.width assert message.sticker.thumb.height == sticker.thumb.height assert message.sticker.thumb.file_size == sticker.thumb.file_size 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, 'thumb': sticker.thumb.to_dict(), 'emoji': self.emoji, 'file_size': self.file_size, } json_sticker = Sticker.de_json(json_dict, bot) 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.thumb == sticker.thumb def test_send_with_sticker(self, monkeypatch, bot, chat_id, sticker): def test(url, data, **kwargs): return data['sticker'] == sticker.file_id monkeypatch.setattr(bot.request, 'post', test) message = bot.send_sticker(sticker=sticker, chat_id=chat_id) assert message def test_send_sticker_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('sticker') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.send_sticker(chat_id, file) assert test_flag monkeypatch.delattr(bot, '_post') @flaky(3, 1) @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'], ) def test_send_sticker_default_allow_sending_without_reply( self, default_bot, chat_id, sticker, custom ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_sticker( chat_id, sticker, reply_to_message_id=reply_to_message.message_id ) 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['thumb'] == sticker.thumb.to_dict() @flaky(3, 1) def test_error_send_empty_file(self, bot, chat_id): with pytest.raises(TelegramError): bot.send_sticker(chat_id, open(os.devnull, 'rb')) @flaky(3, 1) def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): bot.send_sticker(chat_id, '') def test_error_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): bot.send_sticker(chat_id) def test_equality(self, sticker): a = Sticker( sticker.file_id, sticker.file_unique_id, self.width, self.height, self.is_animated, self.is_video, ) b = Sticker( '', sticker.file_unique_id, self.width, self.height, self.is_animated, self.is_video ) c = Sticker(sticker.file_id, sticker.file_unique_id, 0, 0, False, True) d = Sticker('', '', self.width, self.height, self.is_animated, self.is_video) e = PhotoSize( sticker.file_id, sticker.file_unique_id, self.width, self.height, self.is_animated ) 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='function') def sticker_set(bot): ss = bot.get_sticker_set(f'test_by_{bot.username}') if len(ss.stickers) > 100: try: for i in range(1, 50): 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.') return ss @pytest.fixture(scope='function') def animated_sticker_set(bot): ss = bot.get_sticker_set(f'animated_test_by_{bot.username}') if len(ss.stickers) > 100: try: for i in range(1, 50): 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.') return ss @pytest.fixture(scope='function') def video_sticker_set(bot): ss = bot.get_sticker_set(f'video_test_by_{bot.username}') if len(ss.stickers) > 100: try: for i in range(1, 50): 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.') return ss @pytest.fixture(scope='function') def sticker_set_thumb_file(): f = open('tests/data/sticker_set_thumb.png', 'rb') yield f f.close() class TestStickerSet: title = 'Test stickers' is_animated = True is_video = True contains_masks = False stickers = [Sticker('file_id', 'file_un_id', 512, 512, True, True)] name = 'NOTAREALNAME' def test_de_json(self, bot, sticker): name = f'test_by_{bot.username}' json_dict = { 'name': name, 'title': self.title, 'is_animated': self.is_animated, 'is_video': self.is_video, 'contains_masks': self.contains_masks, 'stickers': [x.to_dict() for x in self.stickers], 'thumb': sticker.thumb.to_dict(), } sticker_set = StickerSet.de_json(json_dict, bot) assert sticker_set.name == name assert sticker_set.title == self.title assert sticker_set.is_animated == self.is_animated assert sticker_set.is_video == self.is_video assert sticker_set.contains_masks == self.contains_masks assert sticker_set.stickers == self.stickers assert sticker_set.thumb == sticker.thumb 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: bot.get_sticker_set(sticker_set) except BadRequest as e: if not e.message == "Stickerset_invalid": raise e if sticker_set.startswith(test_by): s = bot.create_new_sticker_set( chat_id, name=sticker_set, title="Sticker Test", png_sticker=sticker_file, emojis='😄', ) assert s elif sticker_set.startswith("animated"): a = bot.create_new_sticker_set( chat_id, name=sticker_set, title="Animated Test", tgs_sticker=animated_sticker_file, emojis='😄', ) assert a elif sticker_set.startswith("video"): v = bot.create_new_sticker_set( chat_id, name=sticker_set, title="Video Test", webm_sticker=video_sticker_file, emojis='🤔', ) assert v @flaky(3, 1) def test_bot_methods_1_png(self, bot, chat_id, sticker_file): with open('tests/data/telegram_sticker.png', 'rb') as f: # chat_id was hardcoded as 95205500 but it stopped working for some reason file = bot.upload_sticker_file(chat_id, f) assert file assert bot.add_sticker_to_set( chat_id, f'test_by_{bot.username}', png_sticker=file.file_id, emojis='😄' ) # Also test with file input and mask assert bot.add_sticker_to_set( chat_id, f'test_by_{bot.username}', png_sticker=sticker_file, emojis='😄', mask_position=MaskPosition(MaskPosition.EYES, -1, 1, 2), ) @flaky(3, 1) def test_bot_methods_1_tgs(self, bot, chat_id): assert bot.add_sticker_to_set( chat_id, f'animated_test_by_{bot.username}', tgs_sticker=open('tests/data/telegram_animated_sticker.tgs', 'rb'), emojis='😄', ) @flaky(3, 1) def test_bot_methods_1_webm(self, bot, chat_id): with open('tests/data/telegram_video_sticker.webm', 'rb') as f: assert bot.add_sticker_to_set( chat_id, f'video_test_by_{bot.username}', webm_sticker=f, emojis='🤔' ) 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['is_animated'] == sticker_set.is_animated assert sticker_set_dict['is_video'] == sticker_set.is_video assert sticker_set_dict['contains_masks'] == sticker_set.contains_masks assert sticker_set_dict['stickers'][0] == sticker_set.stickers[0].to_dict() @flaky(3, 1) def test_bot_methods_2_png(self, bot, sticker_set): file_id = sticker_set.stickers[0].file_id assert bot.set_sticker_position_in_set(file_id, 1) @flaky(3, 1) def test_bot_methods_2_tgs(self, bot, animated_sticker_set): file_id = animated_sticker_set.stickers[0].file_id assert bot.set_sticker_position_in_set(file_id, 1) @flaky(3, 1) def test_bot_methods_2_webm(self, bot, video_sticker_set): file_id = video_sticker_set.stickers[0].file_id assert bot.set_sticker_position_in_set(file_id, 1) @flaky(10, 1) def test_bot_methods_3_png(self, bot, chat_id, sticker_set_thumb_file): sleep(1) assert bot.set_sticker_set_thumb( f'test_by_{bot.username}', chat_id, sticker_set_thumb_file ) @flaky(10, 1) def test_bot_methods_3_tgs(self, bot, chat_id, animated_sticker_file, animated_sticker_set): sleep(1) animated_test = f'animated_test_by_{bot.username}' assert bot.set_sticker_set_thumb(animated_test, chat_id, animated_sticker_file) file_id = animated_sticker_set.stickers[-1].file_id # also test with file input and mask assert bot.set_sticker_set_thumb(animated_test, chat_id, file_id) # 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 @flaky(10, 1) def test_bot_methods_4_png(self, bot, sticker_set): sleep(1) file_id = sticker_set.stickers[-1].file_id assert bot.delete_sticker_from_set(file_id) @flaky(10, 1) def test_bot_methods_4_tgs(self, bot, animated_sticker_set): sleep(1) file_id = animated_sticker_set.stickers[-1].file_id assert bot.delete_sticker_from_set(file_id) @flaky(10, 1) def test_bot_methods_4_webm(self, bot, video_sticker_set): sleep(1) file_id = video_sticker_set.stickers[-1].file_id assert bot.delete_sticker_from_set(file_id) def test_upload_sticker_file_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('png_sticker') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.upload_sticker_file(chat_id, file) assert test_flag monkeypatch.delattr(bot, '_post') def test_create_new_sticker_set_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = ( data.get('png_sticker') == expected and data.get('tgs_sticker') == expected and data.get('webm_sticker') == expected ) monkeypatch.setattr(bot, '_post', make_assertion) bot.create_new_sticker_set( chat_id, 'name', 'title', 'emoji', png_sticker=file, tgs_sticker=file, webm_sticker=file, ) assert test_flag monkeypatch.delattr(bot, '_post') def test_add_sticker_to_set_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('png_sticker') == expected and data.get('tgs_sticker') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.add_sticker_to_set(chat_id, 'name', 'emoji', png_sticker=file, tgs_sticker=file) assert test_flag monkeypatch.delattr(bot, '_post') def test_set_sticker_set_thumb_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('thumb') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.set_sticker_set_thumb('name', chat_id, thumb=file) assert test_flag monkeypatch.delattr(bot, '_post') def test_get_file_instance_method(self, monkeypatch, sticker): def make_assertion(*_, **kwargs): return kwargs['file_id'] == sticker.file_id assert check_shortcut_signature(Sticker.get_file, Bot.get_file, ['file_id'], []) assert check_shortcut_call(sticker.get_file, sticker.bot, 'get_file') assert check_defaults_handling(sticker.get_file, sticker.bot) monkeypatch.setattr(sticker.bot, 'get_file', make_assertion) assert sticker.get_file() def test_equality(self): a = StickerSet( self.name, self.title, self.is_animated, self.contains_masks, self.stickers, self.is_video, ) b = StickerSet( self.name, self.title, self.is_animated, self.contains_masks, self.stickers, self.is_video, ) c = StickerSet(self.name, None, None, None, None, None) d = StickerSet( 'blah', self.title, self.is_animated, self.contains_masks, self.stickers, self.is_video ) 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.fixture(scope='class') def mask_position(): return MaskPosition( TestMaskPosition.point, TestMaskPosition.x_shift, TestMaskPosition.y_shift, TestMaskPosition.scale, ) class TestMaskPosition: point = MaskPosition.EYES x_shift = -1 y_shift = 1 scale = 2 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.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) python-telegram-bot-13.11/tests/test_stringcommandhandler.py000066400000000000000000000146551417656324400244250ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram import ( Bot, Update, Message, User, Chat, CallbackQuery, InlineQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, ) from telegram.ext import StringCommandHandler, CallbackContext, JobQueue 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, recwarn, mro_slots): inst = StringCommandHandler('sleepy', self.callback_basic) for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.callback = 'should give warning', self.callback_basic assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(bot, Bot) test_update = isinstance(update, str) self.test_flag = test_bot and test_update 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 sch_callback_args(self, bot, update, args): if update == '/test': self.test_flag = len(args) == 0 else: self.test_flag = args == ['one', 'two'] def callback_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, str) and isinstance(context.update_queue, 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) ) def callback_context_args(self, update, context): self.test_flag = context.args == ['one', 'two'] def test_basic(self, dp): handler = StringCommandHandler('test', self.callback_basic) dp.add_handler(handler) check = handler.check_update('/test') assert check is not None and check is not False dp.process_update('/test') assert self.test_flag check = handler.check_update('/nottest') assert check is None or check is False check = handler.check_update('not /test in front') assert check is None or check is False check = handler.check_update('/test followed by text') assert check is not None and check is not False def test_pass_args(self, dp): handler = StringCommandHandler('test', self.sch_callback_args, pass_args=True) dp.add_handler(handler) dp.process_update('/test') assert self.test_flag self.test_flag = False dp.process_update('/test one two') assert self.test_flag def test_pass_job_or_update_queue(self, dp): handler = StringCommandHandler('test', self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update('/test') assert self.test_flag dp.remove_handler(handler) handler = StringCommandHandler('test', self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update('/test') assert self.test_flag dp.remove_handler(handler) handler = StringCommandHandler( 'test', self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update('/test') assert self.test_flag def test_other_update_types(self, false_update): handler = StringCommandHandler('test', self.callback_basic) assert not handler.check_update(false_update) def test_context(self, cdp): handler = StringCommandHandler('test', self.callback_context) cdp.add_handler(handler) cdp.process_update('/test') assert self.test_flag def test_context_args(self, cdp): handler = StringCommandHandler('test', self.callback_context_args) cdp.add_handler(handler) cdp.process_update('/test') assert not self.test_flag cdp.process_update('/test one two') assert self.test_flag python-telegram-bot-13.11/tests/test_stringregexhandler.py000066400000000000000000000153111417656324400241070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 queue import Queue import pytest from telegram import ( Bot, Update, Message, User, Chat, CallbackQuery, InlineQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, ) from telegram.ext import StringRegexHandler, CallbackContext, JobQueue 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, mro_slots, recwarn): inst = StringRegexHandler('pfft', self.callback_basic) for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.callback = 'should give warning', self.callback_basic assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(bot, Bot) test_update = isinstance(update, str) self.test_flag = test_bot and test_update 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', ' message') if groupdict is not None: self.test_flag = groupdict == {'begin': 't', 'end': ' message'} def callback_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, str) and isinstance(context.update_queue, Queue) and isinstance(context.job_queue, JobQueue) ) def callback_context_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'} def test_basic(self, dp): handler = StringRegexHandler('(?P.*)est(?P.*)', self.callback_basic) dp.add_handler(handler) assert handler.check_update('test message') dp.process_update('test message') assert self.test_flag assert not handler.check_update('does not match') def test_with_passing_group_dict(self, dp): handler = StringRegexHandler( '(?P.*)est(?P.*)', self.callback_group, pass_groups=True ) dp.add_handler(handler) dp.process_update('test message') assert self.test_flag dp.remove_handler(handler) handler = StringRegexHandler( '(?P.*)est(?P.*)', self.callback_group, pass_groupdict=True ) dp.add_handler(handler) self.test_flag = False dp.process_update('test message') assert self.test_flag def test_pass_job_or_update_queue(self, dp): handler = StringRegexHandler('test', self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update('test') assert self.test_flag dp.remove_handler(handler) handler = StringRegexHandler('test', self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update('test') assert self.test_flag dp.remove_handler(handler) handler = StringRegexHandler( 'test', self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update('test') assert self.test_flag def test_other_update_types(self, false_update): handler = StringRegexHandler('test', self.callback_basic) assert not handler.check_update(false_update) def test_context(self, cdp): handler = StringRegexHandler(r'(t)est(.*)', self.callback_context) cdp.add_handler(handler) cdp.process_update('test message') assert self.test_flag def test_context_pattern(self, cdp): handler = StringRegexHandler(r'(t)est(.*)', self.callback_context_pattern) cdp.add_handler(handler) cdp.process_update('test message') assert self.test_flag cdp.remove_handler(handler) handler = StringRegexHandler(r'(t)est(.*)', self.callback_context_pattern) cdp.add_handler(handler) cdp.process_update('test message') assert self.test_flag python-telegram-bot-13.11/tests/test_successfulpayment.py000066400000000000000000000117321417656324400237700ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 @pytest.fixture(scope='class') def successful_payment(): return SuccessfulPayment( TestSuccessfulPayment.currency, TestSuccessfulPayment.total_amount, TestSuccessfulPayment.invoice_payload, TestSuccessfulPayment.telegram_payment_charge_id, TestSuccessfulPayment.provider_payment_charge_id, shipping_option_id=TestSuccessfulPayment.shipping_option_id, order_info=TestSuccessfulPayment.order_info, ) class TestSuccessfulPayment: 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' def test_slot_behaviour(self, successful_payment, recwarn, mro_slots): inst = successful_payment for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.currency = 'should give warning', self.currency assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.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-13.11/tests/test_telegramobject.py000066400000000000000000000110211417656324400231710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 as json_lib import pytest try: import ujson except ImportError: ujson = None from telegram import TelegramObject class TestTelegramObject: def test_to_json_native(self, monkeypatch): if ujson: monkeypatch.setattr('ujson.dumps', json_lib.dumps) # to_json simply takes whatever comes from to_dict, therefore we only need to test it once telegram_object = TelegramObject() # Test that it works with a dict with str keys as well as dicts as lists as values d = {'str': 'str', 'str2': ['str', 'str'], 'str3': {'str': 'str'}} monkeypatch.setattr('telegram.TelegramObject.to_dict', lambda _: d) json = telegram_object.to_json() # Order isn't guarantied assert '"str": "str"' in json assert '"str2": ["str", "str"]' in json assert '"str3": {"str": "str"}' 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): telegram_object.to_json() @pytest.mark.skipif(not ujson, reason='ujson not installed') def test_to_json_ujson(self, monkeypatch): # to_json simply takes whatever comes from to_dict, therefore we only need to test it once telegram_object = TelegramObject() # Test that it works with a dict with str keys as well as dicts as lists as values d = {'str': 'str', 'str2': ['str', 'str'], 'str3': {'str': 'str'}} monkeypatch.setattr('telegram.TelegramObject.to_dict', lambda _: d) json = telegram_object.to_json() # Order isn't guarantied and ujon discards whitespace assert '"str":"str"' in json assert '"str2":["str","str"]' in json assert '"str3":{"str":"str"}' in json # Test that ujson allows tuples # NOTE: This could be seen as a bug (since it's differnt from the normal "json", # but we test it anyways d = {('str', 'str'): 'str'} monkeypatch.setattr('telegram.TelegramObject.to_dict', lambda _: d) telegram_object.to_json() def test_to_dict_private_attribute(self): class TelegramObjectSubclass(TelegramObject): __slots__ = ('a', '_b') # Added slots so that the attrs are converted to dict def __init__(self): self.a = 1 self._b = 2 subclass_instance = TelegramObjectSubclass() assert subclass_instance.to_dict() == {'a': 1} def test_slot_behaviour(self, recwarn, mro_slots): inst = TelegramObject() for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom = 'should give warning' assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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) == 2 assert str(recwarn[0].message) == expected_warning assert str(recwarn[1].message) == expected_warning def test_meaningful_comparison(self, recwarn): class TGO(TelegramObject): _id_attrs = (1,) a = TGO() b = TGO() assert a == b assert len(recwarn) == 0 assert b == a assert len(recwarn) == 0 python-telegram-bot-13.11/tests/test_typehandler.py000066400000000000000000000101101417656324400225170ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 OrderedDict from queue import Queue import pytest from telegram import Bot from telegram.ext import TypeHandler, CallbackContext, JobQueue class TestTypeHandler: test_flag = False def test_slot_behaviour(self, mro_slots, recwarn): inst = TypeHandler(dict, self.callback_basic) for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.callback = 'should give warning', self.callback_basic assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.fixture(autouse=True) def reset(self): self.test_flag = False def callback_basic(self, bot, update): test_bot = isinstance(bot, Bot) test_update = isinstance(update, dict) self.test_flag = test_bot and test_update 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_context(self, update, context): self.test_flag = ( isinstance(context, CallbackContext) and isinstance(context.bot, Bot) and isinstance(update, dict) and isinstance(context.update_queue, 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) ) def test_basic(self, dp): handler = TypeHandler(dict, self.callback_basic) dp.add_handler(handler) assert handler.check_update({'a': 1, 'b': 2}) assert not handler.check_update('not a dict') dp.process_update({'a': 1, 'b': 2}) assert self.test_flag def test_strict(self): handler = TypeHandler(dict, self.callback_basic, strict=True) o = OrderedDict({'a': 1, 'b': 2}) assert handler.check_update({'a': 1, 'b': 2}) assert not handler.check_update(o) def test_pass_job_or_update_queue(self, dp): handler = TypeHandler(dict, self.callback_queue_1, pass_job_queue=True) dp.add_handler(handler) dp.process_update({'a': 1, 'b': 2}) assert self.test_flag dp.remove_handler(handler) handler = TypeHandler(dict, self.callback_queue_1, pass_update_queue=True) dp.add_handler(handler) self.test_flag = False dp.process_update({'a': 1, 'b': 2}) assert self.test_flag dp.remove_handler(handler) handler = TypeHandler( dict, self.callback_queue_2, pass_job_queue=True, pass_update_queue=True ) dp.add_handler(handler) self.test_flag = False dp.process_update({'a': 1, 'b': 2}) assert self.test_flag def test_context(self, cdp): handler = TypeHandler(dict, self.callback_context) cdp.add_handler(handler) cdp.process_update({'a': 1, 'b': 2}) assert self.test_flag python-telegram-bot-13.11/tests/test_update.py000066400000000000000000000160471417656324400215010ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ( Message, User, Update, Chat, CallbackQuery, InlineQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, Poll, PollOption, ChatMemberUpdated, ChatMember, ChatJoinRequest, ) from telegram.poll import PollAnswer from telegram.utils.helpers import from_timestamp message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') chat_member_updated = ChatMemberUpdated( Chat(1, 'chat'), User(1, '', False), from_timestamp(int(time.time())), ChatMember(User(1, '', False), ChatMember.CREATOR), ChatMember(User(1, '', False), ChatMember.CREATOR), ) chat_join_request = ChatJoinRequest( chat=Chat(1, Chat.SUPERGROUP), from_user=User(1, 'first_name', False), date=from_timestamp(int(time.time())), bio='bio', ) 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, '')}, {'poll': Poll('id', '?', [PollOption('.', 1)], False, False, False, Poll.REGULAR, True)}, {'poll_answer': PollAnswer("id", User(1, '', False), [1])}, {'my_chat_member': chat_member_updated}, {'chat_member': chat_member_updated}, {'chat_join_request': chat_join_request}, # 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', ) ids = all_types + ('callback_query_without_message',) @pytest.fixture(params=params, ids=ids) def update(request): return Update(update_id=TestUpdate.update_id, **request.param) class TestUpdate: update_id = 868573637 def test_slot_behaviour(self, update, recwarn, mro_slots): for attr in update.__slots__: assert getattr(update, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not update.__dict__, f"got missing slot(s): {update.__dict__}" assert len(mro_slots(update)) == len(set(mro_slots(update))), "duplicate slot" update.custom, update.update_id = 'should give warning', self.update_id assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list @pytest.mark.parametrize('paramdict', argvalues=params, ids=ids) def test_de_json(self, bot, paramdict): json_dict = {'update_id': TestUpdate.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.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_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 ): 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 ): assert user.id == 1 else: assert user is None 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 ): assert eff_message.message_id == message.message_id else: assert eff_message is None 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) python-telegram-bot-13.11/tests/test_updater.py000066400000000000000000000656111417656324400216640ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 os import signal import sys import threading from contextlib import contextmanager from flaky import flaky from functools import partial from queue import Queue from random import randrange from threading import Thread, Event from time import sleep from urllib.request import Request, urlopen from urllib.error import HTTPError import pytest from telegram import ( TelegramError, Message, User, Chat, Update, Bot, InlineKeyboardMarkup, InlineKeyboardButton, ) from telegram.error import Unauthorized, InvalidToken, TimedOut, RetryAfter from telegram.ext import ( Updater, Dispatcher, DictPersistence, Defaults, InvalidCallbackData, ExtBot, ) from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.ext.utils.webhookhandler import WebhookServer signalskip = pytest.mark.skipif( sys.platform == 'win32', reason="Can't send signals without stopping whole process on windows", ) ASYNCIO_LOCK = threading.Lock() @contextmanager def set_asyncio_event_loop(loop): with ASYNCIO_LOCK: try: orig_lop = asyncio.get_event_loop() except RuntimeError: orig_lop = None asyncio.set_event_loop(loop) try: yield finally: asyncio.set_event_loop(orig_lop) class TestUpdater: message_count = 0 received = None attempts = 0 err_handler_called = Event() cb_handler_called = Event() offset = 0 test_flag = False def test_slot_behaviour(self, updater, mro_slots, recwarn): for at in updater.__slots__: at = f"_Updater{at}" if at.startswith('__') and not at.endswith('__') else at assert getattr(updater, at, 'err') != 'err', f"got extra slot '{at}'" assert not updater.__dict__, f"got missing slot(s): {updater.__dict__}" assert len(mro_slots(updater)) == len(set(mro_slots(updater))), "duplicate slot" updater.custom, updater.running = 'should give warning', updater.running assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list class CustomUpdater(Updater): pass # Tests that setting custom attributes of Updater subclass doesn't raise warning a = CustomUpdater(updater.bot.token) a.my_custom = 'no error!' assert len(recwarn) == 1 updater.__setattr__('__test', 'mangled success') assert getattr(updater, '_Updater__test', 'e') == 'mangled success', "mangling failed" @pytest.fixture(autouse=True) def reset(self): self.message_count = 0 self.received = None self.attempts = 0 self.err_handler_called.clear() self.cb_handler_called.clear() self.test_flag = False def error_handler(self, bot, update, error): self.received = error.message self.err_handler_called.set() def callback(self, bot, update): self.received = update.message.text self.cb_handler_called.set() def test_warn_arbitrary_callback_data(self, bot, recwarn): Updater(bot=bot, arbitrary_callback_data=True) assert len(recwarn) == 1 assert 'Passing arbitrary_callback_data to an Updater' in str(recwarn[0].message) @pytest.mark.parametrize( ('error',), argvalues=[(TelegramError('Test Error 2'),), (Unauthorized('Test Unauthorized'),)], ids=('TelegramError', 'Unauthorized'), ) def test_get_updates_normal_err(self, monkeypatch, updater, error): def test(*args, **kwargs): raise error monkeypatch.setattr(updater.bot, 'get_updates', test) monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) updater.dispatcher.add_error_handler(self.error_handler) updater.start_polling(0.01) # Make sure that the error handler was called self.err_handler_called.wait() assert self.received == error.message # Make sure that Updater polling thread keeps running self.err_handler_called.clear() self.err_handler_called.wait() @pytest.mark.filterwarnings('ignore:.*:pytest.PytestUnhandledThreadExceptionWarning') def test_get_updates_bailout_err(self, monkeypatch, updater, caplog): error = InvalidToken() def test(*args, **kwargs): raise error with caplog.at_level(logging.DEBUG): monkeypatch.setattr(updater.bot, 'get_updates', test) monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) updater.dispatcher.add_error_handler(self.error_handler) updater.start_polling(0.01) assert self.err_handler_called.wait(1) is not True sleep(1) # NOTE: This test might hit a race condition and fail (though the 1 seconds delay above # should work around it). # NOTE: Checking Updater.running is problematic because it is not set to False when there's # an unhandled exception. # TODO: We should have a way to poll Updater status and decide if it's running or not. import pprint pprint.pprint([rec.getMessage() for rec in caplog.get_records('call')]) assert any( f'unhandled exception in Bot:{updater.bot.id}:updater' in rec.getMessage() for rec in caplog.get_records('call') ) @pytest.mark.parametrize( ('error',), argvalues=[(RetryAfter(0.01),), (TimedOut(),)], ids=('RetryAfter', 'TimedOut') ) def test_get_updates_retries(self, monkeypatch, updater, error): event = Event() def test(*args, **kwargs): event.set() raise error monkeypatch.setattr(updater.bot, 'get_updates', test) monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) updater.dispatcher.add_error_handler(self.error_handler) updater.start_polling(0.01) # Make sure that get_updates was called, but not the error handler event.wait() assert self.err_handler_called.wait(0.5) is not True assert self.received != error.message # Make sure that Updater polling thread keeps running event.clear() event.wait() assert self.err_handler_called.wait(0.5) is not True @pytest.mark.parametrize('ext_bot', [True, False]) def test_webhook(self, monkeypatch, updater, ext_bot): # 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 not type(updater.bot) is Bot: updater.bot = Bot(updater.bot.token) q = Queue() monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True) monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u)) ip = '127.0.0.1' port = randrange(1024, 49152) # Select random port updater.start_webhook(ip, port, url_path='TOKEN') sleep(0.2) try: # Now, we send an update to the server via urlopen update = Update( 1, message=Message( 1, None, Chat(1, ''), from_user=User(1, '', False), text='Webhook' ), ) self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN') sleep(0.2) assert q.get(False) == update # Returns 404 if path is incorrect with pytest.raises(HTTPError) as excinfo: self._send_webhook_msg(ip, port, None, 'webookhandler.py') assert excinfo.value.code == 404 with pytest.raises(HTTPError) as excinfo: self._send_webhook_msg( ip, port, None, 'webookhandler.py', get_method=lambda: 'HEAD' ) assert excinfo.value.code == 404 # Test multiple shutdown() calls updater.httpd.shutdown() finally: updater.httpd.shutdown() sleep(0.2) assert not updater.httpd.is_running updater.stop() @pytest.mark.parametrize('invalid_data', [True, False]) def test_webhook_arbitrary_callback_data(self, monkeypatch, updater, invalid_data): """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.bot.arbitrary_callback_data = True try: q = Queue() monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True) monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u)) ip = '127.0.0.1' port = randrange(1024, 49152) # Select random port updater.start_webhook(ip, port, url_path='TOKEN') sleep(0.2) try: # Now, we send an update to the server via urlopen 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) message = Message( 1, None, None, reply_markup=reply_markup, ) update = Update(1, message=message) self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN') sleep(0.2) received_update = q.get(False) assert received_update == update 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' # Test multiple shutdown() calls updater.httpd.shutdown() finally: updater.httpd.shutdown() sleep(0.2) assert not updater.httpd.is_running updater.stop() finally: updater.bot.arbitrary_callback_data = False updater.bot.callback_data_cache.clear_callback_data() updater.bot.callback_data_cache.clear_callback_queries() def test_start_webhook_no_warning_or_error_logs(self, caplog, updater, monkeypatch): monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True) # prevent api calls from @info decorator when updater.bot.id is used in thread names monkeypatch.setattr(updater.bot, '_bot', User(id=123, first_name='bot', is_bot=True)) monkeypatch.setattr(updater.bot, '_commands', []) ip = '127.0.0.1' port = randrange(1024, 49152) # Select random port with caplog.at_level(logging.WARNING): updater.start_webhook(ip, port) updater.stop() assert not caplog.records def test_webhook_ssl(self, monkeypatch, updater): monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True) ip = '127.0.0.1' port = randrange(1024, 49152) # Select random port tg_err = False try: updater._start_webhook( ip, port, url_path='TOKEN', cert='./tests/test_updater.py', key='./tests/test_updater.py', bootstrap_retries=0, drop_pending_updates=False, webhook_url=None, allowed_updates=None, ) except TelegramError: tg_err = True assert tg_err def test_webhook_no_ssl(self, monkeypatch, updater): q = Queue() monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True) monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u)) ip = '127.0.0.1' port = randrange(1024, 49152) # Select random port updater.start_webhook(ip, port, webhook_url=None) sleep(0.2) # Now, we send an update to the server via urlopen update = Update( 1, message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Webhook 2'), ) self._send_webhook_msg(ip, port, update.to_json()) sleep(0.2) assert q.get(False) == update updater.stop() def test_webhook_ssl_just_for_telegram(self, monkeypatch, updater): q = Queue() def set_webhook(**kwargs): self.test_flag.append(bool(kwargs.get('certificate'))) return True orig_wh_server_init = WebhookServer.__init__ def webhook_server_init(*args): self.test_flag = [args[-1] is None] orig_wh_server_init(*args) monkeypatch.setattr(updater.bot, 'set_webhook', set_webhook) monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True) monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u)) monkeypatch.setattr( 'telegram.ext.utils.webhookhandler.WebhookServer.__init__', webhook_server_init ) ip = '127.0.0.1' port = randrange(1024, 49152) # Select random port updater.start_webhook(ip, port, webhook_url=None, cert='./tests/test_updater.py') sleep(0.2) # Now, we send an update to the server via urlopen update = Update( 1, message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Webhook 2'), ) self._send_webhook_msg(ip, port, update.to_json()) sleep(0.2) assert q.get(False) == update updater.stop() assert self.test_flag == [True, True] @pytest.mark.parametrize('pass_max_connections', [True, False]) def test_webhook_max_connections(self, monkeypatch, updater, pass_max_connections): q = Queue() max_connections = 42 def set_webhook(**kwargs): print(kwargs) self.test_flag = kwargs.get('max_connections') == ( max_connections if pass_max_connections else 40 ) return True monkeypatch.setattr(updater.bot, 'set_webhook', set_webhook) monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True) monkeypatch.setattr('telegram.ext.Dispatcher.process_update', lambda _, u: q.put(u)) ip = '127.0.0.1' port = randrange(1024, 49152) # Select random port if pass_max_connections: updater.start_webhook(ip, port, webhook_url=None, max_connections=max_connections) else: updater.start_webhook(ip, port, webhook_url=None) sleep(0.2) # Now, we send an update to the server via urlopen update = Update( 1, message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Webhook 2'), ) self._send_webhook_msg(ip, port, update.to_json()) sleep(0.2) assert q.get(False) == update updater.stop() assert self.test_flag is True @pytest.mark.parametrize(('error',), argvalues=[(TelegramError(''),)], ids=('TelegramError',)) def test_bootstrap_retries_success(self, monkeypatch, updater, error): retries = 2 def attempt(*args, **kwargs): if self.attempts < retries: self.attempts += 1 raise error monkeypatch.setattr(updater.bot, 'set_webhook', attempt) updater.running = True updater._bootstrap(retries, False, 'path', None, bootstrap_interval=0) assert self.attempts == retries @pytest.mark.parametrize( ('error', 'attempts'), argvalues=[(TelegramError(''), 2), (Unauthorized(''), 1), (InvalidToken(), 1)], ids=('TelegramError', 'Unauthorized', 'InvalidToken'), ) def test_bootstrap_retries_error(self, monkeypatch, updater, error, attempts): retries = 1 def attempt(*args, **kwargs): self.attempts += 1 raise error monkeypatch.setattr(updater.bot, 'set_webhook', attempt) updater.running = True with pytest.raises(type(error)): updater._bootstrap(retries, False, 'path', None, bootstrap_interval=0) assert self.attempts == attempts @pytest.mark.parametrize('drop_pending_updates', (True, False)) def test_bootstrap_clean_updates(self, monkeypatch, updater, drop_pending_updates): # As dropping pending updates is done by passing `drop_pending_updates` to # set_webhook, we just check that we pass the correct value self.test_flag = False def delete_webhook(**kwargs): self.test_flag = kwargs.get('drop_pending_updates') == drop_pending_updates monkeypatch.setattr(updater.bot, 'delete_webhook', delete_webhook) updater.running = True updater._bootstrap( 1, drop_pending_updates=drop_pending_updates, webhook_url=None, allowed_updates=None, bootstrap_interval=0, ) assert self.test_flag is True def test_deprecation_warnings_start_webhook(self, recwarn, updater, monkeypatch): monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True) # prevent api calls from @info decorator when updater.bot.id is used in thread names monkeypatch.setattr(updater.bot, '_bot', User(id=123, first_name='bot', is_bot=True)) monkeypatch.setattr(updater.bot, '_commands', []) ip = '127.0.0.1' port = randrange(1024, 49152) # Select random port updater.start_webhook(ip, port, clean=True, force_event_loop=False) updater.stop() for warning in recwarn: print(warning) try: # This is for flaky tests (there's an unclosed socket sometimes) recwarn.pop(ResourceWarning) # internally iterates through recwarn.list and deletes it except AssertionError: pass assert len(recwarn) == 3 assert str(recwarn[0].message).startswith('Old Handler API') assert str(recwarn[1].message).startswith('The argument `clean` of') assert str(recwarn[2].message).startswith('The argument `force_event_loop` of') def test_clean_deprecation_warning_polling(self, recwarn, updater, monkeypatch): monkeypatch.setattr(updater.bot, 'set_webhook', lambda *args, **kwargs: True) monkeypatch.setattr(updater.bot, 'delete_webhook', lambda *args, **kwargs: True) # prevent api calls from @info decorator when updater.bot.id is used in thread names monkeypatch.setattr(updater.bot, '_bot', User(id=123, first_name='bot', is_bot=True)) monkeypatch.setattr(updater.bot, '_commands', []) updater.start_polling(clean=True) updater.stop() for msg in recwarn: print(msg) try: # This is for flaky tests (there's an unclosed socket sometimes) recwarn.pop(ResourceWarning) # internally iterates through recwarn.list and deletes it except AssertionError: pass assert len(recwarn) == 2 assert str(recwarn[0].message).startswith('Old Handler API') assert str(recwarn[1].message).startswith('The argument `clean` of') def test_clean_drop_pending_mutually_exclusive(self, updater): with pytest.raises(TypeError, match='`clean` and `drop_pending_updates` are mutually'): updater.start_polling(clean=True, drop_pending_updates=False) with pytest.raises(TypeError, match='`clean` and `drop_pending_updates` are mutually'): updater.start_webhook(clean=True, drop_pending_updates=False) @flaky(3, 1) def test_webhook_invalid_posts(self, updater): ip = '127.0.0.1' port = randrange(1024, 49152) # select random port for travis thr = Thread( target=updater._start_webhook, args=(ip, port, '', None, None, 0, False, None, None) ) thr.start() sleep(0.2) try: with pytest.raises(HTTPError) as excinfo: self._send_webhook_msg( ip, port, 'data', content_type='application/xml' ) assert excinfo.value.code == 403 with pytest.raises(HTTPError) as excinfo: self._send_webhook_msg(ip, port, 'dummy-payload', content_len=-2) assert excinfo.value.code == 500 # TODO: prevent urllib or the underlying from adding content-length # with pytest.raises(HTTPError) as excinfo: # self._send_webhook_msg(ip, port, 'dummy-payload', content_len=None) # assert excinfo.value.code == 411 with pytest.raises(HTTPError): self._send_webhook_msg(ip, port, 'dummy-payload', content_len='not-a-number') assert excinfo.value.code == 500 finally: updater.httpd.shutdown() thr.join() def _send_webhook_msg( self, ip, port, payload_str, url_path='', content_len=-1, content_type='application/json', get_method=None, ): headers = { 'content-type': content_type, } 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}' req = Request(url, data=payload, headers=headers) if get_method is not None: req.get_method = get_method return urlopen(req) def signal_sender(self, updater): sleep(0.2) while not updater.running: sleep(0.2) os.kill(os.getpid(), signal.SIGTERM) @signalskip def test_idle(self, updater, caplog): updater.start_polling(0.01) Thread(target=partial(self.signal_sender, updater=updater)).start() with caplog.at_level(logging.INFO): updater.idle() # There is a chance of a conflict when getting updates since there can be many tests # (bots) running simultaneously while testing in github actions. records = caplog.records.copy() # To avoid iterating and removing at same time for idx, log in enumerate(records): print(log) msg = log.getMessage() if msg.startswith('Error while getting Updates: Conflict'): caplog.records.pop(idx) # For stability if msg.startswith('No error handlers are registered'): caplog.records.pop(idx) assert len(caplog.records) == 2, caplog.records rec = caplog.records[-2] assert rec.getMessage().startswith(f'Received signal {signal.SIGTERM}') assert rec.levelname == 'INFO' rec = caplog.records[-1] assert rec.getMessage().startswith('Scheduler has been shut down') assert rec.levelname == 'INFO' # If we get this far, idle() ran through sleep(0.5) assert updater.running is False @signalskip def test_user_signal(self, updater): temp_var = {'a': 0} def user_signal_inc(signum, frame): temp_var['a'] = 1 updater.user_sig_handler = user_signal_inc updater.start_polling(0.01) Thread(target=partial(self.signal_sender, updater=updater)).start() updater.idle() # If we get this far, idle() ran through sleep(0.5) assert updater.running is False assert temp_var['a'] != 0 def test_create_bot(self): updater = Updater('123:abcd') assert updater.bot is not None def test_mutual_exclude_token_bot(self): bot = Bot('123:zyxw') with pytest.raises(ValueError): Updater(token='123:abcd', bot=bot) def test_no_token_or_bot_or_dispatcher(self): with pytest.raises(ValueError): Updater() def test_mutual_exclude_bot_private_key(self): bot = Bot('123:zyxw') with pytest.raises(ValueError): Updater(bot=bot, private_key=b'key') def test_mutual_exclude_bot_dispatcher(self, bot): dispatcher = Dispatcher(bot, None) bot = Bot('123:zyxw') with pytest.raises(ValueError): Updater(bot=bot, dispatcher=dispatcher) def test_mutual_exclude_persistence_dispatcher(self, bot): dispatcher = Dispatcher(bot, None) persistence = DictPersistence() with pytest.raises(ValueError): Updater(dispatcher=dispatcher, persistence=persistence) def test_mutual_exclude_workers_dispatcher(self, bot): dispatcher = Dispatcher(bot, None) with pytest.raises(ValueError): Updater(dispatcher=dispatcher, workers=8) def test_mutual_exclude_use_context_dispatcher(self, bot): dispatcher = Dispatcher(bot, None) use_context = not dispatcher.use_context with pytest.raises(ValueError): Updater(dispatcher=dispatcher, use_context=use_context) def test_mutual_exclude_custom_context_dispatcher(self): dispatcher = Dispatcher(None, None) with pytest.raises(ValueError): Updater(dispatcher=dispatcher, context_types=True) def test_defaults_warning(self, bot): with pytest.warns(TelegramDeprecationWarning, match='no effect when a Bot is passed'): Updater(bot=bot, defaults=Defaults()) python-telegram-bot-13.11/tests/test_user.py000066400000000000000000000557451417656324400212050ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 Update, User, Bot, InlineKeyboardButton from telegram.utils.helpers import escape_markdown from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling @pytest.fixture(scope='function') def json_dict(): return { 'id': TestUser.id_, 'is_bot': TestUser.is_bot, 'first_name': TestUser.first_name, 'last_name': TestUser.last_name, 'username': TestUser.username, 'language_code': TestUser.language_code, 'can_join_groups': TestUser.can_join_groups, 'can_read_all_group_messages': TestUser.can_read_all_group_messages, 'supports_inline_queries': TestUser.supports_inline_queries, } @pytest.fixture(scope='function') def user(bot): return User( id=TestUser.id_, first_name=TestUser.first_name, is_bot=TestUser.is_bot, last_name=TestUser.last_name, username=TestUser.username, language_code=TestUser.language_code, can_join_groups=TestUser.can_join_groups, can_read_all_group_messages=TestUser.can_read_all_group_messages, supports_inline_queries=TestUser.supports_inline_queries, bot=bot, ) class TestUser: 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 def test_slot_behaviour(self, user, mro_slots, recwarn): for attr in user.__slots__: assert getattr(user, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not user.__dict__, f"got missing slot(s): {user.__dict__}" assert len(mro_slots(user)) == len(set(mro_slots(user))), "duplicate slot" user.custom, user.id = 'should give warning', self.id_ assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self, json_dict, bot): user = User.de_json(json_dict, bot) 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 def test_de_json_without_username(self, json_dict, bot): del json_dict['username'] user = User.de_json(json_dict, bot) 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 is None 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 def test_de_json_without_username_and_last_name(self, json_dict, bot): del json_dict['username'] del json_dict['last_name'] user = User.de_json(json_dict, bot) assert user.id == self.id_ assert user.is_bot == self.is_bot assert user.first_name == self.first_name assert user.last_name is None assert user.username is None 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 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 def test_instance_method_get_profile_photos(self, monkeypatch, user): 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 check_shortcut_call(user.get_profile_photos, user.bot, 'get_user_profile_photos') assert check_defaults_handling(user.get_profile_photos, user.bot) monkeypatch.setattr(user.bot, 'get_user_profile_photos', make_assertion) assert user.get_profile_photos() def test_instance_method_pin_message(self, monkeypatch, user): def make_assertion(*_, **kwargs): return kwargs['chat_id'] == user.id assert check_shortcut_signature(User.pin_message, Bot.pin_chat_message, ['chat_id'], []) assert check_shortcut_call(user.pin_message, user.bot, 'pin_chat_message') assert check_defaults_handling(user.pin_message, user.bot) monkeypatch.setattr(user.bot, 'pin_chat_message', make_assertion) assert user.pin_message(1) def test_instance_method_unpin_message(self, monkeypatch, user): def make_assertion(*_, **kwargs): return kwargs['chat_id'] == user.id assert check_shortcut_signature( User.unpin_message, Bot.unpin_chat_message, ['chat_id'], [] ) assert check_shortcut_call(user.unpin_message, user.bot, 'unpin_chat_message') assert check_defaults_handling(user.unpin_message, user.bot) monkeypatch.setattr(user.bot, 'unpin_chat_message', make_assertion) assert user.unpin_message() def test_instance_method_unpin_all_messages(self, monkeypatch, user): 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 check_shortcut_call(user.unpin_all_messages, user.bot, 'unpin_all_chat_messages') assert check_defaults_handling(user.unpin_all_messages, user.bot) monkeypatch.setattr(user.bot, 'unpin_all_chat_messages', make_assertion) assert user.unpin_all_messages() def test_instance_method_send_message(self, monkeypatch, user): 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 check_shortcut_call(user.send_message, user.bot, 'send_message') assert check_defaults_handling(user.send_message, user.bot) monkeypatch.setattr(user.bot, 'send_message', make_assertion) assert user.send_message('test') def test_instance_method_send_photo(self, monkeypatch, user): 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 check_shortcut_call(user.send_photo, user.bot, 'send_photo') assert check_defaults_handling(user.send_photo, user.bot) monkeypatch.setattr(user.bot, 'send_photo', make_assertion) assert user.send_photo('test_photo') def test_instance_method_send_media_group(self, monkeypatch, user): 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 check_shortcut_call(user.send_media_group, user.bot, 'send_media_group') assert check_defaults_handling(user.send_media_group, user.bot) monkeypatch.setattr(user.bot, 'send_media_group', make_assertion) assert user.send_media_group('test_media_group') def test_instance_method_send_audio(self, monkeypatch, user): 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 check_shortcut_call(user.send_audio, user.bot, 'send_audio') assert check_defaults_handling(user.send_audio, user.bot) monkeypatch.setattr(user.bot, 'send_audio', make_assertion) assert user.send_audio('test_audio') def test_instance_method_send_chat_action(self, monkeypatch, user): 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 check_shortcut_call(user.send_chat_action, user.bot, 'send_chat_action') assert check_defaults_handling(user.send_chat_action, user.bot) monkeypatch.setattr(user.bot, 'send_chat_action', make_assertion) assert user.send_chat_action('test_chat_action') def test_instance_method_send_contact(self, monkeypatch, user): 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 check_shortcut_call(user.send_contact, user.bot, 'send_contact') assert check_defaults_handling(user.send_contact, user.bot) monkeypatch.setattr(user.bot, 'send_contact', make_assertion) assert user.send_contact(phone_number='test_contact') def test_instance_method_send_dice(self, monkeypatch, user): 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 check_shortcut_call(user.send_dice, user.bot, 'send_dice') assert check_defaults_handling(user.send_dice, user.bot) monkeypatch.setattr(user.bot, 'send_dice', make_assertion) assert user.send_dice(emoji='test_dice') def test_instance_method_send_document(self, monkeypatch, user): 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 check_shortcut_call(user.send_document, user.bot, 'send_document') assert check_defaults_handling(user.send_document, user.bot) monkeypatch.setattr(user.bot, 'send_document', make_assertion) assert user.send_document('test_document') def test_instance_method_send_game(self, monkeypatch, user): 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 check_shortcut_call(user.send_game, user.bot, 'send_game') assert check_defaults_handling(user.send_game, user.bot) monkeypatch.setattr(user.bot, 'send_game', make_assertion) assert user.send_game(game_short_name='test_game') def test_instance_method_send_invoice(self, monkeypatch, user): 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 check_shortcut_call(user.send_invoice, user.bot, 'send_invoice') assert check_defaults_handling(user.send_invoice, user.bot) monkeypatch.setattr(user.bot, 'send_invoice', make_assertion) assert user.send_invoice( 'title', 'description', 'payload', 'provider_token', 'currency', 'prices', ) def test_instance_method_send_location(self, monkeypatch, user): 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 check_shortcut_call(user.send_location, user.bot, 'send_location') assert check_defaults_handling(user.send_location, user.bot) monkeypatch.setattr(user.bot, 'send_location', make_assertion) assert user.send_location('test_location') def test_instance_method_send_sticker(self, monkeypatch, user): 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 check_shortcut_call(user.send_sticker, user.bot, 'send_sticker') assert check_defaults_handling(user.send_sticker, user.bot) monkeypatch.setattr(user.bot, 'send_sticker', make_assertion) assert user.send_sticker('test_sticker') def test_instance_method_send_video(self, monkeypatch, user): 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 check_shortcut_call(user.send_video, user.bot, 'send_video') assert check_defaults_handling(user.send_video, user.bot) monkeypatch.setattr(user.bot, 'send_video', make_assertion) assert user.send_video('test_video') def test_instance_method_send_venue(self, monkeypatch, user): 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 check_shortcut_call(user.send_venue, user.bot, 'send_venue') assert check_defaults_handling(user.send_venue, user.bot) monkeypatch.setattr(user.bot, 'send_venue', make_assertion) assert user.send_venue(title='test_venue') def test_instance_method_send_video_note(self, monkeypatch, user): 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 check_shortcut_call(user.send_video_note, user.bot, 'send_video_note') assert check_defaults_handling(user.send_video_note, user.bot) monkeypatch.setattr(user.bot, 'send_video_note', make_assertion) assert user.send_video_note('test_video_note') def test_instance_method_send_voice(self, monkeypatch, user): 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 check_shortcut_call(user.send_voice, user.bot, 'send_voice') assert check_defaults_handling(user.send_voice, user.bot) monkeypatch.setattr(user.bot, 'send_voice', make_assertion) assert user.send_voice('test_voice') def test_instance_method_send_animation(self, monkeypatch, user): 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 check_shortcut_call(user.send_animation, user.bot, 'send_animation') assert check_defaults_handling(user.send_animation, user.bot) monkeypatch.setattr(user.bot, 'send_animation', make_assertion) assert user.send_animation('test_animation') def test_instance_method_send_poll(self, monkeypatch, user): 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 check_shortcut_call(user.send_poll, user.bot, 'send_poll') assert check_defaults_handling(user.send_poll, user.bot) monkeypatch.setattr(user.bot, 'send_poll', make_assertion) assert user.send_poll(question='test_poll', options=[1, 2]) def test_instance_method_send_copy(self, monkeypatch, user): 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 check_shortcut_call(user.copy_message, user.bot, 'copy_message') assert check_defaults_handling(user.copy_message, user.bot) monkeypatch.setattr(user.bot, 'copy_message', make_assertion) assert user.send_copy(from_chat_id='from_chat_id', message_id='message_id') def test_instance_method_copy_message(self, monkeypatch, user): 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 check_shortcut_call(user.copy_message, user.bot, 'copy_message') assert check_defaults_handling(user.copy_message, user.bot) monkeypatch.setattr(user.bot, 'copy_message', make_assertion) assert user.copy_message(chat_id='chat_id', message_id='message_id') def test_instance_method_approve_join_request(self, monkeypatch, user): 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 check_shortcut_call( user.approve_join_request, user.bot, 'approve_chat_join_request' ) assert check_defaults_handling(user.approve_join_request, user.bot) monkeypatch.setattr(user.bot, 'approve_chat_join_request', make_assertion) assert user.approve_join_request(chat_id='chat_id') def test_instance_method_decline_join_request(self, monkeypatch, user): 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 check_shortcut_call( user.decline_join_request, user.bot, 'decline_chat_join_request' ) assert check_defaults_handling(user.decline_join_request, user.bot) monkeypatch.setattr(user.bot, 'decline_chat_join_request', make_assertion) assert user.decline_join_request(chat_id='chat_id') 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) 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) 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) python-telegram-bot-13.11/tests/test_userprofilephotos.py000066400000000000000000000056041417656324400240100ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 class TestUserProfilePhotos: 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), ], ] def test_slot_behaviour(self, recwarn, mro_slots): inst = UserProfilePhotos(self.total_count, self.photos) for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.total_count = 'should give warning', self.total_count assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.total_count == self.total_count assert user_profile_photos.photos == 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-13.11/tests/test_utils.py000066400000000000000000000026501417656324400213520ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. class TestUtils: def test_promise_deprecation(self, recwarn): import telegram.utils.promise # noqa: F401 assert len(recwarn) == 1 assert str(recwarn[0].message) == ( 'telegram.utils.promise is deprecated. Please use telegram.ext.utils.promise instead.' ) def test_webhookhandler_deprecation(self, recwarn): import telegram.utils.webhookhandler # noqa: F401 assert len(recwarn) == 1 assert str(recwarn[0].message) == ( 'telegram.utils.webhookhandler is deprecated. Please use ' 'telegram.ext.utils.webhookhandler instead.' ) python-telegram-bot-13.11/tests/test_venue.py000066400000000000000000000143651417656324400213420ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 flaky import flaky from telegram import Location, Venue from telegram.error import BadRequest @pytest.fixture(scope='class') def venue(): return Venue( TestVenue.location, TestVenue.title, TestVenue.address, foursquare_id=TestVenue.foursquare_id, foursquare_type=TestVenue.foursquare_type, google_place_id=TestVenue.google_place_id, google_place_type=TestVenue.google_place_type, ) class TestVenue: 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' def test_slot_behaviour(self, venue, mro_slots, recwarn): for attr in venue.__slots__: assert getattr(venue, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not venue.__dict__, f"got missing slot(s): {venue.__dict__}" assert len(mro_slots(venue)) == len(set(mro_slots(venue))), "duplicate slot" venue.custom, venue.title = 'should give warning', self.title assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self, bot): json_dict = { 'location': TestVenue.location.to_dict(), 'title': TestVenue.title, 'address': TestVenue.address, 'foursquare_id': TestVenue.foursquare_id, 'foursquare_type': TestVenue.foursquare_type, 'google_place_id': TestVenue.google_place_id, 'google_place_type': TestVenue.google_place_type, } venue = Venue.de_json(json_dict, bot) 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_send_with_venue(self, monkeypatch, bot, chat_id, venue): def test(url, data, **kwargs): return ( data['longitude'] == self.location.longitude and data['latitude'] == 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', test) message = bot.send_venue(chat_id, venue=venue) assert message @flaky(3, 1) @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'], ) def test_send_venue_default_allow_sending_without_reply( self, default_bot, chat_id, venue, custom ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_venue( chat_id, venue=venue, reply_to_message_id=reply_to_message.message_id ) def test_send_venue_without_required(self, bot, chat_id): with pytest.raises(ValueError, match='Either venue or latitude, longitude, address and'): bot.send_venue(chat_id=chat_id) 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) python-telegram-bot-13.11/tests/test_video.py000066400000000000000000000335161417656324400213250ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 import pytest from flaky import flaky from telegram import Video, TelegramError, Voice, PhotoSize, MessageEntity, Bot from telegram.error import BadRequest from telegram.utils.helpers import escape_markdown from tests.conftest import check_shortcut_call, check_shortcut_signature, check_defaults_handling @pytest.fixture(scope='function') def video_file(): f = open('tests/data/telegram.mp4', 'rb') yield f f.close() @pytest.fixture(scope='class') def video(bot, chat_id): with open('tests/data/telegram.mp4', 'rb') as f: return bot.send_video(chat_id, video=f, timeout=50).video class TestVideo: 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' def test_slot_behaviour(self, video, mro_slots, recwarn): for attr in video.__slots__: assert getattr(video, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not video.__dict__, f"got missing slot(s): {video.__dict__}" assert len(mro_slots(video)) == len(set(mro_slots(video))), "duplicate slot" video.custom, video.width = 'should give warning', self.width assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb, PhotoSize) assert isinstance(video.thumb.file_id, str) assert isinstance(video.thumb.file_unique_id, str) assert video.thumb.file_id != '' assert video.thumb.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 @flaky(3, 1) def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file): message = 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', thumb=thumb_file, ) 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.thumb.file_size == self.thumb_file_size assert message.video.thumb.width == self.thumb_width assert message.video.thumb.height == self.thumb_height assert message.video.file_name == self.file_name assert message.has_protected_content @flaky(3, 1) def test_send_video_custom_filename(self, bot, chat_id, video_file, monkeypatch): def make_assertion(url, data, **kwargs): return data['video'].filename == 'custom_filename' monkeypatch.setattr(bot.request, 'post', make_assertion) assert bot.send_video(chat_id, video_file, filename='custom_filename') @flaky(3, 1) def test_get_and_download(self, bot, video): new_file = bot.get_file(video.file_id) assert new_file.file_size == self.file_size assert new_file.file_id == video.file_id assert new_file.file_unique_id == video.file_unique_id assert new_file.file_path.startswith('https://') new_file.download('telegram.mp4') assert os.path.isfile('telegram.mp4') @flaky(3, 1) def test_send_mp4_file_url(self, bot, chat_id, video): message = 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.thumb, PhotoSize) assert isinstance(message.video.thumb.file_id, str) assert isinstance(message.video.thumb.file_unique_id, str) assert message.video.thumb.file_id != '' assert message.video.thumb.file_unique_id != '' assert message.video.thumb.width == 51 # This seems odd that it's not self.thumb_width assert message.video.thumb.height == 90 # Ditto assert message.video.thumb.file_size == 645 # same assert message.caption == self.caption @flaky(3, 1) 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 = bot.send_video(chat_id, video, caption=test_string, caption_entities=entities) assert message.caption == test_string assert message.caption_entities == entities @flaky(3, 1) def test_resend(self, bot, chat_id, video): message = bot.send_video(chat_id, video.file_id) assert message.video == video def test_send_with_video(self, monkeypatch, bot, chat_id, video): def test(url, data, **kwargs): return data['video'] == video.file_id monkeypatch.setattr(bot.request, 'post', test) message = bot.send_video(chat_id, video=video) assert message @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) 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 = default_bot.send_video(chat_id, video, caption=test_markdown_string) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_video_default_parse_mode_2(self, default_bot, chat_id, video): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_video_default_parse_mode_3(self, default_bot, chat_id, video): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) def test_send_video_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('video') == expected and data.get('thumb') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.send_video(chat_id, file, thumb=file) assert test_flag monkeypatch.delattr(bot, '_post') @flaky(3, 1) @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'], ) def test_send_video_default_allow_sending_without_reply( self, default_bot, chat_id, video, custom ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_video( chat_id, video, reply_to_message_id=reply_to_message.message_id ) 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.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 @flaky(3, 1) def test_error_send_empty_file(self, bot, chat_id): with pytest.raises(TelegramError): bot.send_video(chat_id, open(os.devnull, 'rb')) @flaky(3, 1) def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): bot.send_video(chat_id, '') def test_error_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): bot.send_video(chat_id=chat_id) def test_get_file_instance_method(self, monkeypatch, video): def make_assertion(*_, **kwargs): return kwargs['file_id'] == video.file_id assert check_shortcut_signature(Video.get_file, Bot.get_file, ['file_id'], []) assert check_shortcut_call(video.get_file, video.bot, 'get_file') assert check_defaults_handling(video.get_file, video.bot) monkeypatch.setattr(video.bot, 'get_file', make_assertion) assert video.get_file() 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) python-telegram-bot-13.11/tests/test_videonote.py000066400000000000000000000243301417656324400222050ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 import pytest from flaky import flaky from telegram import VideoNote, TelegramError, Voice, PhotoSize, Bot from telegram.error import BadRequest from tests.conftest import check_shortcut_call, check_shortcut_signature, check_defaults_handling @pytest.fixture(scope='function') def video_note_file(): f = open('tests/data/telegram2.mp4', 'rb') yield f f.close() @pytest.fixture(scope='class') def video_note(bot, chat_id): with open('tests/data/telegram2.mp4', 'rb') as f: return bot.send_video_note(chat_id, video_note=f, timeout=50).video_note class TestVideoNote: 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' def test_slot_behaviour(self, video_note, recwarn, mro_slots): for attr in video_note.__slots__: assert getattr(video_note, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not video_note.__dict__, f"got missing slot(s): {video_note.__dict__}" assert len(mro_slots(video_note)) == len(set(mro_slots(video_note))), "duplicate slot" video_note.custom, video_note.length = 'should give warning', self.length assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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.thumb, PhotoSize) assert isinstance(video_note.thumb.file_id, str) assert isinstance(video_note.thumb.file_unique_id, str) assert video_note.thumb.file_id != '' assert video_note.thumb.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 @flaky(3, 1) def test_send_all_args(self, bot, chat_id, video_note_file, video_note, thumb_file): message = bot.send_video_note( chat_id, video_note_file, duration=self.duration, length=self.length, disable_notification=False, protect_content=True, thumb=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.thumb.file_size == self.thumb_file_size assert message.video_note.thumb.width == self.thumb_width assert message.video_note.thumb.height == self.thumb_height assert message.has_protected_content @flaky(3, 1) def test_send_video_note_custom_filename(self, bot, chat_id, video_note_file, monkeypatch): def make_assertion(url, data, **kwargs): return data['video_note'].filename == 'custom_filename' monkeypatch.setattr(bot.request, 'post', make_assertion) assert bot.send_video_note(chat_id, video_note_file, filename='custom_filename') @flaky(3, 1) def test_get_and_download(self, bot, video_note): new_file = bot.get_file(video_note.file_id) assert new_file.file_size == self.file_size assert new_file.file_id == video_note.file_id assert new_file.file_unique_id == video_note.file_unique_id assert new_file.file_path.startswith('https://') new_file.download('telegram2.mp4') assert os.path.isfile('telegram2.mp4') @flaky(3, 1) def test_resend(self, bot, chat_id, video_note): message = bot.send_video_note(chat_id, video_note.file_id) assert message.video_note == video_note def test_send_with_video_note(self, monkeypatch, bot, chat_id, video_note): def test(url, data, **kwargs): return data['video_note'] == video_note.file_id monkeypatch.setattr(bot.request, 'post', test) message = bot.send_video_note(chat_id, video_note=video_note) assert message 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.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_send_video_note_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('video_note') == expected and data.get('thumb') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.send_video_note(chat_id, file, thumb=file) assert test_flag monkeypatch.delattr(bot, '_post') @flaky(3, 1) @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'], ) def test_send_video_note_default_allow_sending_without_reply( self, default_bot, chat_id, video_note, custom ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_video_note( chat_id, video_note, reply_to_message_id=reply_to_message.message_id ) @flaky(3, 1) def test_error_send_empty_file(self, bot, chat_id): with pytest.raises(TelegramError): bot.send_video_note(chat_id, open(os.devnull, 'rb')) @flaky(3, 1) def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): bot.send_video_note(chat_id, '') def test_error_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): bot.send_video_note(chat_id=chat_id) def test_get_file_instance_method(self, monkeypatch, video_note): 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 check_shortcut_call(video_note.get_file, video_note.bot, 'get_file') assert check_defaults_handling(video_note.get_file, video_note.bot) monkeypatch.setattr(video_note.bot, 'get_file', make_assertion) assert video_note.get_file() 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) python-telegram-bot-13.11/tests/test_voice.py000066400000000000000000000274751417656324400213330ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 import pytest from flaky import flaky from telegram import Audio, Voice, TelegramError, MessageEntity, Bot from telegram.error import BadRequest from telegram.utils.helpers import escape_markdown from tests.conftest import check_shortcut_call, check_shortcut_signature, check_defaults_handling @pytest.fixture(scope='function') def voice_file(): f = open('tests/data/telegram.ogg', 'rb') yield f f.close() @pytest.fixture(scope='class') def voice(bot, chat_id): with open('tests/data/telegram.ogg', 'rb') as f: return bot.send_voice(chat_id, voice=f, timeout=50).voice class TestVoice: 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' def test_slot_behaviour(self, voice, recwarn, mro_slots): for attr in voice.__slots__: assert getattr(voice, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not voice.__dict__, f"got missing slot(s): {voice.__dict__}" assert len(mro_slots(voice)) == len(set(mro_slots(voice))), "duplicate slot" voice.custom, voice.duration = 'should give warning', self.duration assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 @flaky(3, 1) def test_send_all_args(self, bot, chat_id, voice_file, voice): message = 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 @flaky(3, 1) def test_send_voice_custom_filename(self, bot, chat_id, voice_file, monkeypatch): def make_assertion(url, data, **kwargs): return data['voice'].filename == 'custom_filename' monkeypatch.setattr(bot.request, 'post', make_assertion) assert bot.send_voice(chat_id, voice_file, filename='custom_filename') @flaky(3, 1) def test_get_and_download(self, bot, voice): new_file = bot.get_file(voice.file_id) assert new_file.file_size == voice.file_size assert new_file.file_id == voice.file_id assert new_file.file_unique_id == voice.file_unique_id assert new_file.file_path.startswith('https://') new_file.download('telegram.ogg') assert os.path.isfile('telegram.ogg') @flaky(3, 1) def test_send_ogg_url_file(self, bot, chat_id, voice): message = 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 @flaky(3, 1) def test_resend(self, bot, chat_id, voice): message = bot.sendVoice(chat_id, voice.file_id) assert message.voice == voice def test_send_with_voice(self, monkeypatch, bot, chat_id, voice): def test(url, data, **kwargs): return data['voice'] == voice.file_id monkeypatch.setattr(bot.request, 'post', test) message = bot.send_voice(chat_id, voice=voice) assert message @flaky(3, 1) 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 = bot.send_voice( chat_id, voice_file, caption=test_string, caption_entities=entities ) assert message.caption == test_string assert message.caption_entities == entities @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) 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 = default_bot.send_voice(chat_id, voice, caption=test_markdown_string) assert message.caption_markdown == test_markdown_string assert message.caption == test_string @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_voice_default_parse_mode_2(self, default_bot, chat_id, voice): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) @flaky(3, 1) @pytest.mark.parametrize('default_bot', [{'parse_mode': 'Markdown'}], indirect=True) def test_send_voice_default_parse_mode_3(self, default_bot, chat_id, voice): test_markdown_string = '_Italic_ *Bold* `Code`' message = 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) def test_send_voice_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False expected = (Path.cwd() / 'tests/data/telegram.jpg/').as_uri() file = 'tests/data/telegram.jpg' def make_assertion(_, data, *args, **kwargs): nonlocal test_flag test_flag = data.get('voice') == expected monkeypatch.setattr(bot, '_post', make_assertion) bot.send_voice(chat_id, file) assert test_flag monkeypatch.delattr(bot, '_post') @flaky(3, 1) @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'], ) def test_send_voice_default_allow_sending_without_reply( self, default_bot, chat_id, voice, custom ): reply_to_message = default_bot.send_message(chat_id, 'test') reply_to_message.delete() if custom is not None: message = 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 = 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 not found'): default_bot.send_voice( chat_id, voice, reply_to_message_id=reply_to_message.message_id ) 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, 'caption': self.caption, 'mime_type': self.mime_type, 'file_size': self.file_size, } json_voice = Voice.de_json(json_dict, bot) 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 @flaky(3, 1) def test_error_send_empty_file(self, bot, chat_id): with pytest.raises(TelegramError): bot.sendVoice(chat_id, open(os.devnull, 'rb')) @flaky(3, 1) def test_error_send_empty_file_id(self, bot, chat_id): with pytest.raises(TelegramError): bot.sendVoice(chat_id, '') def test_error_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): bot.sendVoice(chat_id) def test_get_file_instance_method(self, monkeypatch, voice): def make_assertion(*_, **kwargs): return kwargs['file_id'] == voice.file_id assert check_shortcut_signature(Voice.get_file, Bot.get_file, ['file_id'], []) assert check_shortcut_call(voice.get_file, voice.bot, 'get_file') assert check_defaults_handling(voice.get_file, voice.bot) monkeypatch.setattr(voice.bot, 'get_file', make_assertion) assert voice.get_file() 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) python-telegram-bot-13.11/tests/test_voicechat.py000066400000000000000000000161431417656324400221610ustar00rootroot00000000000000#!/usr/bin/env python # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 ( VoiceChatStarted, VoiceChatEnded, VoiceChatParticipantsInvited, User, VoiceChatScheduled, ) from telegram.utils.helpers import to_timestamp @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) class TestVoiceChatStarted: def test_slot_behaviour(self, recwarn, mro_slots): action = VoiceChatStarted() for attr in action.__slots__: assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not action.__dict__, f"got missing slot(s): {action.__dict__}" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" action.custom = 'should give warning' assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self): voice_chat_started = VoiceChatStarted.de_json({}, None) assert isinstance(voice_chat_started, VoiceChatStarted) def test_to_dict(self): voice_chat_started = VoiceChatStarted() voice_chat_dict = voice_chat_started.to_dict() assert voice_chat_dict == {} class TestVoiceChatEnded: duration = 100 def test_slot_behaviour(self, recwarn, mro_slots): action = VoiceChatEnded(8) for attr in action.__slots__: assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not action.__dict__, f"got missing slot(s): {action.__dict__}" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" action.custom = 'should give warning' assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self): json_dict = {'duration': self.duration} voice_chat_ended = VoiceChatEnded.de_json(json_dict, None) assert voice_chat_ended.duration == self.duration def test_to_dict(self): voice_chat_ended = VoiceChatEnded(self.duration) voice_chat_dict = voice_chat_ended.to_dict() assert isinstance(voice_chat_dict, dict) assert voice_chat_dict["duration"] == self.duration def test_equality(self): a = VoiceChatEnded(100) b = VoiceChatEnded(100) c = VoiceChatEnded(50) d = VoiceChatStarted() 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 TestVoiceChatParticipantsInvited: def test_slot_behaviour(self, recwarn, mro_slots): action = VoiceChatParticipantsInvited([user1]) for attr in action.__slots__: assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not action.__dict__, f"got missing slot(s): {action.__dict__}" assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" action.custom = 'should give warning' assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_de_json(self, user1, user2, bot): json_data = {"users": [user1.to_dict(), user2.to_dict()]} voice_chat_participants = VoiceChatParticipantsInvited.de_json(json_data, bot) assert isinstance(voice_chat_participants.users, list) assert voice_chat_participants.users[0] == user1 assert voice_chat_participants.users[1] == user2 assert voice_chat_participants.users[0].id == user1.id assert voice_chat_participants.users[1].id == user2.id def test_to_dict(self, user1, user2): voice_chat_participants = VoiceChatParticipantsInvited([user1, user2]) voice_chat_dict = voice_chat_participants.to_dict() assert isinstance(voice_chat_dict, dict) assert voice_chat_dict["users"] == [user1.to_dict(), user2.to_dict()] assert voice_chat_dict["users"][0]["id"] == user1.id assert voice_chat_dict["users"][1]["id"] == user2.id def test_equality(self, user1, user2): a = VoiceChatParticipantsInvited([user1]) b = VoiceChatParticipantsInvited([user1]) c = VoiceChatParticipantsInvited([user1, user2]) d = VoiceChatParticipantsInvited([user2]) e = VoiceChatStarted() 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 TestVoiceChatScheduled: start_date = dtm.datetime.utcnow() def test_slot_behaviour(self, recwarn, mro_slots): inst = VoiceChatScheduled(self.start_date) for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" inst.custom, inst.start_date = 'should give warning', self.start_date assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list def test_expected_values(self): assert pytest.approx(VoiceChatScheduled(start_date=self.start_date) == self.start_date) def test_de_json(self, bot): assert VoiceChatScheduled.de_json({}, bot=bot) is None json_dict = {'start_date': to_timestamp(self.start_date)} voice_chat_scheduled = VoiceChatScheduled.de_json(json_dict, bot) assert pytest.approx(voice_chat_scheduled.start_date == self.start_date) def test_to_dict(self): voice_chat_scheduled = VoiceChatScheduled(self.start_date) voice_chat_scheduled_dict = voice_chat_scheduled.to_dict() assert isinstance(voice_chat_scheduled_dict, dict) assert voice_chat_scheduled_dict["start_date"] == to_timestamp(self.start_date) def test_equality(self): a = VoiceChatScheduled(self.start_date) b = VoiceChatScheduled(self.start_date) c = VoiceChatScheduled(dtm.datetime.utcnow() + dtm.timedelta(seconds=5)) d = VoiceChatStarted() 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-13.11/tests/test_webhookinfo.py000066400000000000000000000073101417656324400225220ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2022 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 import time from telegram import WebhookInfo, LoginUrl @pytest.fixture(scope='class') def webhook_info(): return WebhookInfo( url=TestWebhookInfo.url, has_custom_certificate=TestWebhookInfo.has_custom_certificate, pending_update_count=TestWebhookInfo.pending_update_count, ip_address=TestWebhookInfo.ip_address, last_error_date=TestWebhookInfo.last_error_date, max_connections=TestWebhookInfo.max_connections, allowed_updates=TestWebhookInfo.allowed_updates, ) class TestWebhookInfo: 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'] def test_slot_behaviour(self, webhook_info, mro_slots, recwarn): for attr in webhook_info.__slots__: assert getattr(webhook_info, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not webhook_info.__dict__, f"got missing slot(s): {webhook_info.__dict__}" assert len(mro_slots(webhook_info)) == len(set(mro_slots(webhook_info))), "duplicate slot" webhook_info.custom, webhook_info.url = 'should give warning', self.url assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list 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 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 = 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)