pax_global_header00006660000000000000000000000064136202313360014511gustar00rootroot0000000000000052 comment=72ecc696cb5bc9dc19da7e2f28a0c2b719c60dab python-telegram-bot-12.4.2/000077500000000000000000000000001362023133600155005ustar00rootroot00000000000000python-telegram-bot-12.4.2/.github/000077500000000000000000000000001362023133600170405ustar00rootroot00000000000000python-telegram-bot-12.4.2/.github/CONTRIBUTING.rst000066400000000000000000000224751362023133600215130ustar00rootroot00000000000000How 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 `developers' mailing list`_, and we'll help you get started. That being said, we want to mention that we are very hesistant 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``. 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. - 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. - For consistency, please conform to `Google Python Style Guide`_ and `Google Python Style Docstrings`_. In addition, code should be formatted consistently with other code around it. - 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. - 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:: $ make test If you don't have ``make``, do: .. 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. - To actually make the commit (this will trigger tests for yapf, lint and pep8 automatically): .. code-block:: bash $ git add your-file-changed.py - yapf may change code formatting, make sure to re-add them to your commit. .. code-block:: bash $ git commit -a -m "your-commit-message-here" - 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 ------------------ Specific commandments ##################### - Avoid using "double quotes" where you can reasonably use 'single quotes'. 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 .. _`developers' mailing list`: mailto:devs@python-telegram-bot.org .. _`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 python-telegram-bot-12.4.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001362023133600212235ustar00rootroot00000000000000python-telegram-bot-12.4.2/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000014451362023133600237210ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: 'bug :bug:' assignees: '' --- ### Steps to reproduce 1. 2. 3. ### Expected behaviour Tell us what should happen ### Actual behaviour Tell us what happens instead ### Configuration **Operating System:** **Version of Python, python-telegram-bot & dependencies:** ``$ python -m telegram`` ### Logs Insert logs here (if necessary) python-telegram-bot-12.4.2/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000014431362023133600247520ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: "[FEATURE]" labels: enhancement assignees: '' --- #### Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. *I want to do X, but there is no way to do it.* #### Describe the solution you'd like A clear and concise description of what you want to happen. Ex. *I think it would be nice if you would add feature Y so it will make it easier.* #### Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered. Ex. *I considered Z, but that didn't work because...* #### Additional context Add any other context or screenshots about the feature request here. Ex. *Here's a photo of my cat!* python-telegram-bot-12.4.2/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000020311362023133600234100ustar00rootroot00000000000000--- name: Question about: Get help with errors or general questions title: "[QUESTION]" labels: 'question :question:' assignees: '' --- ### Issue I am facing Please describe the issue here in as much detail as possible ### Traceback to the issue ``` put it here ``` ### Related part of your code ```python put it here ``` python-telegram-bot-12.4.2/.github/workflows/000077500000000000000000000000001362023133600210755ustar00rootroot00000000000000python-telegram-bot-12.4.2/.github/workflows/example_notifier.yml000066400000000000000000000007561362023133600251620ustar00rootroot00000000000000name: 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-12.4.2/.github/workflows/test.yml000066400000000000000000000143051362023133600226020ustar00rootroot00000000000000name: GitHub Actions on: pull_request: branches: - master schedule: - cron: 7 3 * * * push: branches: - master jobs: pytest: name: pytest runs-on: ${{matrix.os}} strategy: matrix: python-version: [3.5, 3.6, 3.7] os: [ubuntu-latest, windows-latest] include: - os: ubuntu-latest python-version: 3.7 test-build: True - os: windows-latest python-version: 3.7 test-build: True fail-fast: False steps: - uses: actions/checkout@v1 - name: Initialize vendored libs run: git submodule update --init --recursive - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 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 run: | pytest -v -m nocoverage nocov_exit=$? pytest -v -m "not nocoverage" --cov cov_exit=$? global_exit=$(( nocov_exit > cov_exit ? nocov_exit : cov_exit )) exit ${global_exit} env: JOB_INDEX: ${{ strategy.job-index }} BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMi43IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzI3X2JvdCJ9LCB7InRva2VuIjogIjY3MTQ2ODg4NjpBQUdQR2ZjaVJJQlVORmU4MjR1SVZkcTdKZTNfWW5BVE5HdyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpHWXdPVGxrTXpNeE4yWTIiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNF9ib3QifSwgeyJ0b2tlbiI6ICI2MjkzMjY1Mzg6QUFGUnJaSnJCN29CM211ekdzR0pYVXZHRTVDUXpNNUNVNG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNbU01WVdKaFl6a3hNMlUxIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjUiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy42IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM2X2JvdCJ9LCB7InRva2VuIjogIjY5NTEwNDA4ODpBQUhmenlsSU9qU0lJUy1lT25JMjB5MkUyMEhvZEhzZnotMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk9HUTFNRGd3WmpJd1pqRmwiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zN19ib3QifSwgeyJ0b2tlbiI6ICI2OTE0MjM1NTQ6QUFGOFdrakNaYm5IcVBfaTZHaFRZaXJGRWxackdhWU9oWDAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZamM1TlRoaU1tUXlNV1ZoIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAyLjciLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMy41IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19weXB5XzM1X2JvdCJ9LCB7InRva2VuIjogIjY5MDA5MTM0NzpBQUZMbVI1cEFCNVljcGVfbU9oN3pNNEpGQk9oMHozVDBUbyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpEaGxOekU1TURrd1lXSmkiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2FwcHZleW9yX2NweXRob25fMzRfYm90In0sIHsidG9rZW4iOiAiNjk0MzA4MDUyOkFBRUIyX3NvbkNrNTVMWTlCRzlBTy1IOGp4aVBTNTVvb0JBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WW1aaVlXWm1NakpoWkdNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gQXBwVmV5b3IgdXNpbmcgQ1B5dGhvbiAyLjciLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QifV0= TEST_BUILD: ${{ matrix.test-build }} TEST_PRE_COMMIT: ${{ matrix.test-pre-commit }} shell: bash --noprofile --norc {0} - name: Submit coverage run: | if [ "$CODECOV_TOKEN" != "" ]; then codecov -F github -t $CODECOV_TOKEN --name "${{ matrix.os }}-${{ matrix.python-version }}" fi env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} shell: bash 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@v1 - name: Initialize vendored libs run: git submodule update --init --recursive - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 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 $? continue-on-error: True 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@v1 - name: Initialize vendored libs run: git submodule update --init --recursive - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 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-12.4.2/.gitignore000066400000000000000000000020371362023133600174720ustar00rootroot00000000000000# 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 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 python-telegram-bot-12.4.2/.gitmodules000066400000000000000000000002221362023133600176510ustar00rootroot00000000000000[submodule "telegram/vendor/urllib3"] path = telegram/vendor/ptb_urllib3 url = https://github.com/python-telegram-bot/urllib3.git branch = ptb python-telegram-bot-12.4.2/.pre-commit-config.yaml000066400000000000000000000011151362023133600217570ustar00rootroot00000000000000repos: - repo: git://github.com/python-telegram-bot/mirrors-yapf sha: 5769e088ef6e0a0d1eb63bd6d0c1fe9f3606d6c8 hooks: - id: yapf files: ^(telegram|tests)/.*\.py$ args: - --diff - repo: git://github.com/pre-commit/pre-commit-hooks sha: 0b70e285e369bcb24b57b74929490ea7be9c4b19 hooks: - id: flake8 - repo: git://github.com/pre-commit/mirrors-pylint sha: 9d8dcbc2b86c796275680f239c1e90dcd50bd398 hooks: - id: pylint files: ^telegram/.*\.py$ args: - --errors-only - --disable=import-error python-telegram-bot-12.4.2/.readthedocs.yml000066400000000000000000000002651362023133600205710ustar00rootroot00000000000000# syntax: https://docs.readthedocs.io/en/latest/yaml-config.html formats: - pdf python: setup_py_install: true version: 3 requirements_file: docs/requirements-docs.txt python-telegram-bot-12.4.2/.travis.yml000066400000000000000000000026121362023133600176120ustar00rootroot00000000000000language: python matrix: include: - python: 2.7 - python: 3.5 - python: 3.6 - python: 3.7 dist: xenial sudo: true - python: 3.7 dist: xenial env: TEST_OFFICIAL=true - python: pypy2.7-5.10.0 dist: xenial - python: pypy3.5-5.10.1 dist: xenial - python: 3.8-dev dist: xenial allow_failures: - python: pypy2.7-5.10.0 - python: pypy3.5-5.10.1 dist: trusty sudo: false branches: only: - master - /^[vV]\d+$/ cache: directories: - $HOME/.cache/pip - $HOME/.pre-commit before_cache: - rm -f $HOME/.cache/pip/log/debug.log - rm -f $HOME/.pre-commit/pre-commit.log install: # fix TypeError from old version of this - pip install -U codecov pytest-cov - echo $TRAVIS_PYTHON_VERSION - if [[ $TRAVIS_PYTHON_VERSION == '3.7'* ]]; then pip install -U git+https://github.com/yaml/pyyaml.git; else true; fi - pip install -U -r requirements.txt - pip install -U -r requirements-dev.txt - if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then pip install ujson; else true; fi script: - if [[ $TEST_OFFICIAL != 'true' ]]; then pytest -v -m nocoverage; else true; fi - if [[ $TEST_OFFICIAL != 'true' ]]; then pytest -v -m "not nocoverage" --cov; else true; fi - if [[ $TEST_OFFICIAL == 'true' ]]; then pytest -v tests/test_official.py; else true; fi after_success: - coverage combine - codecov -F Travis python-telegram-bot-12.4.2/AUTHORS.rst000066400000000000000000000077461362023133600173750ustar00rootroot00000000000000Credits ======= ``python-telegram-bot`` was originally created by `Leandro Toledo `_ and is now maintained by `Jannes Höke `_ (`@jh0ker `_ on Telegram), `Noam Meltzer `_, `Pieter Schutz `_, `Jasmin Bom `_ and `Hinrich Mahler `_. 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: - `Alateas `_ - `Ales Dokshanin `_ - `Ambro17 `_ - `Anton Tagunov `_ - `Avanatiker `_ - `Balduro `_ - `Bibo-Joshi `_ - `bimmlerd `_ - `d-qoi `_ - `daimajia `_ - `Daniel Reed `_ - `Eana Hufwe `_ - `Ehsan Online `_ - `Eli Gao `_ - `Emilio Molinari `_ - `ErgoZ Riftbit Vaper `_ - `Eugene Lisitsky `_ - `Eugenio Panadero `_ - `Evan Haberecht `_ - `evgfilim1 `_ - `franciscod `_ - `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 `_ - `macrojames `_ - `Michael Elovskikh `_ - `Mischa Krüger `_ - `naveenvhegde `_ - `neurrone `_ - `njittam `_ - `Noam Meltzer `_ - `Oleg Shlyazhko `_ - `Oleg Sushchenko `_ - `Or Bin `_ - `overquota `_ - `Patrick Hofmann `_ - `Paul Larsen `_ - `Pieter Schutz `_ - `Poolitzer `_ - `Rahiel Kasim `_ - `Riko Naka `_ - `Rizlas `_ - `Sahil Sharma `_ - `Sascha `_ - `Shelomentsev D `_ - `Simon Schürrle `_ - `sooyhwang `_ - `syntx `_ - `thodnev `_ - `Trainer Jono `_ - `Valentijn `_ - `voider1 `_ - `Vorobjev Simon `_ - `Wagner Macedo `_ - `wjt `_ - `zeshuaro `_ Please add yourself here alphabetically when you submit your first pull request. python-telegram-bot-12.4.2/CHANGES.rst000066400000000000000000001356551362023133600173210ustar00rootroot00000000000000========= Changelog ========= 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-12.4.2/CODE_OF_CONDUCT.md000066400000000000000000000064151362023133600203050ustar00rootroot00000000000000# 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-12.4.2/LICENSE000066400000000000000000000772461362023133600165250ustar00rootroot00000000000000 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-12.4.2/LICENSE.dual000066400000000000000000001164001362023133600174330ustar00rootroot00000000000000 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-12.4.2/LICENSE.lesser000066400000000000000000000167431362023133600200140ustar00rootroot00000000000000 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-12.4.2/MANIFEST.in000066400000000000000000000000711362023133600172340ustar00rootroot00000000000000include LICENSE LICENSE.lesser Makefile requirements.txt python-telegram-bot-12.4.2/Makefile000066400000000000000000000024301362023133600171370ustar00rootroot00000000000000.DEFAULT_GOAL := help .PHONY: clean pep257 pep8 yapf lint test install PYLINT := pylint PYTEST := pytest PEP257 := pep257 PEP8 := flake8 YAPF := yapf PIP := pip clean: rm -fr build rm -fr dist find . -name '*.pyc' -exec rm -f {} \; find . -name '*.pyo' -exec rm -f {} \; find . -name '*~' -exec rm -f {} \; find . -regex "./telegram.\(mp3\|mp4\|ogg\|png\|webp\)" -exec rm {} \; pep257: $(PEP257) telegram pep8: $(PEP8) telegram yapf: $(YAPF) -r telegram lint: $(PYLINT) -E telegram --disable=no-name-in-module,import-error test: $(PYTEST) -v install: $(PIP) install -r requirements.txt -r requirements-dev.txt help: @echo "Available targets:" @echo "- clean Clean up the source directory" @echo "- pep257 Check docstring style with pep257" @echo "- pep8 Check style with flake8" @echo "- lint Check style with pylint" @echo "- yapf Check style with yapf" @echo "- test Run tests using pytest" @echo @echo "Available variables:" @echo "- PYLINT default: $(PYLINT)" @echo "- PYTEST default: $(PYTEST)" @echo "- PEP257 default: $(PEP257)" @echo "- PEP8 default: $(PEP8)" @echo "- YAPF default: $(YAPF)" @echo "- PIP default: $(PIP)" python-telegram-bot-12.4.2/README.rst000066400000000000000000000165031362023133600171740ustar00rootroot00000000000000.. 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://www.cpu.re/static/python-telegram-bot/downloads.svg :target: https://www.cpu.re/static/python-telegram-bot/downloads-by-python-version.txt :alt: PyPi Package Monthly Download .. image:: https://img.shields.io/badge/docs-latest-af1a97.svg :target: https://python-telegram-bot.readthedocs.io/ :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?event=schedule :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 .. image:: https://img.shields.io/badge/Telegram-Group-blue.svg :target: https://telegram.me/pythontelegrambotgroup :alt: Telegram Group .. image:: https://img.shields.io/badge/IRC-Channel-blue.svg :target: https://webchat.freenode.net/?channels=##python-telegram-bot :alt: IRC Bridge ================= 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.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. ==================== Telegram API support ==================== All types and methods of the Telegram Bot API **4.1** 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 =============== 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 ``echobot2``, 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 `_. 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 `_. ======= 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-12.4.2/appveyor.yml000066400000000000000000000021361362023133600200720ustar00rootroot00000000000000environment: matrix: # For Python versions available on Appveyor, see # https://www.appveyor.com/docs/windows-images-software/#python # The list here is complete (excluding Python 2.6, which # isn't covered by this document) at the time of writing. - PYTHON: "C:\\Python27" - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python37" # - PYTHON: "C:\\Python38" branches: only: - master - /^[vV]\d+$/ skip_branch_with_pr: true max_jobs: 1 install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "git submodule update --init --recursive" # Check that we have the expected version and architecture for Python - "python --version" # We need wheel installed to build wheels # fix TypeError from an old version of this - "pip install attrs==17.4.0" - "pip install -U codecov pytest-cov" - "pip install -r requirements.txt" - "pip install -r requirements-dev.txt" build: off test_script: - "pytest --version" - "pytest -m \"not nocoverage\" --cov --cov-report xml:coverage.xml" after_test: - "codecov -f coverage.xml -F Appveyor" python-telegram-bot-12.4.2/codecov.yml000066400000000000000000000000171362023133600176430ustar00rootroot00000000000000comment: false python-telegram-bot-12.4.2/contrib/000077500000000000000000000000001362023133600171405ustar00rootroot00000000000000python-telegram-bot-12.4.2/contrib/build-debian.sh000077500000000000000000000001261362023133600220150ustar00rootroot00000000000000#!/bin/bash cp -R contrib/debian . debuild -us -uc debian/rules clean rm -rf debian python-telegram-bot-12.4.2/contrib/debian/000077500000000000000000000000001362023133600203625ustar00rootroot00000000000000python-telegram-bot-12.4.2/contrib/debian/changelog000066400000000000000000000002461362023133600222360ustar00rootroot00000000000000telegram (12.0.0b1) unstable; urgency=medium * Debian packaging; * Initial Release. -- Marco Marinello Thu, 22 Aug 2019 20:36:47 +0200 python-telegram-bot-12.4.2/contrib/debian/compat000066400000000000000000000000031362023133600215610ustar00rootroot0000000000000011 python-telegram-bot-12.4.2/contrib/debian/control000066400000000000000000000017571362023133600217770ustar00rootroot00000000000000Source: 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 2.7, 3.3+ 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-12.4.2/contrib/debian/copyright000066400000000000000000000021521362023133600223150ustar00rootroot00000000000000Format: 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-12.4.2/contrib/debian/install000066400000000000000000000000601362023133600217470ustar00rootroot00000000000000AUTHORS.rst /usr/share/doc/python3-telegram-bot python-telegram-bot-12.4.2/contrib/debian/rules000077500000000000000000000011271362023133600214430ustar00rootroot00000000000000#!/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 python2,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-12.4.2/contrib/debian/source/000077500000000000000000000000001362023133600216625ustar00rootroot00000000000000python-telegram-bot-12.4.2/contrib/debian/source/format000066400000000000000000000000151362023133600230710ustar00rootroot000000000000003.0 (native) python-telegram-bot-12.4.2/contrib/debian/source/options000066400000000000000000000000521362023133600232750ustar00rootroot00000000000000extend-diff-ignore = "^[^/]*[.]egg-info/" python-telegram-bot-12.4.2/docs/000077500000000000000000000000001362023133600164305ustar00rootroot00000000000000python-telegram-bot-12.4.2/docs/Makefile000066400000000000000000000164451362023133600201020ustar00rootroot00000000000000# 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-12.4.2/docs/requirements-docs.txt000066400000000000000000000000621362023133600226400ustar00rootroot00000000000000sphinx>=1.7.9 sphinx_rtd_theme sphinx-pypi-upload python-telegram-bot-12.4.2/docs/source/000077500000000000000000000000001362023133600177305ustar00rootroot00000000000000python-telegram-bot-12.4.2/docs/source/_static/000077500000000000000000000000001362023133600213565ustar00rootroot00000000000000python-telegram-bot-12.4.2/docs/source/_static/.placeholder000066400000000000000000000000001362023133600236270ustar00rootroot00000000000000python-telegram-bot-12.4.2/docs/source/changelog.rst000066400000000000000000000000361362023133600224100ustar00rootroot00000000000000.. include:: ../../CHANGES.rstpython-telegram-bot-12.4.2/docs/source/conf.py000066400000000000000000000231331362023133600212310ustar00rootroot00000000000000# -*- 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 shlex # 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 = '1.7.9' # 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' ] # 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-2020, 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 = '12.4' # telegram.__version__[:3] # The full version, including alpha/beta/rc tags. release = '12.4.2' # 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 = {} # 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 = 'PythonTelegramBotdoc' # -- 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, 'PythonTelegramBot.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, 'pythontelegrambot', 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, 'PythonTelegramBot', u'Python Telegram Bot Documentation', author, 'PythonTelegramBot', "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.connect('autodoc-skip-member', autodoc_skip_member) python-telegram-bot-12.4.2/docs/source/index.rst000066400000000000000000000025201362023133600215700ustar00rootroot00000000000000.. 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 at our `github in the examples folder `_. 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 Changelog --------- .. toctree:: :maxdepth: 2 changelog python-telegram-bot-12.4.2/docs/source/ptb-logo-orange.ico000066400000000000000000013226261362023133600234340ustar00rootroot00000000000000 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-12.4.2/docs/source/ptb-logo-orange.png000077500000000000000000002234721362023133600234470ustar00rootroot00000000000000PNG  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-12.4.2/docs/source/telegram.animation.rst000066400000000000000000000001561362023133600242420ustar00rootroot00000000000000telegram.Animation ================== .. autoclass:: telegram.Animation :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.audio.rst000066400000000000000000000001421362023133600233570ustar00rootroot00000000000000telegram.Audio ============== .. autoclass:: telegram.Audio :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.bot.rst000066400000000000000000000001331362023133600230420ustar00rootroot00000000000000telegram.Bot ============ .. autoclass:: telegram.Bot :members: :show-inheritance:python-telegram-bot-12.4.2/docs/source/telegram.callbackgame.rst000066400000000000000000000001671362023133600246530ustar00rootroot00000000000000telegram.Callbackgame ===================== .. autoclass:: telegram.CallbackGame :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.callbackquery.rst000066400000000000000000000001721362023133600251030ustar00rootroot00000000000000telegram.CallbackQuery ====================== .. autoclass:: telegram.CallbackQuery :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.chat.rst000066400000000000000000000001371362023133600232010ustar00rootroot00000000000000telegram.Chat ============= .. autoclass:: telegram.Chat :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.chataction.rst000066400000000000000000000001611362023133600243740ustar00rootroot00000000000000telegram.ChatAction =================== .. autoclass:: telegram.ChatAction :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.chatmember.rst000066400000000000000000000001611362023133600243660ustar00rootroot00000000000000telegram.ChatMember =================== .. autoclass:: telegram.ChatMember :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.chatpermissions.rst000066400000000000000000000002001362023133600254640ustar00rootroot00000000000000telegram.ChatPermissions ======================== .. autoclass:: telegram.ChatPermissions :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.chatphoto.rst000066400000000000000000000001561362023133600242540ustar00rootroot00000000000000telegram.ChatPhoto ================== .. autoclass:: telegram.ChatPhoto :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.choseninlineresult.rst000066400000000000000000000002111362023133600261700ustar00rootroot00000000000000telegram.ChosenInlineResult =========================== .. autoclass:: telegram.ChosenInlineResult :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.constants.rst000066400000000000000000000001751362023133600243000ustar00rootroot00000000000000telegram.constants Module ========================= .. automodule:: telegram.constants :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.contact.rst000066400000000000000000000001501362023133600237100ustar00rootroot00000000000000telegram.Contact ================ .. autoclass:: telegram.Contact :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.credentials.rst000066400000000000000000000001641362023133600245570ustar00rootroot00000000000000telegram.Credentials ==================== .. autoclass:: telegram.Credentials :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.datacredentials.rst000066400000000000000000000002001362023133600254000ustar00rootroot00000000000000telegram.DataCredentials ======================== .. autoclass:: telegram.DataCredentials :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.document.rst000066400000000000000000000001531362023133600240760ustar00rootroot00000000000000telegram.Document ================= .. autoclass:: telegram.Document :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.encryptedcredentials.rst000066400000000000000000000002171362023133600264740ustar00rootroot00000000000000telegram.EncryptedCredentials ============================= .. autoclass:: telegram.EncryptedCredentials :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.encryptedpassportelement.rst000066400000000000000000000002331362023133600274220ustar00rootroot00000000000000telegram.EncryptedPassportElement ================================= .. autoclass:: telegram.EncryptedPassportElement :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.error.rst000066400000000000000000000002051362023133600234070ustar00rootroot00000000000000telegram.error module ===================== .. automodule:: telegram.error :members: :undoc-members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.basepersistence.rst000066400000000000000000000002141362023133600262340ustar00rootroot00000000000000telegram.ext.BasePersistence ============================ .. autoclass:: telegram.ext.BasePersistence :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.callbackcontext.rst000066400000000000000000000001651362023133600262230ustar00rootroot00000000000000telegram.ext.CallbackContext ============================ .. autoclass:: telegram.ext.CallbackContext :members: python-telegram-bot-12.4.2/docs/source/telegram.ext.callbackqueryhandler.rst000066400000000000000000000002331362023133600272360ustar00rootroot00000000000000telegram.ext.CallbackQueryHandler ================================= .. autoclass:: telegram.ext.CallbackQueryHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.choseninlineresulthandler.rst000066400000000000000000000002521362023133600303320ustar00rootroot00000000000000telegram.ext.ChosenInlineResultHandler ====================================== .. autoclass:: telegram.ext.ChosenInlineResultHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.commandhandler.rst000066400000000000000000000002111362023133600260260ustar00rootroot00000000000000telegram.ext.CommandHandler =========================== .. autoclass:: telegram.ext.CommandHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.conversationhandler.rst000066400000000000000000000002301362023133600271230ustar00rootroot00000000000000telegram.ext.ConversationHandler ================================ .. autoclass:: telegram.ext.ConversationHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.defaults.rst000066400000000000000000000001671362023133600246730ustar00rootroot00000000000000telegram.ext.Defaults ===================== .. autoclass:: telegram.ext.Defaults :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.delayqueue.rst000066400000000000000000000002231362023133600252200ustar00rootroot00000000000000telegram.ext.DelayQueue ======================= .. autoclass:: telegram.ext.DelayQueue :members: :show-inheritance: :special-members: python-telegram-bot-12.4.2/docs/source/telegram.ext.dictpersistence.rst000066400000000000000000000002141362023133600262450ustar00rootroot00000000000000telegram.ext.DictPersistence ============================ .. autoclass:: telegram.ext.DictPersistence :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.dispatcher.rst000066400000000000000000000001751362023133600252110ustar00rootroot00000000000000telegram.ext.Dispatcher ======================= .. autoclass:: telegram.ext.Dispatcher :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.dispatcherhandlerstop.rst000066400000000000000000000002361362023133600274530ustar00rootroot00000000000000telegram.ext.DispatcherHandlerStop ================================== .. autoclass:: telegram.ext.DispatcherHandlerStop :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.filters.rst000066400000000000000000000002031362023133600245230ustar00rootroot00000000000000telegram.ext.filters Module =========================== .. automodule:: telegram.ext.filters :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.handler.rst000066400000000000000000000002101362023133600244660ustar00rootroot00000000000000telegram.ext.Handler ==================== .. autoclass:: telegram.ext.Handler :members: :undoc-members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.inlinequeryhandler.rst000066400000000000000000000002251362023133600267610ustar00rootroot00000000000000telegram.ext.InlineQueryHandler =============================== .. autoclass:: telegram.ext.InlineQueryHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.job.rst000066400000000000000000000001551362023133600236330ustar00rootroot00000000000000telegram.ext.Job ===================== .. autoclass:: telegram.ext.Job :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.jobqueue.rst000066400000000000000000000001671362023133600247030ustar00rootroot00000000000000telegram.ext.JobQueue ===================== .. autoclass:: telegram.ext.JobQueue :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.messagehandler.rst000066400000000000000000000002111362023133600260340ustar00rootroot00000000000000telegram.ext.MessageHandler =========================== .. autoclass:: telegram.ext.MessageHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.messagequeue.rst000066400000000000000000000002311362023133600255450ustar00rootroot00000000000000telegram.ext.MessageQueue ========================= .. autoclass:: telegram.ext.MessageQueue :members: :show-inheritance: :special-members: python-telegram-bot-12.4.2/docs/source/telegram.ext.picklepersistence.rst000066400000000000000000000002221362023133600265700ustar00rootroot00000000000000telegram.ext.PicklePersistence ============================== .. autoclass:: telegram.ext.PicklePersistence :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.precheckoutqueryhandler.rst000066400000000000000000000002441362023133600300200ustar00rootroot00000000000000telegram.ext.PreCheckoutQueryHandler ==================================== .. autoclass:: telegram.ext.PreCheckoutQueryHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.prefixhandler.rst000066400000000000000000000002071362023133600257120ustar00rootroot00000000000000telegram.ext.PrefixHandler =========================== .. autoclass:: telegram.ext.PrefixHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.regexhandler.rst000066400000000000000000000002031362023133600255230ustar00rootroot00000000000000telegram.ext.RegexHandler ========================= .. autoclass:: telegram.ext.RegexHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.rst000066400000000000000000000017461362023133600230710ustar00rootroot00000000000000telegram.ext package ==================== .. toctree:: telegram.ext.updater telegram.ext.dispatcher telegram.ext.dispatcherhandlerstop telegram.ext.filters telegram.ext.job telegram.ext.jobqueue telegram.ext.messagequeue telegram.ext.delayqueue telegram.ext.callbackcontext telegram.ext.defaults Handlers -------- .. toctree:: telegram.ext.handler telegram.ext.callbackqueryhandler telegram.ext.choseninlineresulthandler telegram.ext.conversationhandler telegram.ext.commandhandler telegram.ext.inlinequeryhandler telegram.ext.messagehandler 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.dictpersistencepython-telegram-bot-12.4.2/docs/source/telegram.ext.shippingqueryhandler.rst000066400000000000000000000002331362023133600273230ustar00rootroot00000000000000telegram.ext.ShippingQueryHandler ================================= .. autoclass:: telegram.ext.ShippingQueryHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.stringcommandhandler.rst000066400000000000000000000002331362023133600272610ustar00rootroot00000000000000telegram.ext.StringCommandHandler ================================= .. autoclass:: telegram.ext.StringCommandHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.stringregexhandler.rst000066400000000000000000000002251362023133600267560ustar00rootroot00000000000000telegram.ext.StringRegexHandler =============================== .. autoclass:: telegram.ext.StringRegexHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.typehandler.rst000066400000000000000000000002001362023133600253670ustar00rootroot00000000000000telegram.ext.TypeHandler ======================== .. autoclass:: telegram.ext.TypeHandler :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.ext.updater.rst000066400000000000000000000001641362023133600245250ustar00rootroot00000000000000telegram.ext.Updater ==================== .. autoclass:: telegram.ext.Updater :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.file.rst000066400000000000000000000001371362023133600232010ustar00rootroot00000000000000telegram.File ============= .. autoclass:: telegram.File :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.filecredentials.rst000066400000000000000000000002001362023133600254060ustar00rootroot00000000000000telegram.FileCredentials ======================== .. autoclass:: telegram.FileCredentials :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.forcereply.rst000066400000000000000000000001611362023133600244310ustar00rootroot00000000000000telegram.ForceReply =================== .. autoclass:: telegram.ForceReply :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.game.rst000066400000000000000000000001371362023133600231730ustar00rootroot00000000000000telegram.Game ============= .. autoclass:: telegram.Game :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.gamehighscore.rst000066400000000000000000000001721362023133600250660ustar00rootroot00000000000000telegram.GameHighScore ====================== .. autoclass:: telegram.GameHighScore :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.iddocumentdata.rst000066400000000000000000000001751362023133600252510ustar00rootroot00000000000000telegram.IdDocumentData ======================= .. autoclass:: telegram.IdDocumentData :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinekeyboardbutton.rst000066400000000000000000000002171362023133600265140ustar00rootroot00000000000000telegram.InlineKeyboardButton ============================= .. autoclass:: telegram.InlineKeyboardButton :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinekeyboardmarkup.rst000066400000000000000000000002171362023133600265000ustar00rootroot00000000000000telegram.InlineKeyboardMarkup ============================= .. autoclass:: telegram.InlineKeyboardMarkup :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequery.rst000066400000000000000000000001641362023133600246260ustar00rootroot00000000000000telegram.InlineQuery ==================== .. autoclass:: telegram.InlineQuery :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresult.rst000066400000000000000000000002061362023133600260620ustar00rootroot00000000000000telegram.InlineQueryResult ========================== .. autoclass:: telegram.InlineQueryResult :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultarticle.rst000066400000000000000000000002331362023133600274260ustar00rootroot00000000000000telegram.InlineQueryResultArticle ================================= .. autoclass:: telegram.InlineQueryResultArticle :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultaudio.rst000066400000000000000000000002251362023133600271050ustar00rootroot00000000000000telegram.InlineQueryResultAudio =============================== .. autoclass:: telegram.InlineQueryResultAudio :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultcachedaudio.rst000066400000000000000000000002471362023133600302410ustar00rootroot00000000000000telegram.InlineQueryResultCachedAudio ===================================== .. autoclass:: telegram.InlineQueryResultCachedAudio :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultcacheddocument.rst000066400000000000000000000002601362023133600307510ustar00rootroot00000000000000telegram.InlineQueryResultCachedDocument ======================================== .. autoclass:: telegram.InlineQueryResultCachedDocument :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultcachedgif.rst000066400000000000000000000002411362023133600276770ustar00rootroot00000000000000telegram.InlineQueryResultCachedGif =================================== .. autoclass:: telegram.InlineQueryResultCachedGif :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultcachedmpeg4gif.rst000066400000000000000000000002601362023133600306350ustar00rootroot00000000000000telegram.InlineQueryResultCachedMpeg4Gif ======================================== .. autoclass:: telegram.InlineQueryResultCachedMpeg4Gif :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultcachedphoto.rst000066400000000000000000000002471362023133600302710ustar00rootroot00000000000000telegram.InlineQueryResultCachedPhoto ===================================== .. autoclass:: telegram.InlineQueryResultCachedPhoto :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultcachedsticker.rst000066400000000000000000000002551362023133600306030ustar00rootroot00000000000000telegram.InlineQueryResultCachedSticker ======================================= .. autoclass:: telegram.InlineQueryResultCachedSticker :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultcachedvideo.rst000066400000000000000000000002471362023133600302460ustar00rootroot00000000000000telegram.InlineQueryResultCachedVideo ===================================== .. autoclass:: telegram.InlineQueryResultCachedVideo :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultcachedvoice.rst000066400000000000000000000002471362023133600302450ustar00rootroot00000000000000telegram.InlineQueryResultCachedVoice ===================================== .. autoclass:: telegram.InlineQueryResultCachedVoice :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultcontact.rst000066400000000000000000000002331362023133600274360ustar00rootroot00000000000000telegram.InlineQueryResultContact ================================= .. autoclass:: telegram.InlineQueryResultContact :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultdocument.rst000066400000000000000000000002361362023133600276240ustar00rootroot00000000000000telegram.InlineQueryResultDocument ================================== .. autoclass:: telegram.InlineQueryResultDocument :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultgame.rst000066400000000000000000000002221362023133600267120ustar00rootroot00000000000000telegram.InlineQueryResultGame ============================== .. autoclass:: telegram.InlineQueryResultGame :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultgif.rst000066400000000000000000000002171362023133600265520ustar00rootroot00000000000000telegram.InlineQueryResultGif ============================= .. autoclass:: telegram.InlineQueryResultGif :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultlocation.rst000066400000000000000000000002361362023133600276160ustar00rootroot00000000000000telegram.InlineQueryResultLocation ================================== .. autoclass:: telegram.InlineQueryResultLocation :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultmpeg4gif.rst000066400000000000000000000002361362023133600275100ustar00rootroot00000000000000telegram.InlineQueryResultMpeg4Gif ================================== .. autoclass:: telegram.InlineQueryResultMpeg4Gif :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultphoto.rst000066400000000000000000000002251362023133600271350ustar00rootroot00000000000000telegram.InlineQueryResultPhoto =============================== .. autoclass:: telegram.InlineQueryResultPhoto :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultvenue.rst000066400000000000000000000002251362023133600271260ustar00rootroot00000000000000telegram.InlineQueryResultVenue =============================== .. autoclass:: telegram.InlineQueryResultVenue :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultvideo.rst000066400000000000000000000002251362023133600271120ustar00rootroot00000000000000telegram.InlineQueryResultVideo =============================== .. autoclass:: telegram.InlineQueryResultVideo :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inlinequeryresultvoice.rst000066400000000000000000000002251362023133600271110ustar00rootroot00000000000000telegram.InlineQueryResultVoice =============================== .. autoclass:: telegram.InlineQueryResultVoice :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputcontactmessagecontent.rst000066400000000000000000000002411362023133600277310ustar00rootroot00000000000000telegram.InputContactMessageContent =================================== .. autoclass:: telegram.InputContactMessageContent :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputfile.rst000066400000000000000000000001561362023133600242620ustar00rootroot00000000000000telegram.InputFile ================== .. autoclass:: telegram.InputFile :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputlocationmessagecontent.rst000066400000000000000000000002441362023133600301110ustar00rootroot00000000000000telegram.InputLocationMessageContent ==================================== .. autoclass:: telegram.InputLocationMessageContent :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputmedia.rst000066400000000000000000000001611362023133600244160ustar00rootroot00000000000000telegram.InputMedia =================== .. autoclass:: telegram.InputMedia :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputmediaanimation.rst000066400000000000000000000002141362023133600263150ustar00rootroot00000000000000telegram.InputMediaAnimation ============================ .. autoclass:: telegram.InputMediaAnimation :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputmediaaudio.rst000066400000000000000000000002001362023133600254320ustar00rootroot00000000000000telegram.InputMediaAudio ======================== .. autoclass:: telegram.InputMediaAudio :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputmediadocument.rst000066400000000000000000000002111362023133600261510ustar00rootroot00000000000000telegram.InputMediaDocument =========================== .. autoclass:: telegram.InputMediaDocument :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputmediaphoto.rst000066400000000000000000000002001362023133600254620ustar00rootroot00000000000000telegram.InputMediaPhoto ======================== .. autoclass:: telegram.InputMediaPhoto :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputmediavideo.rst000066400000000000000000000002001362023133600254370ustar00rootroot00000000000000telegram.InputMediaVideo ======================== .. autoclass:: telegram.InputMediaVideo :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputmessagecontent.rst000066400000000000000000000002141362023133600263550ustar00rootroot00000000000000telegram.InputMessageContent ============================ .. autoclass:: telegram.InputMessageContent :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputtextmessagecontent.rst000066400000000000000000000002301362023133600272600ustar00rootroot00000000000000telegram.InputTextMessageContent ================================ .. autoclass:: telegram.InputTextMessageContent :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.inputvenuemessagecontent.rst000066400000000000000000000002331362023133600274210ustar00rootroot00000000000000telegram.InputVenueMessageContent ================================= .. autoclass:: telegram.InputVenueMessageContent :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.invoice.rst000066400000000000000000000001501362023133600237110ustar00rootroot00000000000000telegram.Invoice ================ .. autoclass:: telegram.Invoice :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.keyboardbutton.rst000066400000000000000000000001751362023133600253200ustar00rootroot00000000000000telegram.KeyboardButton ======================= .. autoclass:: telegram.KeyboardButton :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.labeledprice.rst000066400000000000000000000001671362023133600247000ustar00rootroot00000000000000telegram.LabeledPrice ===================== .. autoclass:: telegram.LabeledPrice :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.location.rst000066400000000000000000000001531362023133600240700ustar00rootroot00000000000000telegram.Location ================= .. autoclass:: telegram.Location :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.loginurl.rst000066400000000000000000000001531362023133600241130ustar00rootroot00000000000000telegram.LoginUrl ================= .. autoclass:: telegram.LoginUrl :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.maskposition.rst000066400000000000000000000001671362023133600250050ustar00rootroot00000000000000telegram.MaskPosition ===================== .. autoclass:: telegram.MaskPosition :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.message.rst000066400000000000000000000001501362023133600237010ustar00rootroot00000000000000telegram.Message ================ .. autoclass:: telegram.Message :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.messageentity.rst000066400000000000000000000001721362023133600251420ustar00rootroot00000000000000telegram.MessageEntity ====================== .. autoclass:: telegram.MessageEntity :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.orderinfo.rst000066400000000000000000000001561362023133600242520ustar00rootroot00000000000000telegram.OrderInfo ================== .. autoclass:: telegram.OrderInfo :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.parsemode.rst000066400000000000000000000001561362023133600242420ustar00rootroot00000000000000telegram.ParseMode ================== .. autoclass:: telegram.ParseMode :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.passportdata.rst000066400000000000000000000001671362023133600247720ustar00rootroot00000000000000telegram.PassportData ===================== .. autoclass:: telegram.PassportData :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.passportelementerror.rst000066400000000000000000000002171362023133600265600ustar00rootroot00000000000000telegram.PassportElementError ============================= .. autoclass:: telegram.PassportElementError :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.passportelementerrordatafield.rst000066400000000000000000000002521362023133600304150ustar00rootroot00000000000000telegram.PassportElementErrorDataField ====================================== .. autoclass:: telegram.PassportElementErrorDataField :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.passportelementerrorfile.rst000066400000000000000000000002331362023133600274160ustar00rootroot00000000000000telegram.PassportElementErrorFile ================================= .. autoclass:: telegram.PassportElementErrorFile :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.passportelementerrorfiles.rst000066400000000000000000000002361362023133600276040ustar00rootroot00000000000000telegram.PassportElementErrorFiles ================================== .. autoclass:: telegram.PassportElementErrorFiles :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.passportelementerrorfrontside.rst000066400000000000000000000002521362023133600304750ustar00rootroot00000000000000telegram.PassportElementErrorFrontSide ====================================== .. autoclass:: telegram.PassportElementErrorFrontSide :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.passportelementerrorreverseside.rst000066400000000000000000000002601362023133600310170ustar00rootroot00000000000000telegram.PassportElementErrorReverseSide ======================================== .. autoclass:: telegram.PassportElementErrorReverseSide :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.passportfile.rst000066400000000000000000000001671362023133600250000ustar00rootroot00000000000000telegram.PassportFile ===================== .. autoclass:: telegram.PassportFile :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.personaldetails.rst000066400000000000000000000002001362023133600254420ustar00rootroot00000000000000telegram.PersonalDetails ======================== .. autoclass:: telegram.PersonalDetails :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.photosize.rst000066400000000000000000000001561362023133600243070ustar00rootroot00000000000000telegram.PhotoSize ================== .. autoclass:: telegram.PhotoSize :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.poll.rst000066400000000000000000000001371362023133600232300ustar00rootroot00000000000000telegram.Poll ============= .. autoclass:: telegram.Poll :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.polloption.rst000066400000000000000000000001611362023133600244560ustar00rootroot00000000000000telegram.PollOption =================== .. autoclass:: telegram.PollOption :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.precheckoutquery.rst000066400000000000000000000002031362023133600256560ustar00rootroot00000000000000telegram.PreCheckoutQuery ========================= .. autoclass:: telegram.PreCheckoutQuery :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.replykeyboardmarkup.rst000066400000000000000000000002141362023133600263520ustar00rootroot00000000000000telegram.ReplyKeyboardMarkup ============================ .. autoclass:: telegram.ReplyKeyboardMarkup :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.replykeyboardremove.rst000066400000000000000000000002141362023133600263500ustar00rootroot00000000000000telegram.ReplyKeyboardRemove ============================ .. autoclass:: telegram.ReplyKeyboardRemove :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.replymarkup.rst000066400000000000000000000001641362023133600246350ustar00rootroot00000000000000telegram.ReplyMarkup ==================== .. autoclass:: telegram.ReplyMarkup :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.residentialaddress.rst000066400000000000000000000002111362023133600261240ustar00rootroot00000000000000telegram.ResidentialAddress =========================== .. autoclass:: telegram.ResidentialAddress :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.rst000066400000000000000000000065111362023133600222650ustar00rootroot00000000000000.. include:: telegram.ext.rst telegram package ================ .. toctree:: telegram.animation telegram.audio telegram.bot telegram.callbackquery telegram.chat telegram.chataction telegram.chatmember telegram.chatpermissions telegram.chatphoto telegram.constants telegram.contact 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.location telegram.loginurl telegram.message telegram.messageentity telegram.parsemode telegram.photosize telegram.poll telegram.polloption telegram.replykeyboardremove telegram.replykeyboardmarkup telegram.replymarkup telegram.telegramobject telegram.update telegram.user telegram.userprofilephotos telegram.venue telegram.video telegram.videonote telegram.voice 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.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.passportelementerrorreverseside telegram.passportelementerrorfrontside telegram.passportelementerrorfiles telegram.passportelementerrordatafield telegram.passportelementerrorfile telegram.credentials telegram.datacredentials telegram.securedata telegram.filecredentials telegram.iddocumentdata telegram.personaldetails telegram.residentialaddress telegram.passportdata telegram.passportfile telegram.encryptedpassportelement telegram.encryptedcredentials .. include:: telegram.utils.rst python-telegram-bot-12.4.2/docs/source/telegram.securedata.rst000066400000000000000000000001611362023133600243770ustar00rootroot00000000000000telegram.SecureData =================== .. autoclass:: telegram.SecureData :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.shippingaddress.rst000066400000000000000000000002001362023133600254400ustar00rootroot00000000000000telegram.ShippingAddress ======================== .. autoclass:: telegram.ShippingAddress :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.shippingoption.rst000066400000000000000000000001751362023133600253360ustar00rootroot00000000000000telegram.ShippingOption ======================= .. autoclass:: telegram.ShippingOption :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.shippingquery.rst000066400000000000000000000001721362023133600251700ustar00rootroot00000000000000telegram.ShippingQuery ====================== .. autoclass:: telegram.ShippingQuery :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.sticker.rst000066400000000000000000000001501362023133600237210ustar00rootroot00000000000000telegram.Sticker ================ .. autoclass:: telegram.Sticker :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.stickerset.rst000066400000000000000000000001611362023133600244370ustar00rootroot00000000000000telegram.StickerSet =================== .. autoclass:: telegram.StickerSet :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.successfulpayment.rst000066400000000000000000000002061362023133600260340ustar00rootroot00000000000000telegram.SuccessfulPayment ========================== .. autoclass:: telegram.SuccessfulPayment :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.telegramobject.rst000066400000000000000000000001751362023133600252530ustar00rootroot00000000000000telegram.TelegramObject ======================= .. autoclass:: telegram.TelegramObject :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.update.rst000066400000000000000000000001711362023133600235420ustar00rootroot00000000000000telegram.Update =============== .. autoclass:: telegram.Update :members: :undoc-members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.user.rst000066400000000000000000000001631362023133600232370ustar00rootroot00000000000000telegram.User ============= .. autoclass:: telegram.User :members: :undoc-members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.userprofilephotos.rst000066400000000000000000000002061362023133600260530ustar00rootroot00000000000000telegram.UserProfilePhotos ========================== .. autoclass:: telegram.UserProfilePhotos :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.utils.helpers.rst000066400000000000000000000002111362023133600250540ustar00rootroot00000000000000telegram.utils.helpers Module ============================= .. automodule:: telegram.utils.helpers :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.utils.promise.rst000066400000000000000000000002221362023133600250720ustar00rootroot00000000000000telegram.utils.promise.Promise ============================== .. autoclass:: telegram.utils.promise.Promise :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.utils.request.rst000066400000000000000000000002221362023133600251040ustar00rootroot00000000000000telegram.utils.request.Request ============================== .. autoclass:: telegram.utils.request.Request :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.utils.rst000066400000000000000000000002161362023133600234200ustar00rootroot00000000000000telegram.utils package ====================== .. toctree:: telegram.utils.helpers telegram.utils.promise telegram.utils.request python-telegram-bot-12.4.2/docs/source/telegram.venue.rst000066400000000000000000000001421362023133600234000ustar00rootroot00000000000000telegram.Venue ============== .. autoclass:: telegram.Venue :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.video.rst000066400000000000000000000001421362023133600233640ustar00rootroot00000000000000telegram.Video ============== .. autoclass:: telegram.Video :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.videonote.rst000066400000000000000000000001561362023133600242570ustar00rootroot00000000000000telegram.VideoNote ================== .. autoclass:: telegram.VideoNote :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.voice.rst000066400000000000000000000001421362023133600233630ustar00rootroot00000000000000telegram.Voice ============== .. autoclass:: telegram.Voice :members: :show-inheritance: python-telegram-bot-12.4.2/docs/source/telegram.webhookinfo.rst000066400000000000000000000001641362023133600245740ustar00rootroot00000000000000telegram.WebhookInfo ==================== .. autoclass:: telegram.WebhookInfo :members: :show-inheritance: python-telegram-bot-12.4.2/examples/000077500000000000000000000000001362023133600173165ustar00rootroot00000000000000python-telegram-bot-12.4.2/examples/LICENSE.txt000066400000000000000000000146331362023133600211500ustar00rootroot00000000000000CC0 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-12.4.2/examples/README.md000066400000000000000000000111151362023133600205740ustar00rootroot00000000000000# 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 [`echobot.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. ### [`echobot2.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot2.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! ### [`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. ### [`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. ### [`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). ### [`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). ### [`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. ## Pure API The [`echobot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py) example uses only the pure, "bare-metal" API wrapper. python-telegram-bot-12.4.2/examples/conversationbot.png000066400000000000000000002445541362023133600232610ustar00rootroot00000000000000PNG  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-12.4.2/examples/conversationbot.py000066400000000000000000000121401362023133600231050ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # 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) from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters, ConversationHandler) # 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, context): 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)) return GENDER def gender(update, context): 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, context): 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, context): 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, context): 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, context): 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, context): 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, context): 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 error(update, context): """Log Errors caused by Updates.""" logger.warning('Update "%s" caused error "%s"', update, context.error) def main(): # Create the Updater and pass it your bot's token. # Make sure to set use_context=True to use the new context based callbacks # Post version 12 this will no longer be necessary updater = Updater("TOKEN", use_context=True) # Get the dispatcher to register handlers dp = 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, bio)] }, fallbacks=[CommandHandler('cancel', cancel)] ) dp.add_handler(conv_handler) # log all errors dp.add_error_handler(error) # 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-12.4.2/examples/conversationbot2.png000066400000000000000000002431161362023133600233340ustar00rootroot00000000000000PNG  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䓙}.^~۷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-12.4.2/examples/nestedconversationbot.py000066400000000000000000000302221362023133600243110ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # 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 telegram import (InlineKeyboardMarkup, InlineKeyboardButton) from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters, ConversationHandler, CallbackQueryHandler) # 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): if level == PARENTS: return ('Father', 'Mother') elif level == CHILDREN: return ('Brother', 'Sister') # Top level conversation callbacks def start(update, context): """Select an action: Adding parent/child or show data.""" text = 'You may add a familiy 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 do send a new message if context.user_data.get(START_OVER): update.callback_query.edit_message_text(text=text, reply_markup=keyboard) else: update.message.reply_text('Hi, I\'m FamiliyBot and 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, context): """Add information about youself.""" 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.edit_message_text(text=text, reply_markup=keyboard) return DESCRIBING_SELF def show_data(update, context): """Pretty print gathered data.""" def prettyprint(user_data, level): people = user_data.get(level) if not people: return '\nNo information yet.' text = '' if level == SELF: for person in user_data[level]: text += '\nName: {0}, Age: {1}'.format(person.get(NAME, '-'), person.get(AGE, '-')) else: male, female = _name_switcher(level) for person in user_data[level]: gender = female if person[GENDER] == FEMALE else male text += '\n{0}: Name: {1}, Age: {2}'.format(gender, person.get(NAME, '-'), person.get(AGE, '-')) return text ud = context.user_data text = 'Yourself:' + prettyprint(ud, SELF) text += '\n\nParents:' + prettyprint(ud, PARENTS) text += '\n\nChildren:' + prettyprint(ud, CHILDREN) buttons = [[ InlineKeyboardButton(text='Back', callback_data=str(END)) ]] keyboard = InlineKeyboardMarkup(buttons) update.callback_query.edit_message_text(text=text, reply_markup=keyboard) ud[START_OVER] = True return SHOWING def stop(update, context): """End Conversation by command.""" update.message.reply_text('Okay, bye.') return END def end(update, context): """End conversation from InlineKeyboardButton.""" text = 'See you around!' update.callback_query.edit_message_text(text=text) return END # Second level conversation callbacks def select_level(update, context): """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.edit_message_text(text=text, reply_markup=keyboard) return SELECTING_LEVEL def select_gender(update, context): """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='Add ' + male, callback_data=str(MALE)), InlineKeyboardButton(text='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.edit_message_text(text=text, reply_markup=keyboard) return SELECTING_GENDER def end_second_level(update, context): """Return to top level conversation.""" context.user_data[START_OVER] = True start(update, context) return END # Third level callbacks def select_feature(update, context): """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.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, context): """Prompt user to input data for selected feature.""" context.user_data[CURRENT_FEATURE] = update.callback_query.data text = 'Okay, tell me.' update.callback_query.edit_message_text(text=text) return TYPING def save_input(update, context): """Save input for feature and return to feature selection.""" ud = context.user_data ud[FEATURES][ud[CURRENT_FEATURE]] = update.message.text ud[START_OVER] = True return select_feature(update, context) def end_describing(update, context): """End gathering of features and return to parent conversation.""" ud = context.user_data level = ud[CURRENT_LEVEL] if not ud.get(level): ud[level] = [] ud[level].append(ud[FEATURES]) # Print upper level menu if level == SELF: ud[START_OVER] = True start(update, context) else: select_level(update, context) return END def stop_nested(update, context): """Completely end conversation from within nested conversation.""" update.message.reply_text('Okay, bye.') return STOPPING # Error handler def error(update, context): """Log Errors caused by Updates.""" logger.warning('Update "%s" caused error "%s"', update, context.error) def main(): # Create the Updater and pass it your bot's token. # Make sure to set use_context=True to use the new context based callbacks # Post version 12 this will no longer be necessary updater = Updater("TOKEN", use_context=True) # Get the dispatcher to register handlers dp = 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, 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 alltogether 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='^{0}$|^{1}$'.format(str(PARENTS), str(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 alltogether STOPPING: END, } ) # Set up top level ConversationHandler (selecting action) conv_handler = ConversationHandler( entry_points=[CommandHandler('start', start)], states={ SHOWING: [CallbackQueryHandler(start, pattern='^' + str(END) + '$')], SELECTING_ACTION: [ add_member_conv, CallbackQueryHandler(show_data, pattern='^' + str(SHOWING) + '$'), CallbackQueryHandler(adding_self, pattern='^' + str(ADDING_SELF) + '$'), CallbackQueryHandler(end, pattern='^' + str(END) + '$'), ], DESCRIBING_SELF: [description_conv], }, fallbacks=[CommandHandler('stop', stop)], ) # Because the states of the third level conversation map to the ones of the # second level conversation, we need to be a bit hacky about that: conv_handler.states[SELECTING_LEVEL] = conv_handler.states[SELECTING_ACTION] conv_handler.states[STOPPING] = conv_handler.entry_points dp.add_handler(conv_handler) # log all errors dp.add_error_handler(error) # 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-12.4.2/examples/passportbot.html000066400000000000000000000021131362023133600225610ustar00rootroot00000000000000 Telegram passport test!

Telegram passport test

python-telegram-bot-12.4.2/examples/passportbot.py000066400000000000000000000103011362023133600222430ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # 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.ext import Updater, MessageHandler, Filters # Enable logging logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG) logger = logging.getLogger(__name__) def msg(update, context): # If we received any passport data passport_data = update.message.passport_data if 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'): if data.front_side: file = data.front_side.get_file() print(data.type, file) file.download() if data.type in ('driver_license' and 'identity_card'): if data.reverse_side: file = data.reverse_side.get_file() print(data.type, file) file.download() if data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport'): if data.selfie: file = data.selfie.get_file() print(data.type, file) 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 error(update, context): """Log Errors caused by Updates.""" logger.warning('Update "%s" caused error "%s"', update, context.error) def main(): """Start the bot.""" # Create the Updater and pass it your token and private key updater = Updater("TOKEN", private_key=open('private.key', 'rb').read()) # Get the dispatcher to register handlers dp = updater.dispatcher # On messages that include passport data call msg dp.add_handler(MessageHandler(Filters.passport_data, msg)) # log all errors dp.add_error_handler(error) # 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-12.4.2/examples/paymentbot.py000066400000000000000000000130501362023133600220510ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # 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) from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters, PreCheckoutQueryHandler, ShippingQueryHandler) # Enable logging logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO) logger = logging.getLogger(__name__) def error(update, context): """Log Errors caused by Updates.""" logger.warning('Update "%s" caused error "%s"', update, context.error) def start_callback(update, context): msg = "Use /shipping to get an invoice for shipping-payment, " msg += "or /noshipping for an invoice without shipping." update.message.reply_text(msg) def start_with_shipping_callback(update, context): 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" start_parameter = "test-payment" 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, start_parameter, 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, context): 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" start_parameter = "test-payment" 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, start_parameter, currency, prices) def shipping_callback(update, context): 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 else: options = list() # a single LabeledPrice options.append(ShippingOption('1', 'Shipping Option A', [LabeledPrice('A', 100)])) # 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, context): 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, context): # do something after successfully receiving payment? update.message.reply_text("Thank you for your payment!") def main(): # Create the Updater and pass it your bot's token. # Make sure to set use_context=True to use the new context based callbacks # Post version 12 this will no longer be necessary updater = Updater("TOKEN", use_context=True) # Get the dispatcher to register handlers dp = updater.dispatcher # simple start function dp.add_handler(CommandHandler("start", start_callback)) # Add command handler to start the payment invoice dp.add_handler(CommandHandler("shipping", start_with_shipping_callback)) dp.add_handler(CommandHandler("noshipping", start_without_shipping_callback)) # Optional handler if your product requires shipping dp.add_handler(ShippingQueryHandler(shipping_callback)) # Pre-checkout handler to final check dp.add_handler(PreCheckoutQueryHandler(precheckout_callback)) # Success! Notify your user! dp.add_handler(MessageHandler(Filters.successful_payment, successful_payment_callback)) # log all errors dp.add_error_handler(error) # 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-12.4.2/examples/persistentconversationbot.py000066400000000000000000000127001362023133600252300ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # 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. """ from telegram import ReplyKeyboardMarkup from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters, ConversationHandler, PicklePersistence) import logging # 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): facts = list() for key, value in user_data.items(): facts.append('{} - {}'.format(key, value)) return "\n".join(facts).join(['\n', '\n']) def start(update, context): reply_text = "Hi! My name is Doctor Botter." if context.user_data: reply_text += " You already told me your {}. Why don't you tell me something more " \ "about yourself? Or change anything I " \ "already know.".format(", ".join(context.user_data.keys())) 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, context): text = update.message.text.lower() context.user_data['choice'] = text if context.user_data.get(text): reply_text = 'Your {}, I already know the following ' \ 'about that: {}'.format(text, context.user_data[text]) else: reply_text = 'Your {}? Yes, I would love to hear about that!'.format(text) update.message.reply_text(reply_text) return TYPING_REPLY def custom_choice(update, context): update.message.reply_text('Alright, please send me the category first, ' 'for example "Most impressive skill"') return TYPING_CHOICE def received_information(update, context): 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:" "{}" "You can tell me more, or change your opinion on " "something.".format(facts_to_str(context.user_data)), reply_markup=markup) return CHOOSING def show_data(update, context): update.message.reply_text("This is what you already told me:" "{}".format(facts_to_str(context.user_data))) def done(update, context): if 'choice' in context.user_data: del context.user_data['choice'] update.message.reply_text("I learned these facts about you:" "{}" "Until next time!".format(facts_to_str(context.user_data))) return ConversationHandler.END def error(update, context): """Log Errors caused by Updates.""" logger.warning('Update "%s" caused error "%s"', update, context.error) def main(): # Create the Updater and pass it your bot's token. pp = PicklePersistence(filename='conversationbot') updater = Updater("TOKEN", persistence=pp, use_context=True) # Get the dispatcher to register handlers dp = 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, regular_choice), ], TYPING_REPLY: [MessageHandler(Filters.text, received_information), ], }, fallbacks=[MessageHandler(Filters.regex('^Done$'), done)], name="my_conversation", persistent=True ) dp.add_handler(conv_handler) show_data_handler = CommandHandler('show_data', show_data) dp.add_handler(show_data_handler) # log all errors dp.add_error_handler(error) # 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-12.4.2/examples/timerbot.py000066400000000000000000000072321362023133600215210ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # 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.ext import Updater, CommandHandler # 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, context): update.message.reply_text('Hi! Use /set to set a timer') def alarm(context): """Send the alarm message.""" job = context.job context.bot.send_message(job.context, text='Beep!') def set_timer(update, context): """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 # Add job to queue and stop current one if there is a timer already if 'job' in context.chat_data: old_job = context.chat_data['job'] old_job.schedule_removal() new_job = context.job_queue.run_once(alarm, due, context=chat_id) context.chat_data['job'] = new_job update.message.reply_text('Timer successfully set!') except (IndexError, ValueError): update.message.reply_text('Usage: /set ') def unset(update, context): """Remove the job if the user changed their mind.""" if 'job' not in context.chat_data: update.message.reply_text('You have no active timer') return job = context.chat_data['job'] job.schedule_removal() del context.chat_data['job'] update.message.reply_text('Timer successfully unset!') def error(update, context): """Log Errors caused by Updates.""" logger.warning('Update "%s" caused error "%s"', update, context.error) def main(): """Run bot.""" # Create the Updater and pass it your bot's token. # Make sure to set use_context=True to use the new context based callbacks # Post version 12 this will no longer be necessary updater = Updater("TOKEN", use_context=True) # Get the dispatcher to register handlers dp = updater.dispatcher # on different commands - answer in Telegram dp.add_handler(CommandHandler("start", start)) dp.add_handler(CommandHandler("help", start)) dp.add_handler(CommandHandler("set", set_timer, pass_args=True, pass_job_queue=True, pass_chat_data=True)) dp.add_handler(CommandHandler("unset", unset, pass_chat_data=True)) # log all errors dp.add_error_handler(error) # 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-12.4.2/requirements-dev.txt000066400000000000000000000001531362023133600215370ustar00rootroot00000000000000flake8 pep257 pylint flaky yapf pre-commit beautifulsoup4 pytest==4.2.0 pytest-timeout wheel attrs==19.1.0 python-telegram-bot-12.4.2/requirements.txt000066400000000000000000000001021362023133600207550ustar00rootroot00000000000000future>=0.16.0 certifi tornado>=5.1 cryptography decorator>=4.4.0 python-telegram-bot-12.4.2/setup.cfg000066400000000000000000000013331362023133600173210ustar00rootroot00000000000000[wheel] universal = 1 [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 exclude = setup.py, docs/source/conf.py [yapf] based_on_style = google split_before_logical_operator = True column_limit = 99 [tool:pytest] testpaths = tests addopts = --no-success-flaky-report -rsxX filterwarnings = error ignore::DeprecationWarning ignore::telegram.utils.deprecate.TelegramDeprecationWarning [coverage:run] branch = True source = telegram parallel = True concurrency = thread, multiprocessing omit = tests/ telegram/__main__.py telegram/vendor/* python-telegram-bot-12.4.2/setup.py000066400000000000000000000045041362023133600172150ustar00rootroot00000000000000#!/usr/bin/env python """The setup and build script for the python-telegram-bot library.""" import codecs import os import sys from setuptools import setup, find_packages def requirements(): """Build the requirements list for this project""" requirements_list = [] with open('requirements.txt') as requirements: for install in requirements: requirements_list.append(install.strip()) return requirements_list packages = find_packages(exclude=['tests*']) requirements = requirements() # Allow for a package install to not use the vendored urllib3 UPSTREAM_URLLIB3_FLAG = '--with-upstream-urllib3' if UPSTREAM_URLLIB3_FLAG in sys.argv: sys.argv.remove(UPSTREAM_URLLIB3_FLAG) requirements.append('urllib3 >= 1.19.1') packages = [x for x in packages if not x.startswith('telegram.vendor.ptb_urllib3')] with codecs.open('README.rst', 'r', 'utf-8') as fd: fn = os.path.join('telegram', 'version.py') with open(fn) as fh: code = compile(fh.read(), fn, 'exec') exec(code) setup(name='python-telegram-bot', version=__version__, author='Leandro Toledo', author_email='devs@python-telegram-bot.org', license='LGPLv3', url='https://python-telegram-bot.org/', keywords='python telegram bot api wrapper', description="We have made you a wrapper you can't refuse", long_description=fd.read(), packages=packages, install_requires=requirements, extras_require={ 'json': 'ujson', 'socks': 'PySocks' }, 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.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7' ],) python-telegram-bot-12.4.2/telegram/000077500000000000000000000000001362023133600173005ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/__init__.py000066400000000000000000000220771362023133600214210ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 .user import User from .files.chatphoto import ChatPhoto from .chat import Chat from .chatmember import ChatMember 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 .userprofilephotos import UserProfilePhotos 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 .games.game import Game from .poll import Poll, PollOption from .loginurl import LoginUrl 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 .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 .inline.inputcontactmessagecontent import InputContactMessageContent from .payment.labeledprice import LabeledPrice 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 .bot import Bot 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, FileCredentials, TelegramDecryptionError) from .version import __version__ # noqa: F401 __author__ = 'devs@python-telegram-bot.org' __all__ = [ 'Audio', 'Bot', 'Chat', 'ChatMember', 'ChatPermissions', 'ChatAction', 'ChosenInlineResult', 'CallbackQuery', 'Contact', 'Document', 'File', 'ForceReply', 'InlineKeyboardButton', 'InlineKeyboardMarkup', 'InlineQuery', 'InlineQueryResult', 'InlineQueryResult', 'InlineQueryResultArticle', 'InlineQueryResultAudio', 'InlineQueryResultCachedAudio', 'InlineQueryResultCachedDocument', 'InlineQueryResultCachedGif', 'InlineQueryResultCachedMpeg4Gif', 'InlineQueryResultCachedPhoto', 'InlineQueryResultCachedSticker', 'InlineQueryResultCachedVideo', 'InlineQueryResultCachedVoice', 'InlineQueryResultContact', 'InlineQueryResultDocument', 'InlineQueryResultGif', 'InlineQueryResultLocation', 'InlineQueryResultMpeg4Gif', 'InlineQueryResultPhoto', 'InlineQueryResultVenue', 'InlineQueryResultVideo', 'InlineQueryResultVoice', 'InlineQueryResultGame', 'InputContactMessageContent', 'InputFile', 'InputLocationMessageContent', 'InputMessageContent', 'InputTextMessageContent', 'InputVenueMessageContent', 'KeyboardButton', 'Location', 'EncryptedCredentials', 'PassportFile', 'EncryptedPassportElement', 'PassportData', 'Message', 'MessageEntity', 'ParseMode', 'PhotoSize', 'ReplyKeyboardRemove', 'ReplyKeyboardMarkup', 'ReplyMarkup', 'Sticker', 'TelegramError', 'TelegramObject', 'Update', 'User', 'UserProfilePhotos', 'Venue', 'Video', 'Voice', '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', 'WebhookInfo', 'Animation', 'Game', 'GameHighScore', 'VideoNote', 'LabeledPrice', 'SuccessfulPayment', 'ShippingOption', 'ShippingAddress', 'PreCheckoutQuery', 'OrderInfo', 'Invoice', 'ShippingQuery', 'ChatPhoto', 'StickerSet', 'MaskPosition', 'CallbackGame', 'InputMedia', 'InputMediaPhoto', 'InputMediaVideo', 'PassportElementError', 'PassportElementErrorFile', 'PassportElementErrorReverseSide', 'PassportElementErrorFrontSide', 'PassportElementErrorFiles', 'PassportElementErrorDataField', 'PassportElementErrorFile', 'Credentials', 'DataCredentials', 'SecureData', 'FileCredentials', 'IdDocumentData', 'PersonalDetails', 'ResidentialAddress', 'InputMediaVideo', 'InputMediaAnimation', 'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError', 'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile', 'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified', 'Poll', 'PollOption', 'LoginUrl' ] python-telegram-bot-12.4.2/telegram/__main__.py000066400000000000000000000032141362023133600213720ustar00rootroot00000000000000# !/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import sys import subprocess import certifi import future from . import __version__ as telegram_ver def _git_revision(): try: output = subprocess.check_output(["git", "describe", "--long", "--tags"], stderr=subprocess.STDOUT) except (subprocess.SubprocessError, OSError): return None return output.decode().strip() def print_ver_info(): git_revision = _git_revision() print('python-telegram-bot {0}'.format(telegram_ver) + (' ({0})'.format(git_revision) if git_revision else '')) print('certifi {0}'.format(certifi.__version__)) print('future {0}'.format(future.__version__)) print('Python {0}'.format(sys.version.replace('\n', ' '))) def main(): print_ver_info() if __name__ == '__main__': main() python-telegram-bot-12.4.2/telegram/base.py000066400000000000000000000050141362023133600205640ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 from abc import ABCMeta class TelegramObject(object): """Base class for most telegram objects.""" __metaclass__ = ABCMeta _id_attrs = () def __str__(self): return str(self.to_dict()) def __getitem__(self, item): return self.__dict__[item] @classmethod def de_json(cls, data, bot): if not data: return None data = data.copy() return data def to_json(self): """ Returns: :obj:`str` """ return json.dumps(self.to_dict()) def to_dict(self): data = dict() for key in iter(self.__dict__): if key in ('bot', '_id_attrs', '_credentials', '_decrypted_credentials', '_decrypted_data', '_decrypted_secret'): continue value = self.__dict__[key] 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): if isinstance(other, self.__class__): return self._id_attrs == other._id_attrs return super(TelegramObject, self).__eq__(other) # pylint: disable=no-member def __hash__(self): if self._id_attrs: return hash((self.__class__, self._id_attrs)) # pylint: disable=no-member return super(TelegramObject, self).__hash__() python-telegram-bot-12.4.2/telegram/bot.py000066400000000000000000004751321362023133600204520ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # pylint: disable=E0611,E0213,E1102,C0103,E1101,W0613,R0913,R0904 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 inspect from decorator import decorate try: import ujson as json except ImportError: import json import logging import warnings from datetime import datetime from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from future.utils import string_types from telegram import (User, Message, Update, Chat, ChatMember, UserProfilePhotos, File, ReplyMarkup, TelegramObject, WebhookInfo, GameHighScore, StickerSet, PhotoSize, Audio, Document, Sticker, Video, Animation, Voice, VideoNote, Location, Venue, Contact, InputFile, Poll) from telegram.error import InvalidToken, TelegramError from telegram.utils.helpers import to_timestamp, DEFAULT_NONE from telegram.utils.request import Request logging.getLogger(__name__).addHandler(logging.NullHandler()) def info(func): @functools.wraps(func) def decorator(self, *args, **kwargs): if not self.bot: self.get_me() result = func(self, *args, **kwargs) return result return decorator def log(func, *args, **kwargs): logger = logging.getLogger(func.__module__) def decorator(self, *args, **kwargs): logger.debug('Entering: %s', func.__name__) result = func(*args, **kwargs) logger.debug(result) logger.debug('Exiting: %s', func.__name__) return result return decorate(func, decorator) class Bot(TelegramObject): """This object represents a Telegram Bot. 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. """ def __new__(cls, *args, **kwargs): # Get default values from kwargs defaults = kwargs.get('defaults') # Make an instance of the class instance = super(Bot, cls).__new__(cls) if not defaults: return instance # For each method ... for method_name, method in inspect.getmembers(instance, predicate=inspect.ismethod): # ... get kwargs argspec = inspect.getargspec(method) kwarg_names = argspec.args[-len(argspec.defaults or []):] # ... check if Defaults has a attribute that matches the kwarg name needs_default = [ kwarg_name for kwarg_name in kwarg_names if hasattr(defaults, kwarg_name) ] # ... make a dict of kwarg name and the default value default_kwargs = { kwarg_name: getattr(defaults, kwarg_name) for kwarg_name in needs_default if ( getattr(defaults, kwarg_name) is not DEFAULT_NONE ) } # ... apply the defaults using a partial if default_kwargs: setattr(instance, method_name, functools.partial(method, **default_kwargs)) return instance def __init__(self, token, base_url=None, base_file_url=None, request=None, private_key=None, private_key_password=None, defaults=None): self.token = self._validate_token(token) # Gather default self.defaults = defaults 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 = None self._request = request or Request() self.logger = logging.getLogger(__name__) if private_key: self.private_key = serialization.load_pem_private_key(private_key, password=private_key_password, backend=default_backend()) def _message(self, url, data, reply_to_message_id=None, disable_notification=None, reply_markup=None, timeout=None, **kwargs): if reply_to_message_id is not None: data['reply_to_message_id'] = reply_to_message_id if disable_notification is not None: data['disable_notification'] = disable_notification if reply_markup is not None: if isinstance(reply_markup, ReplyMarkup): 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 = self.defaults.parse_mode else: data['media'].parse_mode = None result = self._request.post(url, data, timeout=timeout) if result is True: return result if self.defaults: result['default_quote'] = self.defaults.quote return Message.de_json(result, self) @property def request(self): return self._request @staticmethod def _validate_token(token): """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 @info def id(self): """:obj:`int`: Unique identifier for this bot.""" return self.bot.id @property @info def first_name(self): """:obj:`str`: Bot's first name.""" return self.bot.first_name @property @info def last_name(self): """:obj:`str`: Optional. Bot's last name.""" return self.bot.last_name @property @info def username(self): """:obj:`str`: Bot's username.""" return self.bot.username @property def name(self): """:obj:`str`: Bot's @username.""" return '@{0}'.format(self.username) @log def get_me(self, timeout=None, **kwargs): """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). Returns: :class:`telegram.User`: A :class:`telegram.User` instance representing that bot if the credentials are valid, :obj:`None` otherwise. Raises: :class:`telegram.TelegramError` """ url = '{0}/getMe'.format(self.base_url) result = self._request.get(url, timeout=timeout) self.bot = User.de_json(result, self) return self.bot @log def send_message(self, chat_id, text, parse_mode=None, disable_web_page_preview=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=None, **kwargs): """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. 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. 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. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendMessage'.format(self.base_url) data = {'chat_id': chat_id, 'text': text} if parse_mode: data['parse_mode'] = parse_mode if disable_web_page_preview: data['disable_web_page_preview'] = disable_web_page_preview return self._message(url, data, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, timeout=timeout, **kwargs) @log def delete_message(self, chat_id, message_id, timeout=None, **kwargs): """ 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. - Bots can delete outgoing messages in private chats, groups, and supergroups. - Bots can delete incoming messages in private chats. - Bots granted 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 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool`: On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/deleteMessage'.format(self.base_url) data = {'chat_id': chat_id, 'message_id': message_id} result = self._request.post(url, data, timeout=timeout) return result @log def forward_message(self, chat_id, from_chat_id, message_id, disable_notification=False, timeout=None, **kwargs): """Use this method to forward messages of any kind. 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). disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/forwardMessage'.format(self.base_url) data = {} 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(url, data, disable_notification=disable_notification, timeout=timeout, **kwargs) @log def send_photo(self, chat_id, photo, caption=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=20, parse_mode=None, **kwargs): """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` | :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. caption (:obj:`str`, optional): Photo caption (may also be used when resending photos by file_id), 0-1024 characters. 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. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendPhoto'.format(self.base_url) if isinstance(photo, PhotoSize): photo = photo.file_id elif InputFile.is_file(photo): photo = InputFile(photo) data = {'chat_id': chat_id, 'photo': photo} if caption: data['caption'] = caption if parse_mode: data['parse_mode'] = parse_mode return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def send_audio(self, chat_id, audio, duration=None, performer=None, title=None, caption=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=20, parse_mode=None, thumb=None, **kwargs): """ 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 format. On success, the sent Message is returned. 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 sendVoice 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` | :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. caption (:obj:`str`, optional): Audio caption, 0-1024 characters. 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. 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. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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`, optional): Thumbnail of the file sent. 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 is passed as a string or file_id. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendAudio'.format(self.base_url) if isinstance(audio, Audio): audio = audio.file_id elif InputFile.is_file(audio): audio = InputFile(audio) data = {'chat_id': chat_id, 'audio': audio} if duration: data['duration'] = duration if performer: data['performer'] = performer if title: data['title'] = title if caption: data['caption'] = caption if parse_mode: data['parse_mode'] = parse_mode if thumb: if InputFile.is_file(thumb): thumb = InputFile(thumb, attach=True) data['thumb'] = thumb return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def send_document(self, chat_id, document, filename=None, caption=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=20, parse_mode=None, thumb=None, **kwargs): """Use this method to send general files. 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` | :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. filename (:obj:`str`, optional): File name that shows in telegram message (it is useful when you send file generated by temp module, for example). Undocumented. caption (:obj:`str`, optional): Document caption (may also be used when resending documents by file_id), 0-1024 characters. 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. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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`, optional): Thumbnail of the file sent. 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 is passed as a string or file_id. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendDocument'.format(self.base_url) if isinstance(document, Document): document = document.file_id elif InputFile.is_file(document): document = InputFile(document, filename=filename) data = {'chat_id': chat_id, 'document': document} if caption: data['caption'] = caption if parse_mode: data['parse_mode'] = parse_mode if thumb: if InputFile.is_file(thumb): thumb = InputFile(thumb, attach=True) data['thumb'] = thumb return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def send_sticker(self, chat_id, sticker, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=20, **kwargs): """Use this method to send .webp 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` :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. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendSticker'.format(self.base_url) if isinstance(sticker, Sticker): sticker = sticker.file_id elif InputFile.is_file(sticker): sticker = InputFile(sticker) data = {'chat_id': chat_id, 'sticker': sticker} return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def send_video(self, chat_id, video, duration=None, caption=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=20, width=None, height=None, parse_mode=None, supports_streaming=None, thumb=None, **kwargs): """ Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). Note: The video 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). video (:obj:`str` | `filelike object` | :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. 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. 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. supports_streaming (:obj:`bool`, optional): Pass 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. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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`, optional): Thumbnail of the file sent. 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 is passed as a string or file_id. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendVideo'.format(self.base_url) if isinstance(video, Video): video = video.file_id elif InputFile.is_file(video): video = InputFile(video) data = {'chat_id': chat_id, 'video': video} if duration: data['duration'] = duration if caption: data['caption'] = caption if parse_mode: data['parse_mode'] = parse_mode if supports_streaming: data['supports_streaming'] = supports_streaming if width: data['width'] = width if height: data['height'] = height if thumb: if InputFile.is_file(thumb): thumb = InputFile(thumb, attach=True) data['thumb'] = thumb return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def send_video_note(self, chat_id, video_note, duration=None, length=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=20, thumb=None, **kwargs): """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')`` 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` | :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. duration (:obj:`int`, optional): Duration of sent video in seconds. length (:obj:`int`, optional): Video width and height disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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`, optional): Thumbnail of the file sent. 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 is passed as a string or file_id. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendVideoNote'.format(self.base_url) if isinstance(video_note, VideoNote): video_note = video_note.file_id elif InputFile.is_file(video_note): video_note = InputFile(video_note) data = {'chat_id': chat_id, 'video_note': video_note} if duration is not None: data['duration'] = duration if length is not None: data['length'] = length if thumb: if InputFile.is_file(thumb): thumb = InputFile(thumb, attach=True) data['thumb'] = thumb return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def send_animation(self, chat_id, animation, duration=None, width=None, height=None, thumb=None, caption=None, parse_mode=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=20, **kwargs): """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). 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` | :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. 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`, optional): Thumbnail of the file sent. 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 is passed as a string or file_id. caption (:obj:`str`, optional): Animation caption (may also be used when resending animations by file_id), 0-1024 characters. 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. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendAnimation'.format(self.base_url) if isinstance(animation, Animation): animation = animation.file_id elif InputFile.is_file(animation): animation = InputFile(animation) data = {'chat_id': chat_id, 'animation': animation} if duration: data['duration'] = duration if width: data['width'] = width if height: data['height'] = height if thumb: if InputFile.is_file(thumb): thumb = InputFile(thumb, attach=True) data['thumb'] = thumb if caption: data['caption'] = caption if parse_mode: data['parse_mode'] = parse_mode return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def send_voice(self, chat_id, voice, duration=None, caption=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=20, parse_mode=None, **kwargs): """ 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). 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` | :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. caption (:obj:`str`, optional): Voice message caption, 0-1024 characters. 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. 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. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendVoice'.format(self.base_url) if isinstance(voice, Voice): voice = voice.file_id elif InputFile.is_file(voice): voice = InputFile(voice) data = {'chat_id': chat_id, 'voice': voice} if duration: data['duration'] = duration if caption: data['caption'] = caption if parse_mode: data['parse_mode'] = parse_mode return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def send_media_group(self, chat_id, media, disable_notification=None, reply_to_message_id=None, timeout=20, **kwargs): """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.InputMedia`]): An array describing photos and videos 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. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. timeout (:obj:`int` | :obj:`float`, optional): Send file timeout (default: 20 seconds). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: List[:class:`telegram.Message`]: An array of the sent Messages. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendMediaGroup'.format(self.base_url) data = {'chat_id': chat_id, 'media': media} for m in data['media']: if m.parse_mode == DEFAULT_NONE: if self.defaults: m.parse_mode = self.defaults.parse_mode else: m.parse_mode = None if reply_to_message_id: data['reply_to_message_id'] = reply_to_message_id if disable_notification: data['disable_notification'] = disable_notification result = self._request.post(url, data, timeout=timeout) if self.defaults: for res in result: res['default_quote'] = self.defaults.quote return [Message.de_json(res, self) for res in result] @log def send_location(self, chat_id, latitude=None, longitude=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=None, location=None, live_period=None, **kwargs): """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. live_period (:obj:`int`, optional): Period in seconds for which the location will be updated, should be between 60 and 86400. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendLocation'.format(self.base_url) 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 = {'chat_id': chat_id, 'latitude': latitude, 'longitude': longitude} if live_period: data['live_period'] = live_period return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def edit_message_live_location(self, chat_id=None, message_id=None, inline_message_id=None, latitude=None, longitude=None, location=None, reply_markup=None, timeout=None, **kwargs): """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:`live_period` expires or editing is explicitly disabled by a call to :attr:`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 sent message. 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. 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). Returns: :class:`telegram.Message`: On success the edited message. """ url = '{0}/editMessageLiveLocation'.format(self.base_url) 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 = {'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 return self._message(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs) @log def stop_message_live_location(self, chat_id=None, message_id=None, inline_message_id=None, reply_markup=None, timeout=None, **kwargs): """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`): 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. 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.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). Returns: :class:`telegram.Message`: On success the edited message. """ url = '{0}/stopMessageLiveLocation'.format(self.base_url) data = {} 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(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs) @log def send_venue(self, chat_id, latitude=None, longitude=None, title=None, address=None, foursquare_id=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=None, venue=None, foursquare_type=None, **kwargs): """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 optionally :obj:`foursquare_type`. 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".) 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. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendVenue'.format(self.base_url) 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 data = { '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 return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def send_contact(self, chat_id, phone_number=None, first_name=None, last_name=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=None, contact=None, vcard=None, **kwargs): """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. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendContact'.format(self.base_url) 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 = {'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(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def send_game(self, chat_id, game_short_name, disable_notification=False, reply_to_message_id=None, reply_markup=None, timeout=None, **kwargs): """Use this method to send a game. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format @channelusername). 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. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendGame'.format(self.base_url) data = {'chat_id': chat_id, 'game_short_name': game_short_name} return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def send_chat_action(self, chat_id, action, timeout=None, **kwargs): """ 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). 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool`: ``True`` on success. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendChatAction'.format(self.base_url) data = {'chat_id': chat_id, 'action': action} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def answer_inline_query(self, inline_query_id, results, cache_time=300, is_personal=None, next_offset=None, switch_pm_text=None, switch_pm_parameter=None, timeout=None, **kwargs): """ Use this method to send answers to an inline query. No more than 50 results per query are allowed. Args: inline_query_id (:obj:`str`): Unique identifier for the answered query. results (List[:class:`telegram.InlineQueryResult`)]: A list of results for the inline query. 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 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. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as he read timeout from the server (instead of the one specified during creation of the connection pool). **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/answerInlineQuery'.format(self.base_url) for res in results: if res._has_parse_mode and res.parse_mode == DEFAULT_NONE: if self.defaults: res.parse_mode = self.defaults.parse_mode else: res.parse_mode = None if res._has_input_message_content and res.input_message_content: if (res.input_message_content._has_parse_mode and res.input_message_content.parse_mode == DEFAULT_NONE): if self.defaults: res.input_message_content.parse_mode = self.defaults.parse_mode else: res.input_message_content.parse_mode = None if (res.input_message_content._has_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 = \ self.defaults.disable_web_page_preview else: res.input_message_content.disable_web_page_preview = None results = [res.to_dict() for res in results] data = {'inline_query_id': inline_query_id, 'results': results} 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 data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def get_user_profile_photos(self, user_id, offset=None, limit=100, timeout=None, **kwargs): """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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.UserProfilePhotos` Raises: :class:`telegram.TelegramError` """ url = '{0}/getUserProfilePhotos'.format(self.base_url) data = {'user_id': user_id} if offset is not None: data['offset'] = offset if limit: data['limit'] = limit data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return UserProfilePhotos.de_json(result, self) @log def get_file(self, file_id, timeout=None, **kwargs): """ 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 :attr:`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. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ url = '{0}/getFile'.format(self.base_url) try: file_id = file_id.file_id except AttributeError: pass data = {'file_id': file_id} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) if result.get('file_path'): result['file_path'] = '%s/%s' % (self.base_file_url, result['file_path']) return File.de_json(result, self) @log def kick_chat_member(self, chat_id, user_id, timeout=None, until_date=None, **kwargs): """ Use this method to kick a user from a group or a supergroup. In the case of supergroups, 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 group for this to work. 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). 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Note: In regular groups (non-supergroups), this method will only work if the 'All Members Are Admins' setting is off in the target group. Otherwise members may only be removed by the group's creator or by the member that added them. Returns: :obj:`bool` On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/kickChatMember'.format(self.base_url) data = {'chat_id': chat_id, 'user_id': user_id} data.update(kwargs) if until_date is not None: if isinstance(until_date, datetime): until_date = to_timestamp(until_date) data['until_date'] = until_date result = self._request.post(url, data, timeout=timeout) return result @log def unban_chat_member(self, chat_id, user_id, timeout=None, **kwargs): """Use this method to unban a previously kicked user in a supergroup. The user will not return to the group automatically, but will be able to join via link, etc. The bot must be an administrator in the group for this to work. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool` On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/unbanChatMember'.format(self.base_url) data = {'chat_id': chat_id, 'user_id': user_id} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def answer_callback_query(self, callback_query_id, text=None, show_alert=False, url=None, cache_time=None, timeout=None, **kwargs): """ 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 true, an alert will be shown by the client instead of a notification at the top of the chat screen. Defaults to 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool` On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url_ = '{0}/answerCallbackQuery'.format(self.base_url) data = {'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 data.update(kwargs) result = self._request.post(url_, data, timeout=timeout) return result @log def edit_message_text(self, text, chat_id=None, message_id=None, inline_message_id=None, parse_mode=None, disable_web_page_preview=None, reply_markup=None, timeout=None, **kwargs): """ Use this method to edit text and game 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 sent message. 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. 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. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/editMessageText'.format(self.base_url) data = {'text': text} 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 parse_mode: data['parse_mode'] = parse_mode if disable_web_page_preview: data['disable_web_page_preview'] = disable_web_page_preview return self._message(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs) @log def edit_message_caption(self, chat_id=None, message_id=None, inline_message_id=None, caption=None, reply_markup=None, timeout=None, parse_mode=None, **kwargs): """ Use this method to edit captions 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 sent message. 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. 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. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise ``True`` is returned. Raises: :class:`telegram.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') url = '{0}/editMessageCaption'.format(self.base_url) data = {} if caption: data['caption'] = caption if parse_mode: data['parse_mode'] = parse_mode 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(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs) @log def edit_message_media(self, chat_id=None, message_id=None, inline_message_id=None, media=None, reply_markup=None, timeout=None, **kwargs): """Use this method to edit audio, document, photo, or video messages. If a message is a part of a message album, then it can be edited only to a photo or a video. Otherwise, message type can be changed arbitrarily. When inline message is edited, new file can't be uploaded. Use previously uploaded file via its file_id or specify a URL. On success, if the edited message was sent by the bot, the edited Message is returned, otherwise True is returned. 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 sent message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ 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') url = '{0}/editMessageMedia'.format(self.base_url) data = {'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(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs) @log def edit_message_reply_markup(self, chat_id=None, message_id=None, inline_message_id=None, reply_markup=None, timeout=None, **kwargs): """ 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 sent message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the editedMessage is returned, otherwise ``True`` is returned. Raises: :class:`telegram.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') url = '{0}/editMessageReplyMarkup'.format(self.base_url) data = {} 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(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs) @log def get_updates(self, offset=None, limit=100, timeout=0, read_latency=2., allowed_updates=None, **kwargs): """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 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. 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 regardless of type (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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. Notes: 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.TelegramError` """ url = '{0}/getUpdates'.format(self.base_url) data = {'timeout': timeout} if offset: data['offset'] = offset if limit: data['limit'] = limit if allowed_updates is not None: data['allowed_updates'] = allowed_updates data.update(kwargs) # 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 = self._request.post(url, data, timeout=float(read_latency) + float(timeout)) if result: self.logger.debug('Getting updates: %s', [u['update_id'] for u in result]) else: self.logger.debug('No new updates found.') if self.defaults: for u in result: u['default_quote'] = self.defaults.quote return [Update.de_json(u, self) for u in result] @log def set_webhook(self, url=None, certificate=None, timeout=None, max_connections=40, allowed_updates=None, **kwargs): """ Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an update for the bot, we will send an HTTPS POST request to the specified url, containing a JSON-serialized Update. In case of an unsuccessful request, we will give up after a reasonable amount of attempts. If you'd like to make sure that the Webhook request comes from Telegram, we recommend 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) 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 regardless of type (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. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Note: 1. You will not be able to receive updates using get_updates for as 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, ``True`` is returned. Raises: :class:`telegram.TelegramError` .. _`guide to Webhooks`: https://core.telegram.org/bots/webhooks """ url_ = '{0}/setWebhook'.format(self.base_url) # Backwards-compatibility: 'url' used to be named 'webhook_url' if 'webhook_url' in kwargs: # pragma: no cover warnings.warn("The 'webhook_url' parameter has been renamed to 'url' in accordance " "with the API") if url is not None: raise ValueError("The parameters 'url' and 'webhook_url' are mutually exclusive") url = kwargs['webhook_url'] del kwargs['webhook_url'] data = {} if url is not None: data['url'] = url if certificate: if InputFile.is_file(certificate): certificate = InputFile(certificate) data['certificate'] = certificate if max_connections is not None: data['max_connections'] = max_connections if allowed_updates is not None: data['allowed_updates'] = allowed_updates data.update(kwargs) result = self._request.post(url_, data, timeout=timeout) return result @log def delete_webhook(self, timeout=None, **kwargs): """ Use this method to remove webhook integration if you decide to switch back to getUpdates. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool` On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/deleteWebhook'.format(self.base_url) data = kwargs result = self._request.post(url, data, timeout=timeout) return result @log def leave_chat(self, chat_id, timeout=None, **kwargs): """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 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool` On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/leaveChat'.format(self.base_url) data = {'chat_id': chat_id} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def get_chat(self, chat_id, timeout=None, **kwargs): """ 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 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Chat` Raises: :class:`telegram.TelegramError` """ url = '{0}/getChat'.format(self.base_url) data = {'chat_id': chat_id} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) if self.defaults: result['default_quote'] = self.defaults.quote return Chat.de_json(result, self) @log def get_chat_administrators(self, chat_id, timeout=None, **kwargs): """ Use this method to get a list of administrators in a chat. On success, returns an Array 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. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: List[:class:`telegram.ChatMember`] Raises: :class:`telegram.TelegramError` """ url = '{0}/getChatAdministrators'.format(self.base_url) data = {'chat_id': chat_id} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return [ChatMember.de_json(x, self) for x in result] @log def get_chat_members_count(self, chat_id, timeout=None, **kwargs): """Use this method to get the number of members in a chat 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: int: Number of members in the chat. Raises: :class:`telegram.TelegramError` """ url = '{0}/getChatMembersCount'.format(self.base_url) data = {'chat_id': chat_id} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def get_chat_member(self, chat_id, user_id, timeout=None, **kwargs): """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 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.ChatMember` Raises: :class:`telegram.TelegramError` """ url = '{0}/getChatMember'.format(self.base_url) data = {'chat_id': chat_id, 'user_id': user_id} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return ChatMember.de_json(result, self) @log def set_chat_sticker_set(self, chat_id, sticker_set_name, timeout=None, **kwargs): """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 :attr:`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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool`: True on success. """ url = '{0}/setChatStickerSet'.format(self.base_url) data = {'chat_id': chat_id, 'sticker_set_name': sticker_set_name} result = self._request.post(url, data, timeout=timeout) return result @log def delete_chat_sticker_set(self, chat_id, timeout=None, **kwargs): """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 :attr:`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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool`: True on success. """ url = '{0}/deleteChatStickerSet'.format(self.base_url) data = {'chat_id': chat_id} result = self._request.post(url, data, timeout=timeout) return result def get_webhook_info(self, timeout=None, **kwargs): """Use this method to get current webhook status. Requires no parameters. If the bot is using getUpdates, will return an object with the 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.WebhookInfo` """ url = '{0}/getWebhookInfo'.format(self.base_url) data = kwargs result = self._request.post(url, data, timeout=timeout) return WebhookInfo.de_json(result, self) @log def set_game_score(self, user_id, score, chat_id=None, message_id=None, inline_message_id=None, force=None, disable_edit_message=None, timeout=None, **kwargs): """ Use this method to set the score of the specified user in a game. On success, if the message was sent by the bot, returns the edited Message, otherwise returns True. Returns an error, if the new score is not greater than the user's current score in the chat and force is False. Args: user_id (:obj:`int`): User identifier. score (:obj:`int`): New score, must be non-negative. force (:obj:`bool`, optional): Pass 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 True, if the game message should not be automatically edited to include the current scoreboard. chat_id (int|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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: The edited message, or if the message wasn't sent by the bot , ``True``. Raises: :class:`telegram.TelegramError`: If the new score is not greater than the user's current score in the chat and force is False. """ url = '{0}/setGameScore'.format(self.base_url) data = {'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(url, data, timeout=timeout, **kwargs) @log def get_game_high_scores(self, user_id, chat_id=None, message_id=None, inline_message_id=None, timeout=None, **kwargs): """ Use this method to get data for high score tables. Will return the score of the specified user and several of his neighbors in a game Args: user_id (:obj:`int`): User identifier. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: List[:class:`telegram.GameHighScore`] Raises: :class:`telegram.TelegramError` """ url = '{0}/getGameHighScores'.format(self.base_url) data = {'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 data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return [GameHighScore.de_json(hs, self) for hs in result] @log def send_invoice(self, chat_id, title, description, payload, provider_token, start_parameter, currency, prices, photo_url=None, photo_size=None, photo_width=None, photo_height=None, need_name=None, need_phone_number=None, need_email=None, need_shipping_address=None, is_flexible=None, disable_notification=False, reply_to_message_id=None, reply_markup=None, provider_data=None, send_phone_number_to_provider=None, send_email_to_provider=None, timeout=None, **kwargs): """Use this method to send invoices. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target private chat. title (:obj:`str`): Product name. description (:obj:`str`): Product description. 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. start_parameter (:obj:`str`): Unique deep-linking parameter that can be used to generate this invoice when used as a start parameter. 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.). provider_data (:obj:`str` | :obj:`object`, optional): JSON-encoded 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 True, if you require the user's full name to complete the order. need_phone_number (:obj:`bool`, optional): Pass True, if you require the user's phone number to complete the order. need_email (:obj:`bool`, optional): Pass True, if you require the user's email to complete the order. need_shipping_address (:obj:`bool`, optional): Pass True, if you require the user's shipping address to complete the order. send_phone_number_to_provider (:obj:`bool`, optional): Pass True, if user's phone number should be sent to provider. send_email_to_provider (:obj:`bool`, optional): Pass True, if user's email address should be sent to provider. is_flexible (:obj:`bool`, optional): Pass 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. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. An inlinekeyboard. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendInvoice'.format(self.base_url) data = { 'chat_id': chat_id, 'title': title, 'description': description, 'payload': payload, 'provider_token': provider_token, 'start_parameter': start_parameter, 'currency': currency, 'prices': [p.to_dict() for p in prices] } if provider_data is not None: if isinstance(provider_data, string_types): 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_email_to_provider if send_email_to_provider is not None: data['send_email_to_provider'] = send_email_to_provider return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def answer_shipping_query(self, shipping_query_id, ok, shipping_options=None, error_message=None, timeout=None, **kwargs): """ If you sent an invoice requesting a shipping address and the parameter is_flexible was specified, the Bot API will send an Update with a 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 True if delivery to the specified address is possible and 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 True. A JSON-serialized array of available shipping options. error_message (:obj:`str`, optional): Required if ok is 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool`; On success, True is returned. Raises: :class:`telegram.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') url_ = '{0}/answerShippingQuery'.format(self.base_url) data = {'shipping_query_id': shipping_query_id, 'ok': ok} if ok: data['shipping_options'] = [option.to_dict() for option in shipping_options] if error_message is not None: data['error_message'] = error_message data.update(kwargs) result = self._request.post(url_, data, timeout=timeout) return result @log def answer_pre_checkout_query(self, pre_checkout_query_id, ok, error_message=None, timeout=None, **kwargs): """ Once the user has confirmed their payment and shipping details, the Bot API sends the final confirmation in the form of an Update with the field 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 True if everything is alright (goods are available, etc.) and the bot is ready to proceed with the order. Use False if there are any problems. error_message (:obj:`str`, optional): Required if ok is 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool`: On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ ok = bool(ok) if not (ok ^ (error_message is not None)): raise TelegramError( 'answerPreCheckoutQuery: If ok is True, there should ' 'not be error_message; if ok is False, error_message ' 'should not be empty') url_ = '{0}/answerPreCheckoutQuery'.format(self.base_url) data = {'pre_checkout_query_id': pre_checkout_query_id, 'ok': ok} if error_message is not None: data['error_message'] = error_message data.update(kwargs) result = self._request.post(url_, data, timeout=timeout) return result @log def restrict_chat_member(self, chat_id, user_id, permissions, until_date=None, timeout=None, **kwargs): """ 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 True for all boolean parameters to lift restrictions from a user. Note: Since Bot API 4.4, :attr:`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. permissions (:class:`telegram.ChatPermissions`): 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments Returns: :obj:`bool`: Returns True on success. Raises: :class:`telegram.TelegramError` """ url = '{0}/restrictChatMember'.format(self.base_url) data = {'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) data['until_date'] = until_date data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def promote_chat_member(self, chat_id, user_id, can_change_info=None, can_post_messages=None, can_edit_messages=None, can_delete_messages=None, can_invite_users=None, can_restrict_members=None, can_pin_messages=None, can_promote_members=None, timeout=None, **kwargs): """ 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 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 supergroup (in the format @supergroupusername). user_id (:obj:`int`): Unique identifier of the target user. can_change_info (:obj:`bool`, optional): Pass True, if the administrator can change chat title, photo and other settings. can_post_messages (:obj:`bool`, optional): Pass True, if the administrator can create channel posts, channels only. can_edit_messages (:obj:`bool`, optional): Pass True, if the administrator can edit messages of other users, channels only. can_delete_messages (:obj:`bool`, optional): Pass True, if the administrator can delete messages of other users. can_invite_users (:obj:`bool`, optional): Pass True, if the administrator can invite new users to the chat. can_restrict_members (:obj:`bool`, optional): Pass True, if the administrator can restrict, ban or unban chat members. can_pin_messages (:obj:`bool`, optional): Pass True, if the administrator can pin messages, supergroups only. can_promote_members (:obj:`bool`, optional): Pass 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments Returns: :obj:`bool`: Returns True on success. Raises: :class:`telegram.TelegramError` """ url = '{0}/promoteChatMember'.format(self.base_url) data = {'chat_id': chat_id, 'user_id': user_id} 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 data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def set_chat_permissions(self, chat_id, permissions, timeout=None, **kwargs): """ 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:`can_restrict_members` admin rights. Returns True on success. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments Returns: :obj:`bool`: Returns True on success. Raises: :class:`telegram.TelegramError` """ url = '{0}/setChatPermissions'.format(self.base_url) data = {'chat_id': chat_id, 'permissions': permissions.to_dict()} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def export_chat_invite_link(self, chat_id, timeout=None, **kwargs): """ Use this method to export an invite link to 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). 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments Returns: :obj:`str`: Exported invite link on success. Raises: :class:`telegram.TelegramError` """ url = '{0}/exportChatInviteLink'.format(self.base_url) data = {'chat_id': chat_id} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def set_chat_photo(self, chat_id, photo, timeout=20, **kwargs): """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`): New chat photo. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments Note: In regular groups (non-supergroups), this method will only work if the 'All Members Are Admins' setting is off in the target group. Returns: :obj:`bool`: Returns True on success. Raises: :class:`telegram.TelegramError` """ url = '{0}/setChatPhoto'.format(self.base_url) if InputFile.is_file(photo): photo = InputFile(photo) data = {'chat_id': chat_id, 'photo': photo} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def delete_chat_photo(self, chat_id, timeout=None, **kwargs): """ 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments Note: In regular groups (non-supergroups), this method will only work if the 'All Members Are Admins' setting is off in the target group. Returns: :obj:`bool`: Returns ``True`` on success. Raises: :class:`telegram.TelegramError` """ url = '{0}/deleteChatPhoto'.format(self.base_url) data = {'chat_id': chat_id} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def set_chat_title(self, chat_id, title, timeout=None, **kwargs): """ 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments Note: In regular groups (non-supergroups), this method will only work if the 'All Members Are Admins' setting is off in the target group. Returns: :obj:`bool`: Returns ``True`` on success. Raises: :class:`telegram.TelegramError` """ url = '{0}/setChatTitle'.format(self.base_url) data = {'chat_id': chat_id, 'title': title} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def set_chat_description(self, chat_id, description, timeout=None, **kwargs): """ 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, 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments Returns: :obj:`bool`: Returns ``True`` on success. Raises: :class:`telegram.TelegramError` """ url = '{0}/setChatDescription'.format(self.base_url) data = {'chat_id': chat_id, 'description': description} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def pin_chat_message(self, chat_id, message_id, disable_notification=None, timeout=None, **kwargs): """ Use this method to pin a message in a supergroup. 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). message_id (:obj:`int`): Identifier of a message to pin. disable_notification (:obj:`bool`, optional): Pass True, if it is not necessary to send a notification to all group members about the new pinned 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments Returns: :obj:`bool`: Returns ``True`` on success. Raises: :class:`telegram.TelegramError` """ url = '{0}/pinChatMessage'.format(self.base_url) data = {'chat_id': chat_id, 'message_id': message_id} if disable_notification is not None: data['disable_notification'] = disable_notification data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def unpin_chat_message(self, chat_id, timeout=None, **kwargs): """ Use this method to unpin a message in a supergroup. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments Returns: :obj:`bool`: Returns ``True`` on success. Raises: :class:`telegram.TelegramError` """ url = '{0}/unpinChatMessage'.format(self.base_url) data = {'chat_id': chat_id} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def get_sticker_set(self, name, timeout=None, **kwargs): """Use this method to get a sticker set. Args: name (:obj:`str`): Short name of the sticker set that is used in t.me/addstickers/ URLs (e.g., animals) 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.StickerSet` Raises: :class:`telegram.TelegramError` """ url = '{0}/getStickerSet'.format(self.base_url) data = {'name': name} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return StickerSet.de_json(result, self) @log def upload_sticker_file(self, user_id, png_sticker, timeout=20, **kwargs): """ Use this method to upload a .png file with a sticker for later use in :attr:`create_new_sticker_set` and :attr:`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`): 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. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File`: The uploaded File Raises: :class:`telegram.TelegramError` """ url = '{0}/uploadStickerFile'.format(self.base_url) if InputFile.is_file(png_sticker): png_sticker = InputFile(png_sticker) data = {'user_id': user_id, 'png_sticker': png_sticker} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return File.de_json(result, self) @log def create_new_sticker_set(self, user_id, name, title, png_sticker, emojis, contains_masks=None, mask_position=None, timeout=20, **kwargs): """Use this method to create new sticker set owned by a user. The bot will be able to edit the created sticker set. 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 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`): 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. emojis (:obj:`str`): One or more emoji corresponding to the sticker. contains_masks (:obj:`bool`, optional): Pass 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool`: On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/createNewStickerSet'.format(self.base_url) if InputFile.is_file(png_sticker): png_sticker = InputFile(png_sticker) data = {'user_id': user_id, 'name': name, 'title': title, 'png_sticker': png_sticker, 'emojis': emojis} if contains_masks is not None: data['contains_masks'] = contains_masks if mask_position is not None: data['mask_position'] = mask_position data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def add_sticker_to_set(self, user_id, name, png_sticker, emojis, mask_position=None, timeout=20, **kwargs): """Use this method to add a new sticker to a set created by the bot. 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 created sticker set owner. name (:obj:`str`): Sticker set name. png_sticker (:obj:`str` | `filelike object`): 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. emojis (:obj:`str`): One or more emoji corresponding to the sticker. mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask should beplaced 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool`: On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/addStickerToSet'.format(self.base_url) if InputFile.is_file(png_sticker): png_sticker = InputFile(png_sticker) data = {'user_id': user_id, 'name': name, 'png_sticker': png_sticker, 'emojis': emojis} if mask_position is not None: data['mask_position'] = mask_position data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def set_sticker_position_in_set(self, sticker, position, timeout=None, **kwargs): """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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool`: On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/setStickerPositionInSet'.format(self.base_url) data = {'sticker': sticker, 'position': position} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def delete_sticker_from_set(self, sticker, timeout=None, **kwargs): """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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool`: On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/deleteStickerFromSet'.format(self.base_url) data = {'sticker': sticker} data.update(kwargs) result = self._request.post(url, data, timeout=timeout) return result @log def set_passport_data_errors(self, user_id, errors, timeout=None, **kwargs): """ 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). Returns True on success. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :obj:`bool`: On success, ``True`` is returned. Raises: :class:`telegram.TelegramError` """ url_ = '{0}/setPassportDataErrors'.format(self.base_url) data = {'user_id': user_id, 'errors': [error.to_dict() for error in errors]} data.update(kwargs) result = self._request.post(url_, data, timeout=timeout) return result @log def send_poll(self, chat_id, question, options, disable_notification=None, reply_to_message_id=None, reply_markup=None, timeout=None, **kwargs): """ Use this method to send a native poll. A native poll can't be sent to a private chat. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target private chat. question (:obj:`str`): Poll question, 1-255 characters. options (List[:obj:`str`]): List of answer options, 2-10 strings 1-100 characters each. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Message`: On success, the sent Message is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/sendPoll'.format(self.base_url) data = { 'chat_id': chat_id, 'question': question, 'options': options } return self._message(url, data, timeout=timeout, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, **kwargs) @log def stop_poll(self, chat_id, message_id, reply_markup=None, timeout=None, **kwargs): """ 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.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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.Poll`: On success, the stopped Poll with the final results is returned. Raises: :class:`telegram.TelegramError` """ url = '{0}/stopPoll'.format(self.base_url) data = { 'chat_id': chat_id, 'message_id': message_id } if reply_markup: if isinstance(reply_markup, ReplyMarkup): data['reply_markup'] = reply_markup.to_json() else: data['reply_markup'] = reply_markup result = self._request.post(url, data, timeout=timeout) return Poll.de_json(result, self) def to_dict(self): data = {'id': self.id, 'username': self.username, 'first_name': self.first_name} if self.last_name: data['last_name'] = self.last_name return data def __reduce__(self): return (self.__class__, (self.token, self.base_url.replace(self.token, ''), self.base_file_url.replace(self.token, ''))) # camelCase aliases getMe = get_me """Alias for :attr:`get_me`""" sendMessage = send_message """Alias for :attr:`send_message`""" deleteMessage = delete_message """Alias for :attr:`delete_message`""" forwardMessage = forward_message """Alias for :attr:`forward_message`""" sendPhoto = send_photo """Alias for :attr:`send_photo`""" sendAudio = send_audio """Alias for :attr:`send_audio`""" sendDocument = send_document """Alias for :attr:`send_document`""" sendSticker = send_sticker """Alias for :attr:`send_sticker`""" sendVideo = send_video """Alias for :attr:`send_video`""" sendAnimation = send_animation """Alias for :attr:`send_animation`""" sendVoice = send_voice """Alias for :attr:`send_voice`""" sendVideoNote = send_video_note """Alias for :attr:`send_video_note`""" sendMediaGroup = send_media_group """Alias for :attr:`send_media_group`""" sendLocation = send_location """Alias for :attr:`send_location`""" editMessageLiveLocation = edit_message_live_location """Alias for :attr:`edit_message_live_location`""" stopMessageLiveLocation = stop_message_live_location """Alias for :attr:`stop_message_live_location`""" sendVenue = send_venue """Alias for :attr:`send_venue`""" sendContact = send_contact """Alias for :attr:`send_contact`""" sendGame = send_game """Alias for :attr:`send_game`""" sendChatAction = send_chat_action """Alias for :attr:`send_chat_action`""" answerInlineQuery = answer_inline_query """Alias for :attr:`answer_inline_query`""" getUserProfilePhotos = get_user_profile_photos """Alias for :attr:`get_user_profile_photos`""" getFile = get_file """Alias for :attr:`get_file`""" kickChatMember = kick_chat_member """Alias for :attr:`kick_chat_member`""" unbanChatMember = unban_chat_member """Alias for :attr:`unban_chat_member`""" answerCallbackQuery = answer_callback_query """Alias for :attr:`answer_callback_query`""" editMessageText = edit_message_text """Alias for :attr:`edit_message_text`""" editMessageCaption = edit_message_caption """Alias for :attr:`edit_message_caption`""" editMessageMedia = edit_message_media """Alias for :attr:`edit_message_media`""" editMessageReplyMarkup = edit_message_reply_markup """Alias for :attr:`edit_message_reply_markup`""" getUpdates = get_updates """Alias for :attr:`get_updates`""" setWebhook = set_webhook """Alias for :attr:`set_webhook`""" deleteWebhook = delete_webhook """Alias for :attr:`delete_webhook`""" leaveChat = leave_chat """Alias for :attr:`leave_chat`""" getChat = get_chat """Alias for :attr:`get_chat`""" getChatAdministrators = get_chat_administrators """Alias for :attr:`get_chat_administrators`""" getChatMember = get_chat_member """Alias for :attr:`get_chat_member`""" setChatStickerSet = set_chat_sticker_set """Alias for :attr:`set_chat_sticker_set`""" deleteChatStickerSet = delete_chat_sticker_set """Alias for :attr:`delete_chat_sticker_set`""" getChatMembersCount = get_chat_members_count """Alias for :attr:`get_chat_members_count`""" getWebhookInfo = get_webhook_info """Alias for :attr:`get_webhook_info`""" setGameScore = set_game_score """Alias for :attr:`set_game_score`""" getGameHighScores = get_game_high_scores """Alias for :attr:`get_game_high_scores`""" sendInvoice = send_invoice """Alias for :attr:`send_invoice`""" answerShippingQuery = answer_shipping_query """Alias for :attr:`answer_shipping_query`""" answerPreCheckoutQuery = answer_pre_checkout_query """Alias for :attr:`answer_pre_checkout_query`""" restrictChatMember = restrict_chat_member """Alias for :attr:`restrict_chat_member`""" promoteChatMember = promote_chat_member """Alias for :attr:`promote_chat_member`""" setChatPermissions = set_chat_permissions """Alias for :attr:`set_chat_permissions`""" exportChatInviteLink = export_chat_invite_link """Alias for :attr:`export_chat_invite_link`""" setChatPhoto = set_chat_photo """Alias for :attr:`set_chat_photo`""" deleteChatPhoto = delete_chat_photo """Alias for :attr:`delete_chat_photo`""" setChatTitle = set_chat_title """Alias for :attr:`set_chat_title`""" setChatDescription = set_chat_description """Alias for :attr:`set_chat_description`""" pinChatMessage = pin_chat_message """Alias for :attr:`pin_chat_message`""" unpinChatMessage = unpin_chat_message """Alias for :attr:`unpin_chat_message`""" getStickerSet = get_sticker_set """Alias for :attr:`get_sticker_set`""" uploadStickerFile = upload_sticker_file """Alias for :attr:`upload_sticker_file`""" createNewStickerSet = create_new_sticker_set """Alias for :attr:`create_new_sticker_set`""" addStickerToSet = add_sticker_to_set """Alias for :attr:`add_sticker_to_set`""" setStickerPositionInSet = set_sticker_position_in_set """Alias for :attr:`set_sticker_position_in_set`""" deleteStickerFromSet = delete_sticker_from_set """Alias for :attr:`delete_sticker_from_set`""" setPassportDataErrors = set_passport_data_errors """Alias for :attr:`set_passport_data_errors`""" sendPoll = send_poll """Alias for :attr:`send_poll`""" stopPoll = stop_poll """Alias for :attr:`stop_poll`""" python-telegram-bot-12.4.2/telegram/callbackquery.py000066400000000000000000000212501362023133600224740ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 CallbackQuery""" from telegram import TelegramObject, Message, User 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. 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. Attributes: id (:obj:`str`): Unique identifier for this query. from_user (:class:`telegram.User`): Sender. message (:class:`telegram.Message`): Optional. Message with the callback button that originated the query. inline_message_id (:obj:`str`): Optional. Identifier of the message sent via the bot in inline mode, that originated the query. chat_instance (:obj:`str`): Optional. Global identifier, uniquely corresponding to the chat to which the message with the callback button was sent. data (:obj:`str`): Optional. Data associated with the callback button. game_short_name (:obj:`str`): Optional. Short name of a Game to be returned. Args: id (:obj:`str`): Unique identifier for this query. from_user (:class:`telegram.User`): Sender. 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. inline_message_id (:obj:`str`, optional): Identifier of the message sent via the bot in inline mode, that originated the query. chat_instance (:obj:`str`, optional): Global identifier, uniquely corresponding to the chat to which the message with the callback button was sent. Useful for high scores in games. data (:obj:`str`, optional): Data associated with the callback button. Be aware that a bad client can send arbitrary data in this field. game_short_name (:obj:`str`, optional): Short name of a Game to be returned, serves as the unique identifier for the game Note: 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). """ def __init__(self, id, from_user, chat_instance, message=None, data=None, inline_message_id=None, game_short_name=None, bot=None, **kwargs): # Required self.id = id 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, bot): if not data: return None data = super(CallbackQuery, cls).de_json(data, bot) data['from_user'] = User.de_json(data.get('from'), bot) message = data.get('message') if message: message['default_quote'] = data.get('default_quote') data['message'] = Message.de_json(message, bot) return cls(bot=bot, **data) def answer(self, *args, **kwargs): """Shortcut for:: bot.answer_callback_query(update.callback_query.id, *args, **kwargs) Returns: :obj:`bool`: On success, ``True`` is returned. """ return self.bot.answerCallbackQuery(self.id, *args, **kwargs) def edit_message_text(self, text, *args, **kwargs): """Shortcut for either:: bot.edit_message_text(text, chat_id=update.callback_query.message.chat_id, message_id=update.callback_query.message.message_id, *args, **kwargs) or:: bot.edit_message_text(text, inline_message_id=update.callback_query.inline_message_id, *args, **kwargs) Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise ``True`` is returned. """ if self.inline_message_id: return self.bot.edit_message_text(text, inline_message_id=self.inline_message_id, *args, **kwargs) else: return self.bot.edit_message_text(text, chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs) def edit_message_caption(self, caption, *args, **kwargs): """Shortcut for either:: bot.edit_message_caption(caption=caption, chat_id=update.callback_query.message.chat_id, message_id=update.callback_query.message.message_id, *args, **kwargs) or:: bot.edit_message_caption(caption=caption inline_message_id=update.callback_query.inline_message_id, *args, **kwargs) Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise ``True`` is returned. """ if self.inline_message_id: return self.bot.edit_message_caption(caption=caption, inline_message_id=self.inline_message_id, *args, **kwargs) else: return self.bot.edit_message_caption(caption=caption, chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs) def edit_message_reply_markup(self, reply_markup, *args, **kwargs): """Shortcut for either:: bot.edit_message_replyMarkup(chat_id=update.callback_query.message.chat_id, message_id=update.callback_query.message.message_id, 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) Returns: :class:`telegram.Message`: On success, if edited message is sent by the bot, the edited Message is returned, otherwise ``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, *args, **kwargs) else: return self.bot.edit_message_reply_markup(reply_markup=reply_markup, chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs) python-telegram-bot-12.4.2/telegram/chat.py000066400000000000000000000314201362023133600205710ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0103,W0622 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Chat.""" from telegram import TelegramObject, ChatPhoto from .chatpermissions import ChatPermissions class Chat(TelegramObject): """This object represents a chat. 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. description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats. invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats. pinned_message (:class:`telegram.Message`): Optional. Pinned message, for supergroups. Returned only in get_chat. permissions (:class:`telegram.ChatPermission`): Optional. Default chat member permissions, for groups and supergroups. Returned only in getChat. sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set. can_set_sticker_set (:obj:`bool`): Optional. ``True``, if the bot can change group the sticker set. 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 getChat. description (:obj:`str`, optional): Description, for groups, supergroups and channel chats. Returned only in get_chat. invite_link (:obj:`str`, optional): Chat invite link, for supergroups and channel chats. Returned only in get_chat. pinned_message (:class:`telegram.Message`, optional): Pinned message, for supergroups. Returned only in get_chat. permissions (:class:`telegram.ChatPermission`): Optional. Default chat member permissions, for groups and supergroups. Returned only in getChat. 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 get_chat. can_set_sticker_set (:obj:`bool`, optional): ``True``, if the bot can change group the sticker set. Returned only in get_chat. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ PRIVATE = 'private' """:obj:`str`: 'private'""" GROUP = 'group' """:obj:`str`: 'group'""" SUPERGROUP = 'supergroup' """:obj:`str`: 'supergroup'""" CHANNEL = 'channel' """:obj:`str`: 'channel'""" def __init__(self, id, type, title=None, username=None, first_name=None, last_name=None, bot=None, photo=None, description=None, invite_link=None, pinned_message=None, permissions=None, sticker_set_name=None, can_set_sticker_set=None, **kwargs): # Required self.id = int(id) 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.description = description self.invite_link = invite_link self.pinned_message = pinned_message self.permissions = permissions self.sticker_set_name = sticker_set_name self.can_set_sticker_set = can_set_sticker_set self.bot = bot self._id_attrs = (self.id,) @property def link(self): """:obj:`str`: Convenience property. If the chat has a :attr:`username`, returns a t.me link of the chat.""" if self.username: return "https://t.me/{}".format(self.username) return None @classmethod def de_json(cls, data, bot): if not data: return None data['photo'] = ChatPhoto.de_json(data.get('photo'), bot) from telegram import Message pinned_message = data.get('pinned_message') if pinned_message: pinned_message['default_quote'] = data.get('default_quote') data['pinned_message'] = Message.de_json(pinned_message, bot) data['permissions'] = ChatPermissions.de_json(data.get('permissions'), bot) return cls(bot=bot, **data) def send_action(self, *args, **kwargs): """Shortcut for:: bot.send_chat_action(update.message.chat.id, *args, **kwargs) Returns: :obj:`bool`: If the action was sent successfully. """ return self.bot.send_chat_action(self.id, *args, **kwargs) def leave(self, *args, **kwargs): """Shortcut for:: bot.leave_chat(update.message.chat.id, *args, **kwargs) Returns: :obj:`bool` If the action was sent successfully. """ return self.bot.leave_chat(self.id, *args, **kwargs) def get_administrators(self, *args, **kwargs): """Shortcut for:: bot.get_chat_administrators(update.message.chat.id, *args, **kwargs) 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(self.id, *args, **kwargs) def get_members_count(self, *args, **kwargs): """Shortcut for:: bot.get_chat_members_count(update.message.chat.id, *args, **kwargs) Returns: :obj:`int` """ return self.bot.get_chat_members_count(self.id, *args, **kwargs) def get_member(self, *args, **kwargs): """Shortcut for:: bot.get_chat_member(update.message.chat.id, *args, **kwargs) Returns: :class:`telegram.ChatMember` """ return self.bot.get_chat_member(self.id, *args, **kwargs) def kick_member(self, *args, **kwargs): """Shortcut for:: bot.kick_chat_member(update.message.chat.id, *args, **kwargs) Returns: :obj:`bool`: If the action was sent succesfully. Note: This method will only work if the `All Members Are Admins` setting is off in the target group. Otherwise members may only be removed by the group's creator or by the member that added them. """ return self.bot.kick_chat_member(self.id, *args, **kwargs) def unban_member(self, *args, **kwargs): """Shortcut for:: bot.unban_chat_member(update.message.chat.id, *args, **kwargs) Returns: :obj:`bool`: If the action was sent successfully. """ return self.bot.unban_chat_member(self.id, *args, **kwargs) def set_permissions(self, *args, **kwargs): """Shortcut for:: bot.set_chat_permissions(update.message.chat.id, *args, **kwargs) Returns: :obj:`bool`: If the action was sent successfully. """ return self.bot.set_chat_permissions(self.id, *args, **kwargs) def send_message(self, *args, **kwargs): """Shortcut for:: bot.send_message(Chat.id, *args, **kwargs) Where Chat is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_message(self.id, *args, **kwargs) def send_photo(self, *args, **kwargs): """Shortcut for:: bot.send_photo(Chat.id, *args, **kwargs) Where Chat is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_photo(self.id, *args, **kwargs) def send_audio(self, *args, **kwargs): """Shortcut for:: bot.send_audio(Chat.id, *args, **kwargs) Where Chat is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_audio(self.id, *args, **kwargs) def send_document(self, *args, **kwargs): """Shortcut for:: bot.send_document(Chat.id, *args, **kwargs) Where Chat is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_document(self.id, *args, **kwargs) def send_animation(self, *args, **kwargs): """Shortcut for:: bot.send_animation(Chat.id, *args, **kwargs) Where Chat is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_animation(self.id, *args, **kwargs) def send_sticker(self, *args, **kwargs): """Shortcut for:: bot.send_sticker(Chat.id, *args, **kwargs) Where Chat is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_sticker(self.id, *args, **kwargs) def send_video(self, *args, **kwargs): """Shortcut for:: bot.send_video(Chat.id, *args, **kwargs) Where Chat is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_video(self.id, *args, **kwargs) def send_video_note(self, *args, **kwargs): """Shortcut for:: bot.send_video_note(Chat.id, *args, **kwargs) Where Chat is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_video_note(self.id, *args, **kwargs) def send_voice(self, *args, **kwargs): """Shortcut for:: bot.send_voice(Chat.id, *args, **kwargs) Where Chat is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_voice(self.id, *args, **kwargs) def send_poll(self, *args, **kwargs): """Shortcut for:: bot.send_poll(Chat.id, *args, **kwargs) Where Chat is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_poll(self.id, *args, **kwargs) python-telegram-bot-12.4.2/telegram/chataction.py000066400000000000000000000033171362023133600217730ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" class ChatAction(object): """Helper class to provide constants for different chatactions.""" FIND_LOCATION = 'find_location' """:obj:`str`: 'find_location'""" RECORD_AUDIO = 'record_audio' """:obj:`str`: 'record_audio'""" RECORD_VIDEO = 'record_video' """:obj:`str`: 'record_video'""" RECORD_VIDEO_NOTE = 'record_video_note' """:obj:`str`: 'record_video_note'""" TYPING = 'typing' """:obj:`str`: 'typing'""" UPLOAD_AUDIO = 'upload_audio' """:obj:`str`: 'upload_audio'""" UPLOAD_DOCUMENT = 'upload_document' """:obj:`str`: 'upload_document'""" UPLOAD_PHOTO = 'upload_photo' """:obj:`str`: 'upload_photo'""" UPLOAD_VIDEO = 'upload_video' """:obj:`str`: 'upload_video'""" UPLOAD_VIDEO_NOTE = 'upload_video_note' """:obj:`str`: 'upload_video_note'""" python-telegram-bot-12.4.2/telegram/chatmember.py000066400000000000000000000204421362023133600217630ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" from telegram import User, TelegramObject from telegram.utils.helpers import to_timestamp, from_timestamp class ChatMember(TelegramObject): """This object contains information about one member of the chat. Attributes: user (:class:`telegram.User`): Information about the user. status (:obj:`str`): The member's status in the chat. until_date (:class:`datetime.datetime`): Optional. Date when restrictions will be lifted for this user. can_be_edited (:obj:`bool`): Optional. If the bot is allowed to edit administrator privileges of that user. can_change_info (:obj:`bool`): Optional. If the user can change the chat title, photo and other settings. can_post_messages (:obj:`bool`): Optional. If the administrator can post in the channel. can_edit_messages (:obj:`bool`): Optional. If the administrator can edit messages of other users. can_delete_messages (:obj:`bool`): Optional. If the administrator can delete messages of other users. can_invite_users (:obj:`bool`): Optional. If the user can invite new users to the chat. can_restrict_members (:obj:`bool`): Optional. If the administrator can restrict, ban or unban chat members. can_pin_messages (:obj:`bool`): Optional. If the user can pin messages. can_promote_members (:obj:`bool`): Optional. If the administrator can add new administrators. is_member (:obj:`bool`): Optional. Restricted only. True, if the user is a member of the chat at the moment of the request. can_send_messages (:obj:`bool`): Optional. If the user can send text messages, contacts, locations and venues. can_send_media_messages (:obj:`bool`): Optional. If the user can send media messages, implies can_send_messages. can_send_polls (:obj:`bool`): Optional. True, if the user is allowed to send polls. can_send_other_messages (:obj:`bool`): Optional. If the user can send animations, games, stickers and use inline bots, implies can_send_media_messages. can_add_web_page_previews (:obj:`bool`): Optional. If user may add web page previews to his messages, implies can_send_media_messages Args: user (:class:`telegram.User`): Information about the user. status (:obj:`str`): The member's status in the chat. Can be 'creator', 'administrator', 'member', 'restricted', 'left' or 'kicked'. until_date (:class:`datetime.datetime`, optional): Restricted and kicked only. Date when restrictions will be lifted for this user. can_be_edited (:obj:`bool`, optional): Administrators only. True, if the bot is allowed to edit administrator privileges of that user. can_change_info (:obj:`bool`, optional): Administrators and restricted only. True, if the user can change the chat title, photo and other settings. can_post_messages (:obj:`bool`, optional): Administrators only. True, if the administrator can post in the channel, channels only. can_edit_messages (:obj:`bool`, optional): Administrators only. True, if the administrator can edit messages of other users, channels only. can_delete_messages (:obj:`bool`, optional): Administrators only. True, if the administrator can delete messages of other user. can_invite_users (:obj:`bool`, optional): Administrators and restricted only. True, if the user can invite new users to the chat. can_restrict_members (:obj:`bool`, optional): Administrators only. True, if the administrator can restrict, ban or unban chat members. can_pin_messages (:obj:`bool`, optional): Administrators and restricted only. True, if the user can pin messages, supergroups only. can_promote_members (:obj:`bool`, optional): Administrators only. 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). is_member (:obj:`bool`, optional): Restricted only. True, if the user is a member of the chat at the moment of the request. can_send_messages (:obj:`bool`, optional): Restricted only. True, if the user can send text messages, contacts, locations and venues. can_send_media_messages (:obj:`bool`, optional): Restricted only. True, if the user can send audios, documents, photos, videos, video notes and voice notes, implies can_send_messages. can_send_polls (:obj:`bool`, optional): Restricted only. True, if the user is allowed to send polls. can_send_other_messages (:obj:`bool`, optional): Restricted only. True, if the user can send animations, games, stickers and use inline bots, implies can_send_media_messages. can_add_web_page_previews (:obj:`bool`, optional): Restricted only. True, if user may add web page previews to his messages, implies can_send_media_messages. """ ADMINISTRATOR = 'administrator' """:obj:`str`: 'administrator'""" CREATOR = 'creator' """:obj:`str`: 'creator'""" KICKED = 'kicked' """:obj:`str`: 'kicked'""" LEFT = 'left' """:obj:`str`: 'left'""" MEMBER = 'member' """:obj:`str`: 'member'""" RESTRICTED = 'restricted' """:obj:`str`: 'restricted'""" def __init__(self, user, status, until_date=None, can_be_edited=None, can_change_info=None, can_post_messages=None, can_edit_messages=None, can_delete_messages=None, can_invite_users=None, can_restrict_members=None, can_pin_messages=None, can_promote_members=None, can_send_messages=None, can_send_media_messages=None, can_send_polls=None, can_send_other_messages=None, can_add_web_page_previews=None, is_member=None, **kwargs): # Required self.user = user self.status = status 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._id_attrs = (self.user, self.status) @classmethod def de_json(cls, data, bot): if not data: return None data = super(ChatMember, cls).de_json(data, bot) data['user'] = User.de_json(data.get('user'), bot) data['until_date'] = from_timestamp(data.get('until_date', None)) return cls(**data) def to_dict(self): data = super(ChatMember, self).to_dict() data['until_date'] = to_timestamp(self.until_date) return data python-telegram-bot-12.4.2/telegram/chatpermissions.py000066400000000000000000000107741362023133600230760ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class ChatPermissions(TelegramObject): """Describes actions that a non-administrator user is allowed to take in a chat. Attributes: can_send_messages (:obj:`bool`): Optional. True, if the user is allowed to send text messages, contacts, locations and venues. can_send_media_messages (:obj:`bool`): Optional. 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. True, if the user is allowed to send polls, implies :attr:`can_send_messages`. can_send_other_messages (:obj:`bool`): Optional. 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. 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. 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. True, if the user is allowed to invite new users to the chat. can_pin_messages (:obj:`bool`): Optional. True, if the user is allowed to pin messages. Ignored in public supergroups. Args: can_send_messages (:obj:`bool`, optional): True, if the user is allowed to send text messages, contacts, locations and venues. can_send_media_messages (:obj:`bool`, optional): 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): True, if the user is allowed to send polls, implies :attr:`can_send_messages`. can_send_other_messages (:obj:`bool`, optional): 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): 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): 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): True, if the user is allowed to invite new users to the chat. can_pin_messages (:obj:`bool`, optional): True, if the user is allowed to pin messages. Ignored in public supergroups. """ def __init__(self, can_send_messages=None, can_send_media_messages=None, can_send_polls=None, can_send_other_messages=None, can_add_web_page_previews=None, can_change_info=None, can_invite_users=None, can_pin_messages=None, **kwargs): # 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 @classmethod def de_json(cls, data, bot): if not data: return None return cls(**data) python-telegram-bot-12.4.2/telegram/choseninlineresult.py000066400000000000000000000061531362023133600235740ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0902,R0912,R0913 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject, User, Location class ChosenInlineResult(TelegramObject): """ Represents a result of an inline query that was chosen by the user and sent to their chat partner. Note: In Python `from` is a reserved word, use `from_user` instead. 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. 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. """ def __init__(self, result_id, from_user, query, location=None, inline_message_id=None, **kwargs): # 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, bot): if not data: return None data = super(ChosenInlineResult, cls).de_json(data, bot) # 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-12.4.2/telegram/constants.py000066400000000000000000000043561362023133600216760ustar00rootroot00000000000000# python-telegram-bot - a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # 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: 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 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) """ MAX_MESSAGE_LENGTH = 4096 MAX_CAPTION_LENGTH = 1024 # constants above this line are tested SUPPORTED_WEBHOOK_PORTS = [443, 80, 88, 8443] MAX_FILESIZE_DOWNLOAD = int(20E6) # (20MB) MAX_FILESIZE_UPLOAD = int(50E6) # (50MB) MAX_PHOTOSIZE_UPLOAD = int(10E6) # (10MB) MAX_MESSAGES_PER_SECOND_PER_CHAT = 1 MAX_MESSAGES_PER_SECOND = 30 MAX_MESSAGES_PER_MINUTE_PER_GROUP = 20 MAX_MESSAGE_ENTITIES = 100 MAX_INLINE_QUERY_RESULTS = 50 python-telegram-bot-12.4.2/telegram/error.py000066400000000000000000000054501362023133600210070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Telegram errors.""" def _lstrip_str(in_s, lstr): """ Args: in_s (:obj:`str`): in string lstr (:obj:`str`): substr to strip from left side Returns: str: """ if in_s.startswith(lstr): res = in_s[len(lstr):] else: res = in_s return res class TelegramError(Exception): def __init__(self, message): super(TelegramError, self).__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): return '%s' % (self.message) class Unauthorized(TelegramError): pass class InvalidToken(TelegramError): def __init__(self): super(InvalidToken, self).__init__('Invalid token') class NetworkError(TelegramError): pass class BadRequest(NetworkError): pass class TimedOut(NetworkError): def __init__(self): super(TimedOut, self).__init__('Timed out') class ChatMigrated(TelegramError): """ Args: new_chat_id (:obj:`int`): """ def __init__(self, new_chat_id): super(ChatMigrated, self).__init__('Group migrated to supergroup. New chat id: {}'.format(new_chat_id)) self.new_chat_id = new_chat_id class RetryAfter(TelegramError): """ Args: retry_after (:obj:`int`): """ def __init__(self, retry_after): super(RetryAfter, self).__init__('Flood control exceeded. Retry in {} seconds'.format(retry_after)) self.retry_after = float(retry_after) class Conflict(TelegramError): """ Raised when a long poll or webhook conflicts with another one. Args: msg (:obj:`str`): The message from telegrams server. """ def __init__(self, msg): super(Conflict, self).__init__(msg) python-telegram-bot-12.4.2/telegram/ext/000077500000000000000000000000001362023133600201005ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/ext/__init__.py000066400000000000000000000050371362023133600222160ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """Extensions over the Telegram Bot API to facilitate bot making""" from .basepersistence import BasePersistence from .picklepersistence import PicklePersistence from .dictpersistence import DictPersistence from .handler import Handler from .callbackcontext import CallbackContext from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async 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, 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 .defaults import Defaults __all__ = ('Dispatcher', 'JobQueue', 'Job', 'Updater', 'CallbackQueryHandler', 'ChosenInlineResultHandler', 'CommandHandler', 'Handler', 'InlineQueryHandler', 'MessageHandler', 'BaseFilter', 'Filters', 'RegexHandler', 'StringCommandHandler', 'StringRegexHandler', 'TypeHandler', 'ConversationHandler', 'PreCheckoutQueryHandler', 'ShippingQueryHandler', 'MessageQueue', 'DelayQueue', 'DispatcherHandlerStop', 'run_async', 'CallbackContext', 'BasePersistence', 'PicklePersistence', 'DictPersistence', 'PrefixHandler', 'Defaults') python-telegram-bot-12.4.2/telegram/ext/basepersistence.py000066400000000000000000000140451362023133600236350ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" class BasePersistence(object): """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 means: * If :attr:`store_bot_data` is ``True`` you must overwrite :meth:`get_bot_data` and :meth:`update_bot_data`. * If :attr:`store_chat_data` is ``True`` you must overwrite :meth:`get_chat_data` and :meth:`update_chat_data`. * If :attr:`store_user_data` is ``True`` you must overwrite :meth:`get_user_data` and :meth:`update_user_data`. * If you want to store conversation data with :class:`telegram.ext.ConversationHandler`, you must overwrite :meth:`get_conversations` and :meth:`update_conversation`. * :meth:`flush` will be called when the bot is shutdown. 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. Args: store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this persistence class. Default is ``True``. store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this persistence class. Default is ``True`` . store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this persistence class. Default is ``True`` . """ def __init__(self, store_user_data=True, store_chat_data=True, store_bot_data=True): self.store_user_data = store_user_data self.store_chat_data = store_chat_data self.store_bot_data = store_bot_data def get_user_data(self): """"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 ``defaultdict(dict)``. Returns: :obj:`defaultdict`: The restored user data. """ raise NotImplementedError def get_chat_data(self): """"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 ``defaultdict(dict)``. Returns: :obj:`defaultdict`: The restored chat data. """ raise NotImplementedError def get_bot_data(self): """"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 ``dict``. Returns: :obj:`defaultdict`: The restored bot data. """ raise NotImplementedError def get_conversations(self, name): """"Will be called by :class:`telegram.ext.Dispatcher` when a :class:`telegram.ext.ConversationHandler` is added if :attr:`telegram.ext.ConversationHandler.persistent` is ``True``. It should return the conversations for the handler with `name` or an empty ``dict`` Args: name (:obj:`str`): The handlers name. Returns: :obj:`dict`: The restored conversations for the handler. """ raise NotImplementedError def update_conversation(self, name, key, new_state): """Will be called when a :attr:`telegram.ext.ConversationHandler.update_state` is called. this allows the storeage of the new state in the persistence. Args: name (:obj:`str`): The handlers name. key (:obj:`tuple`): The key the state is changed for. new_state (:obj:`tuple` | :obj:`any`): The new state for the given key. """ raise NotImplementedError def update_user_data(self, user_id, data): """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 (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` [user_id]. """ raise NotImplementedError def update_chat_data(self, chat_id, data): """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 (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id]. """ raise NotImplementedError def update_bot_data(self, data): """Will be called by the :class:`telegram.ext.Dispatcher` after a handler has handled an update. Args: data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.bot_data` . """ raise NotImplementedError def flush(self): """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. If this is not of any importance just pass will be sufficient. """ pass python-telegram-bot-12.4.2/telegram/ext/callbackcontext.py000066400000000000000000000167141362023133600236240ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the CallbackContext class.""" from telegram import Update class CallbackContext(object): """ 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. 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. Attributes: bot_data (:obj:`dict`, optional): A dict that can be used to keep any data in. For each update it will be the same ``dict``. chat_data (: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 `_. user_data (: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``. 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 (:class:`telegram.TelegramError`, optional): The Telegram error that was raised. Only present when passed to a error handler registered with :attr:`telegram.ext.Dispatcher.add_error_handler`. job (:class:`telegram.ext.Job`): The job that that originated this callback. Only present when passed to the callback of :class:`telegram.ext.Job`. """ def __init__(self, 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._bot_data = dispatcher.bot_data self._chat_data = None self._user_data = None self.args = None self.matches = None self.error = None self.job = None @property def dispatcher(self): """:class:`telegram.ext.Dispatcher`: The dispatcher associated with this context.""" return self._dispatcher @property def bot_data(self): return self._bot_data @bot_data.setter def bot_data(self, value): raise AttributeError("You can not assign a new value to bot_data, see " "https://git.io/fjxKe") @property def chat_data(self): return self._chat_data @chat_data.setter def chat_data(self, value): raise AttributeError("You can not assign a new value to chat_data, see " "https://git.io/fjxKe") @property def user_data(self): return self._user_data @user_data.setter def user_data(self, value): raise AttributeError("You can not assign a new value to user_data, see " "https://git.io/fjxKe") @classmethod def from_error(cls, update, error, dispatcher): self = cls.from_update(update, dispatcher) self.error = error return self @classmethod def from_update(cls, update, dispatcher): self = cls(dispatcher) if update is not None and isinstance(update, Update): chat = update.effective_chat user = update.effective_user if chat: self._chat_data = dispatcher.chat_data[chat.id] if user: self._user_data = dispatcher.user_data[user.id] return self @classmethod def from_job(cls, job, dispatcher): self = cls(dispatcher) self.job = job return self def update(self, data): self.__dict__.update(data) @property def bot(self): """:class:`telegram.Bot`: The bot associated with this context.""" return self._dispatcher.bot @property def job_queue(self): """ :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): """ :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): """ `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] # pylint: disable=unsubscriptable-object except (IndexError, TypeError): return None python-telegram-bot-12.4.2/telegram/ext/callbackqueryhandler.py000066400000000000000000000164151362023133600246410ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 future.utils import string_types from telegram import Update from .handler import Handler class CallbackQueryHandler(Handler): """Handler class to handle Telegram callback queries. Optionally based on a regex. Read the documentation of the ``re`` module for more information. 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` | `Pattern`): Optional. Regex pattern to test :attr:`telegram.CallbackQuery.data` against. 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. 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. 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 ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pattern (:obj:`str` | `Pattern`, optional): Regex pattern. If not ``None``, ``re.match`` is used on :attr:`telegram.CallbackQuery.data` to determine if an update should be handled by this handler. 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 ``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 ``False`` DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``user_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``chat_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. """ def __init__(self, callback, pass_update_queue=False, pass_job_queue=False, pattern=None, pass_groups=False, pass_groupdict=False, pass_user_data=False, pass_chat_data=False): super(CallbackQueryHandler, self).__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) if isinstance(pattern, string_types): pattern = re.compile(pattern) self.pattern = pattern self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict def check_update(self, update): """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update`): Incoming telegram update. Returns: :obj:`bool` """ if isinstance(update, Update) and update.callback_query: if self.pattern: if update.callback_query.data: match = re.match(self.pattern, update.callback_query.data) if match: return match else: return True def collect_optional_args(self, dispatcher, update=None, check_result=None): optional_args = super(CallbackQueryHandler, self).collect_optional_args(dispatcher, update, check_result) if self.pattern: 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, update, dispatcher, check_result): if self.pattern: context.matches = [check_result] python-telegram-bot-12.4.2/telegram/ext/choseninlineresulthandler.py000066400000000000000000000103701362023133600257260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" from telegram import Update from .handler import Handler class ChosenInlineResultHandler(Handler): """Handler class to handle Telegram updates that contain a chosen inline result. 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. 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. 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 ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``user_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``chat_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. """ def check_update(self, update): """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update`): Incoming telegram update. Returns: :obj:`bool` """ return isinstance(update, Update) and update.chosen_inline_result python-telegram-bot-12.4.2/telegram/ext/commandhandler.py000066400000000000000000000413621362023133600234340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 future.utils import string_types from telegram.ext import Filters from telegram.utils.deprecate import TelegramDeprecationWarning from telegram import Update, MessageEntity from .handler import Handler class CommandHandler(Handler): """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. Attributes: command (:obj:`str` | List[:obj:`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. 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. Args: command (:obj:`str` | List[:obj:`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 ``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 ``False`` DEPRECATED: Please switch to context based callbacks. pass_update_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``user_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``chat_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. Raises: ValueError - when command is too long or has illegal chars. """ def __init__(self, command, callback, filters=None, allow_edited=None, pass_args=False, pass_update_queue=False, pass_job_queue=False, pass_user_data=False, pass_chat_data=False): super(CommandHandler, self).__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) if isinstance(command, string_types): 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): """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update`): Incoming telegram 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): command = message.text[1:message.entities[0].length] args = message.text.split()[1:] command = command.split('@') command.append(message.bot.username) if not (command[0].lower() in self.command and command[1].lower() == message.bot.username.lower()): return None filter_result = self.filters(update) if filter_result: return args, filter_result else: return False def collect_optional_args(self, dispatcher, update=None, check_result=None): optional_args = super(CommandHandler, self).collect_optional_args(dispatcher, update) if self.pass_args: optional_args['args'] = check_result[0] return optional_args def collect_additional_context(self, context, update, dispatcher, check_result): 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: PrefixHandler('!', 'test', callback) will respond to '!test'. Multiple prefixes, single command: PrefixHandler(['!', '#'], 'test', callback) will respond to '!test' and '#test'. Miltiple prefixes and commands: 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``. Attributes: prefix (:obj:`str` | List[:obj:`str`]): The prefix(es) that will precede :attr:`command`. command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler should listen for. 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. 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. Args: prefix (:obj:`str` | List[:obj:`str`]): The prefix(es) that will precede :attr:`command`. command (:obj:`str` | List[:obj:`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 ``False`` DEPRECATED: Please switch to context based callbacks. pass_update_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``user_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``chat_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. """ def __init__(self, prefix, command, callback, filters=None, pass_args=False, pass_update_queue=False, pass_job_queue=False, pass_user_data=False, pass_chat_data=False): super(PrefixHandler, self).__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) if isinstance(prefix, string_types): self.prefix = [prefix.lower()] else: self.prefix = prefix if isinstance(command, string_types): self.command = [command.lower()] else: self.command = command self.command = [x.lower() + y.lower() for x in self.prefix for y in self.command] def check_update(self, update): """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update`): Incoming telegram 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.command: return None filter_result = self.filters(update) if filter_result: return text_list[1:], filter_result else: return False def collect_additional_context(self, context, update, dispatcher, check_result): context.args = check_result[0] if isinstance(check_result[1], dict): context.update(check_result[1]) python-telegram-bot-12.4.2/telegram/ext/conversationhandler.py000066400000000000000000000504231362023133600245260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the ConversationHandler.""" import logging import warnings from threading import Lock from telegram import Update from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler, ChosenInlineResultHandler, CallbackContext) from telegram.utils.promise import Promise class _ConversationTimeoutContext(object): def __init__(self, conversation_key, update, dispatcher): self.conversation_key = conversation_key self.update = update self.dispatcher = dispatcher class ConversationHandler(Handler): """ A handler to hold a conversation with a single user by managing four collections of other handlers. 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.RegexHandler`. 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 ``None`` by default), the state will not change. If an entry point callback function returns 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``. 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 Attributes: entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can trigger the start of the conversation. 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. 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 ``False`` on :attr:`check_update`. allow_reentry (:obj:`bool`): Determines if a user can restart a conversation with an entry point. per_chat (:obj:`bool`): If the conversationkey should contain the Chat's ID. per_user (:obj:`bool`): If the conversationkey should contain the User's ID. per_message (:obj:`bool`): If the conversationkey should contain the Message's ID. 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 (default), there will be no timeout. When it's triggered, the last received update will be handled by ALL the handler's who's `check_update` method returns True that are in the state :attr:`ConversationHandler.TIMEOUT`. 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. 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 ``True`` will be used. If all return ``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 ``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 ``False`` on :attr:`check_update`. The first handler which :attr:`check_update` method returns ``True`` will be used. If all return ``False``, the update is not handled. allow_reentry (:obj:`bool`, optional): If set to ``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 ``True``. per_user (:obj:`bool`, optional): If the conversationkey should contain the User's ID. Default is ``True``. per_message (:obj:`bool`, optional): If the conversationkey should contain the Message's ID. Default is ``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 None (default), there will be no timeout. The last received update will be handled by ALL the handler's who's `check_update` method returns True that are in the state :attr:`ConversationHandler.TIMEOUT`. 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. Raises: ValueError """ END = -1 """:obj:`int`: Used as a constant to return when a conversation is ended.""" TIMEOUT = -2 """:obj:`int`: Used as a constant to handle state when a conversation is timed out.""" WAITING = -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.""" def __init__(self, entry_points, states, fallbacks, allow_reentry=False, per_chat=True, per_user=True, per_message=False, conversation_timeout=None, name=None, persistent=False, map_to_parent=None): 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 = persistent self._persistence = None """:obj:`telegram.ext.BasePersistance`: The persistence used to store conversations. Set by dispatcher""" self.map_to_parent = map_to_parent self.timeout_jobs = dict() self._timeout_jobs_lock = Lock() self._conversations = dict() 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() 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 @property def persistence(self): return self._persistence @persistence.setter def persistence(self, persistence): 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): return self._conversations @conversations.setter def conversations(self, value): self._conversations = value # Set conversations for nested conversations for handlers in self.states.values(): for handler in handlers: if isinstance(handler, ConversationHandler): handler.conversations = self.persistence.get_conversations(handler.name) def _get_key(self, update): chat = update.effective_chat user = update.effective_user key = list() if self.per_chat: key.append(chat.id) 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 or update.callback_query.message.message_id) return tuple(key) def check_update(self, update): """ 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`): Incoming telegram update. Returns: :obj:`bool` """ # Ignore messages in channels if (not isinstance(update, Update) or update.channel_post or self.per_chat and not update.effective_chat or self.per_message and not update.callback_query or 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...') old_state, new_state = state if new_state.done.wait(0): 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("{}".format(exc)) res = old_state finally: if res is None and old_state is None: res = self.END self.update_state(res, key) with self._conversations_lock: state = self.conversations.get(key) else: handlers = self.states.get(self.WAITING, []) for handler in handlers: check = handler.check_update(update) if check is not None and check is not False: return key, handler, 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 def handle_update(self, update, dispatcher, check_result, context=None): """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. """ conversation_key, handler, check_result = check_result 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() new_state = handler.handle_update(update, dispatcher, check_result, context) with self._timeout_jobs_lock: if self.conversation_timeout and new_state != self.END: # Add the new timeout job self.timeout_jobs[conversation_key] = dispatcher.job_queue.run_once( self._trigger_timeout, self.conversation_timeout, context=_ConversationTimeoutContext(conversation_key, update, dispatcher)) if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent: self.update_state(self.END, conversation_key) return self.map_to_parent.get(new_state) else: self.update_state(new_state, conversation_key) def update_state(self, new_state, key): 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: 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: self.persistence.update_conversation(self.name, key, (self.conversations.get(key), new_state)) elif new_state is not None: with self._conversations_lock: self.conversations[key] = new_state if self.persistent: self.persistence.update_conversation(self.name, key, new_state) def _trigger_timeout(self, context, job=None): self.logger.debug('conversation timeout was triggered!') # Backward compatibility with bots that do not use CallbackContext callback_context = None if isinstance(context, CallbackContext): job = context.job callback_context = context context = job.context with self._timeout_jobs_lock: found_job = self.timeout_jobs[context.conversation_key] if found_job is not job: # The timeout has been canceled in handle_update return del self.timeout_jobs[context.conversation_key] handlers = self.states.get(self.TIMEOUT, []) for handler in handlers: check = handler.check_update(context.update) if check is not None and check is not False: handler.handle_update(context.update, context.dispatcher, check, callback_context) self.update_state(self.END, context.conversation_key) python-telegram-bot-12.4.2/telegram/ext/defaults.py000066400000000000000000000125551362023133600222710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the class Defaults, which allows to pass default values to Updater.""" from telegram.utils.helpers import DEFAULT_NONE class Defaults: """Convenience Class to gather all parameters with a (user defined) default value Attributes: parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width toxt 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. 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). quote (:obj:`bool`): Optional. If set to ``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: ``True`` in group chats and ``False`` in private chats. Parameters: parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width toxt 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. 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). quote (:obj:`bool`, opitonal): If set to ``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: ``True`` in group chats and ``False`` in private chats. """ def __init__(self, parse_mode=None, disable_notification=None, disable_web_page_preview=None, # Timeout needs special treatment, since the bot methods have two different # default values for timeout (None and 20s) timeout=DEFAULT_NONE, quote=None): self._parse_mode = parse_mode self._disable_notification = disable_notification self._disable_web_page_preview = disable_web_page_preview self._timeout = timeout self._quote = quote @property def parse_mode(self): return self._parse_mode @parse_mode.setter def parse_mode(self, value): raise AttributeError("You can not assign a new value to defaults after because it would " "not have any effect.") @property def disable_notification(self): return self._disable_notification @disable_notification.setter def disable_notification(self, value): 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): return self._disable_web_page_preview @disable_web_page_preview.setter def disable_web_page_preview(self, value): raise AttributeError("You can not assign a new value to defaults after because it would " "not have any effect.") @property def timeout(self): return self._timeout @timeout.setter def timeout(self, value): raise AttributeError("You can not assign a new value to defaults after because it would " "not have any effect.") @property def quote(self): return self._quote @quote.setter def quote(self, value): raise AttributeError("You can not assign a new value to defaults after because it would " "not have any effect.") def __hash__(self): return hash((self._parse_mode, self._disable_notification, self._disable_web_page_preview, self._timeout, self._quote)) def __eq__(self, other): if isinstance(other, Defaults): return self.__dict__ == other.__dict__ return False def __ne__(self, other): return not self == other python-telegram-bot-12.4.2/telegram/ext/dictpersistence.py000066400000000000000000000226031362023133600236450ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 copy import deepcopy from telegram.utils.helpers import decode_user_chat_data_from_json,\ decode_conversations_from_json, encode_conversations_to_json try: import ujson as json except ImportError: import json from collections import defaultdict from telegram.ext import BasePersistence class DictPersistence(BasePersistence): """Using python's dicts and json for making your bot persistent. 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. Args: store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this persistence class. Default is ``True``. store_chat_data (:obj:`bool`, optional): Whether user_data should be saved by this persistence class. Default is ``True``. store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this persistence class. Default is ``True`` . user_data_json (:obj:`str`, optional): Json string that will be used to reconstruct user_data on creating this persistence. Default is ``""``. chat_data_json (:obj:`str`, optional): Json string that will be used to reconstruct chat_data on creating this persistence. Default is ``""``. bot_data_json (:obj:`str`, optional): Json string that will be used to reconstruct bot_data on creating this persistence. Default is ``""``. conversations_json (:obj:`str`, optional): Json string that will be used to reconstruct conversation on creating this persistence. Default is ``""``. """ def __init__(self, store_user_data=True, store_chat_data=True, store_bot_data=True, user_data_json='', chat_data_json='', bot_data_json='', conversations_json=''): super(DictPersistence, self).__init__(store_user_data=store_user_data, store_chat_data=store_chat_data, store_bot_data=store_bot_data) self._user_data = None self._chat_data = None self._bot_data = None self._conversations = None self._user_data_json = None self._chat_data_json = None self._bot_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): raise TypeError("Unable to deserialize user_data_json. Not valid JSON") 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): raise TypeError("Unable to deserialize chat_data_json. Not valid JSON") if bot_data_json: try: self._bot_data = json.loads(bot_data_json) self._bot_data_json = bot_data_json except (ValueError, AttributeError): raise TypeError("Unable to deserialize bot_data_json. Not valid JSON") if not isinstance(self._bot_data, dict): raise TypeError("bot_data_json must be serialized dict") if conversations_json: try: self._conversations = decode_conversations_from_json(conversations_json) self._conversations_json = conversations_json except (ValueError, AttributeError): raise TypeError("Unable to deserialize conversations_json. Not valid JSON") @property def user_data(self): """:obj:`dict`: The user_data as a dict""" return self._user_data @property def user_data_json(self): """:obj:`str`: The user_data serialized as a JSON-string.""" if self._user_data_json: return self._user_data_json else: return json.dumps(self.user_data) @property def chat_data(self): """:obj:`dict`: The chat_data as a dict""" return self._chat_data @property def chat_data_json(self): """:obj:`str`: The chat_data serialized as a JSON-string.""" if self._chat_data_json: return self._chat_data_json else: return json.dumps(self.chat_data) @property def bot_data(self): """:obj:`dict`: The bot_data as a dict""" return self._bot_data @property def bot_data_json(self): """:obj:`str`: The bot_data serialized as a JSON-string.""" if self._bot_data_json: return self._bot_data_json else: return json.dumps(self.bot_data) @property def conversations(self): """:obj:`dict`: The conversations as a dict""" return self._conversations @property def conversations_json(self): """:obj:`str`: The conversations serialized as a JSON-string.""" if self._conversations_json: return self._conversations_json else: return encode_conversations_to_json(self.conversations) def get_user_data(self): """Returns the user_data created from the ``user_data_json`` or an empty defaultdict. Returns: :obj:`defaultdict`: The restored user data. """ if self.user_data: pass else: self._user_data = defaultdict(dict) return deepcopy(self.user_data) def get_chat_data(self): """Returns the chat_data created from the ``chat_data_json`` or an empty defaultdict. Returns: :obj:`defaultdict`: The restored user data. """ if self.chat_data: pass else: self._chat_data = defaultdict(dict) return deepcopy(self.chat_data) def get_bot_data(self): """Returns the bot_data created from the ``bot_data_json`` or an empty dict. Returns: :obj:`defaultdict`: The restored user data. """ if self.bot_data: pass else: self._bot_data = {} return deepcopy(self.bot_data) def get_conversations(self, name): """Returns the conversations created from the ``conversations_json`` or an empty defaultdict. Returns: :obj:`defaultdict`: The restored user data. """ if self.conversations: pass else: self._conversations = {} return self.conversations.get(name, {}).copy() def update_conversation(self, name, key, new_state): """Will update the conversations for the given handler. Args: name (:obj:`str`): The handlers 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 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, data): """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.get(user_id) == data: return self._user_data[user_id] = data self._user_data_json = None def update_chat_data(self, chat_id, data): """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.get(chat_id) == data: return self._chat_data[chat_id] = data self._chat_data_json = None def update_bot_data(self, data): """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.copy() self._bot_data_json = None python-telegram-bot-12.4.2/telegram/ext/dispatcher.py000066400000000000000000000527021362023133600226060ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 functools import wraps from threading import Thread, Lock, Event, current_thread, BoundedSemaphore from time import sleep from uuid import uuid4 from collections import defaultdict from queue import Queue, Empty from future.builtins import range from telegram import TelegramError, Update from telegram.ext.handler import Handler from telegram.ext.callbackcontext import CallbackContext from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.promise import Promise from telegram.ext import BasePersistence logging.getLogger(__name__).addHandler(logging.NullHandler()) DEFAULT_GROUP = 0 def run_async(func): """ 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. 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, **kwargs): return Dispatcher.get_instance().run_async(func, *args, **kwargs) return async_func class DispatcherHandlerStop(Exception): """Raise this in handler to prevent execution any other handler (even in different group).""" pass class Dispatcher(object): """This class dispatches all kinds of updates to its registered handlers. 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`): Number of maximum concurrent worker threads for the ``@run_async`` decorator. 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 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. 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 ``True`` Use the context based callback API. During the deprecation period of the old API the default is ``False``. **New users**: set this to ``True``. """ __singleton_lock = Lock() __singleton_semaphore = BoundedSemaphore() __singleton = None logger = logging.getLogger(__name__) def __init__(self, bot, update_queue, workers=4, exception_event=None, job_queue=None, persistence=None, use_context=False): self.bot = bot self.update_queue = update_queue self.job_queue = job_queue self.workers = workers self.use_context = use_context if not use_context: warnings.warn('Old Handler API is deprecated - see https://git.io/fxJuV for details', TelegramDeprecationWarning, stacklevel=3) self.user_data = defaultdict(dict) self.chat_data = defaultdict(dict) self.bot_data = {} if persistence: if not isinstance(persistence, BasePersistence): raise TypeError("persistence should be based on telegram.ext.BasePersistence") self.persistence = persistence 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, dict): raise ValueError("bot_data must be of type dict") else: self.persistence = None self.handlers = {} """Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]: Holds the handlers per group.""" self.groups = [] """List[:obj:`int`]: A list with all groups.""" self.error_handlers = [] """List[:obj:`callable`]: A list of errorHandlers.""" 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() self.__async_threads = 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=0): self._set_singleton(self) else: self._set_singleton(None) @property def exception_event(self): return self.__exception_event def _init_async_threads(self, base_name, workers): base_name = '{}_'.format(base_name) if base_name else '' for i in range(workers): thread = Thread(target=self._pooled, name='Bot:{}:worker:{}{}'.format(self.bot.id, base_name, i)) self.__async_threads.add(thread) thread.start() @classmethod def _set_singleton(cls, val): cls.logger.debug('Setting singleton dispatcher as %s', val) cls.__singleton = weakref.ref(val) if val else None @classmethod def get_instance(cls): """Get the singleton instance of this class. Returns: :class:`telegram.ext.Dispatcher` Raises: RuntimeError """ if cls.__singleton is not None: return cls.__singleton() # pylint: disable=not-callable else: raise RuntimeError('{} not initialized or multiple instances exist'.format( cls.__name__)) def _pooled(self): thr_name = current_thread().getName() 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 isinstance(promise.exception, DispatcherHandlerStop): self.logger.warning( 'DispatcherHandlerStop is not supported with async functions; func: %s', promise.pooled_function.__name__) def run_async(self, func, *args, **kwargs): """Queue a function (with given args/kwargs) to be run asynchronously. 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. Args: func (:obj:`callable`): The function to run in the thread. *args (:obj:`tuple`, optional): Arguments to `func`. **kwargs (:obj:`dict`, optional): Keyword arguments to `func`. Returns: Promise """ # TODO: handle exception in async threads # set a threading.Event to notify caller thread promise = Promise(func, args, kwargs) self.__async_queue.put(promise) return promise def start(self, ready=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(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 elif 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): """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 {0}/{1} to end'.format(i + 1, total)) thr.join() self.__async_threads.remove(thr) self.logger.debug('async thread {0}/{1} has ended'.format(i + 1, total)) @property def has_running_threads(self): return self.running or bool(self.__async_threads) def process_update(self, update): """Processes a single update. Args: update (:obj:`str` | :class:`telegram.Update` | :class:`telegram.TelegramError`): The update to process. """ def persist_update(update): """Persist a single update. Args: update (:class:`telegram.Update`): The update to process. """ if self.persistence and isinstance(update, Update): if self.persistence.store_bot_data: try: self.persistence.update_bot_data(self.bot_data) except Exception as e: try: self.dispatch_error(update, e) 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 and update.effective_chat: chat_id = update.effective_chat.id try: self.persistence.update_chat_data(chat_id, self.chat_data[chat_id]) except Exception as e: try: self.dispatch_error(update, e) 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 and update.effective_user: user_id = update.effective_user.id try: self.persistence.update_user_data(user_id, self.user_data[user_id]) except Exception as e: try: self.dispatch_error(update, e) 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) # 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 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 = CallbackContext.from_update(update, self) handler.handle_update(update, self, check, context) persist_update(update) break # Stop processing with any other handler. except DispatcherHandlerStop: self.logger.debug('Stopping further handlers due to DispatcherHandlerStop') persist_update(update) break # Dispatch any error. except Exception as e: try: self.dispatch_error(update, e) except DispatcherHandlerStop: self.logger.debug('Error handler stopped further handlers') break # Errors should not stop the thread. except Exception: self.logger.exception('An error was raised while processing the update and an ' 'uncaught error was raised while handling the error ' 'with an error_handler') def add_handler(self, handler, group=DEFAULT_GROUP): """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 if not isinstance(handler, Handler): raise TypeError('handler is not an instance of {0}'.format(Handler.__name__)) if not isinstance(group, int): raise TypeError('group is not int') if isinstance(handler, ConversationHandler) and handler.persistent: if not self.persistence: raise ValueError( "Conversationhandler {} can not be persistent if dispatcher has no " "persistence".format(handler.name)) handler.persistence = self.persistence handler.conversations = self.persistence.get_conversations(handler.name) if group not in self.handlers: self.handlers[group] = list() self.groups.append(group) self.groups = sorted(self.groups) self.handlers[group].append(handler) def remove_handler(self, handler, group=DEFAULT_GROUP): """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 :attr:`user_data`, :attr:`chat_data` and :attr:`bot_data` in :attr:`persistence`. """ if self.persistence: if self.persistence.store_bot_data: self.persistence.update_bot_data(self.bot_data) if self.persistence.store_chat_data: for chat_id in self.chat_data: self.persistence.update_chat_data(chat_id, self.chat_data[chat_id]) if self.persistence.store_user_data: for user_id in self.user_data: self.persistence.update_user_data(user_id, self.user_data[user_id]) def add_error_handler(self, callback): """Registers an error handler in the Dispatcher. This handler will receive every error which happens in your bot. 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: Update, context: CallbackContext)`` The error that happened will be present in context.error. Note: See https://git.io/fxJuV for more info about switching to context based API. """ self.error_handlers.append(callback) def remove_error_handler(self, callback): """Removes an error handler. Args: callback (:obj:`callable`): The error handler to remove. """ if callback in self.error_handlers: self.error_handlers.remove(callback) def dispatch_error(self, update, error): """Dispatches an error. Args: update (:obj:`str` | :class:`telegram.Update` | None): The update that caused the error error (:obj:`Exception`): The error that was raised. """ if self.error_handlers: for callback in self.error_handlers: if self.use_context: callback(update, CallbackContext.from_error(update, error, self)) else: callback(self.bot, update, error) else: self.logger.exception( 'No error handlers are registered, logging exception.', exc_info=error) python-telegram-bot-12.4.2/telegram/ext/filters.py000066400000000000000000001127171362023133600221330ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 Filters for use with the MessageHandler class.""" import re from future.utils import string_types from telegram import Chat, Update, MessageEntity __all__ = ['Filters', 'BaseFilter', 'InvertedFilter', 'MergedFilter'] class BaseFilter(object): """Base class for all Message Filters. Subclassing from this class filters to be combined using bitwise operators: And: >>> (Filters.text & Filters.entity(MENTION)) Or: >>> (Filters.audio | Filters.video) 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 that pythons `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. If you want to create your own filters create a class inheriting from this class and implement a `filter` method that returns a boolean: `True` if the message should be handled, `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 `name` class variable. Attributes: name (:obj:`str`): Name for this filter. Defaults to the type of filter. update_filter (:obj:`bool`): Whether this filter should work on update. If ``False`` it will run the filter on :attr:`update.effective_message``. Default is ``False``. 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). """ name = None update_filter = False data_filter = False def __call__(self, update): if self.update_filter: return self.filter(update) else: return self.filter(update.effective_message) def __and__(self, other): return MergedFilter(self, and_filter=other) def __or__(self, other): return MergedFilter(self, or_filter=other) def __invert__(self): return InvertedFilter(self) def __repr__(self): # 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 def filter(self, update): """This method must be overwritten. Note: If :attr:`update_filter` is false then the first argument is `message` and of type :class:`telegram.Message`. Args: update (:class:`telegram.Update`): The update that is tested. Returns: :obj:`dict` or :obj:`bool` """ raise NotImplementedError class InvertedFilter(BaseFilter): """Represents a filter that has been inverted. Args: f: The filter to invert. """ update_filter = True def __init__(self, f): self.f = f def filter(self, update): return not bool(self.f(update)) def __repr__(self): return "".format(self.f) class MergedFilter(BaseFilter): """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. """ update_filter = True def __init__(self, base_filter, and_filter=None, or_filter=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 def _merge(self, base_output, comp_output): 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): 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 else: comp_output = self.or_filter(update) if comp_output: if self.data_filter: return comp_output return True return False def __repr__(self): return "<{} {} {}>".format(self.base_filter, "and" if self.and_filter else "or", self.and_filter or self.or_filter) class Filters(object): """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. """ class _All(BaseFilter): name = 'Filters.all' def filter(self, message): return True all = _All() """All Messages.""" class _Text(BaseFilter): name = 'Filters.text' class _TextIterable(BaseFilter): def __init__(self, iterable): self.iterable = iterable self.name = 'Filters.text({})'.format(iterable) def filter(self, message): if message.text: return message.text in self.iterable return False def __call__(self, update): if isinstance(update, Update): return self.filter(update.effective_message) else: return self._TextIterable(update) def filter(self, message): return bool(message.text) text = _Text() """Text Messages. If an iterable of strings is passed, it filters messages to only allow those whose text is appearing in the given iterable. Examples: To allow any text message, simply use ``MessageHandler(Filters.text, callback_method)``. A simple usecase for passing an iterable is to allow only messages that were send by a custom :class:`telegram.ReplyKeyboardMarkup`:: buttons = ['Start', 'Settings', 'Back'] markup = ReplyKeyboardMarkup.from_column(buttons) ... MessageHandler(Filters.text(buttons), callback_method) Args: update (Iterable[:obj:`str`], optional): Which messages to allow. Only exact matches are allowed. If not specified, will allow any text message. """ class _Caption(BaseFilter): name = 'Filters.caption' class _CaptionIterable(BaseFilter): def __init__(self, iterable): self.iterable = iterable self.name = 'Filters.caption({})'.format(iterable) def filter(self, message): if message.caption: return message.caption in self.iterable return False def __call__(self, update): if isinstance(update, Update): return self.filter(update.effective_message) else: return self._CaptionIterable(update) def filter(self, message): return bool(message.caption) caption = _Caption() """Messages with a caption. If an iterable of strings is passed, it filters messages to only allow those whose caption is appearing in the given iterable. Examples: ``MessageHandler(Filters.caption, callback_method)`` Args: update (Iterable[:obj:`str`], optional): Which captions to allow. Only exact matches are allowed. If not specified, will allow any message with a caption. """ class _Command(BaseFilter): name = 'Filters.command' class _CommandOnlyStart(BaseFilter): def __init__(self, only_start): self.only_start = only_start self.name = 'Filters.command({})'.format(only_start) def filter(self, message): return (message.entities and any([e.type == MessageEntity.BOT_COMMAND for e in message.entities])) def __call__(self, update): if isinstance(update, Update): return self.filter(update.effective_message) else: return self._CommandOnlyStart(update) def filter(self, message): return (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 ``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) Args: update (:obj:`bool`, optional): Whether to only allow messages that `start` with a bot command. Defaults to ``True``. """ class regex(BaseFilter): """ 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 that pythons `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. """ data_filter = True def __init__(self, pattern): if isinstance(pattern, string_types): pattern = re.compile(pattern) self.pattern = pattern self.name = 'Filters.regex({})'.format(self.pattern) def filter(self, message): """""" # remove method from docs if message.text: match = self.pattern.search(message.text) if match: return {'matches': [match]} return {} class _Reply(BaseFilter): name = 'Filters.reply' def filter(self, message): return bool(message.reply_to_message) reply = _Reply() """Messages that are a reply to another message.""" class _Audio(BaseFilter): name = 'Filters.audio' def filter(self, message): return bool(message.audio) audio = _Audio() """Messages that contain :class:`telegram.Audio`.""" class _Document(BaseFilter): name = 'Filters.document' class category(BaseFilter): """This Filter 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.documents.category('audio/') returnes `True` for all types of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav' """ def __init__(self, category): """Initialize the category you want to filter Args: category (str, optional): category of the media you want to filter""" self.category = category self.name = "Filters.document.category('{}')".format(self.category) def filter(self, message): """""" # remove method from docs if message.document: return message.document.mime_type.startswith(self.category) application = category('application/') audio = category('audio/') image = category('image/') video = category('video/') text = category('text/') class mime_type(BaseFilter): """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.documents.mime_type('audio/mpeg')`` filters all audio in mp3 format. """ def __init__(self, mimetype): """Initialize the category you want to filter Args: filetype (str, optional): mime_type of the media you want to filter""" self.mimetype = mimetype self.name = "Filters.document.mime_type('{}')".format(self.mimetype) def filter(self, message): """""" # remove method from docs if message.document: return message.document.mime_type == self.mimetype 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') def filter(self, message): 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: This Filter 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.documents.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: 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.documents.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")``- """ class _Animation(BaseFilter): name = 'Filters.animation' def filter(self, message): return bool(message.animation) animation = _Animation() """Messages that contain :class:`telegram.Animation`.""" class _Photo(BaseFilter): name = 'Filters.photo' def filter(self, message): return bool(message.photo) photo = _Photo() """Messages that contain :class:`telegram.PhotoSize`.""" class _Sticker(BaseFilter): name = 'Filters.sticker' def filter(self, message): return bool(message.sticker) sticker = _Sticker() """Messages that contain :class:`telegram.Sticker`.""" class _Video(BaseFilter): name = 'Filters.video' def filter(self, message): return bool(message.video) video = _Video() """Messages that contain :class:`telegram.Video`.""" class _Voice(BaseFilter): name = 'Filters.voice' def filter(self, message): return bool(message.voice) voice = _Voice() """Messages that contain :class:`telegram.Voice`.""" class _VideoNote(BaseFilter): name = 'Filters.video_note' def filter(self, message): return bool(message.video_note) video_note = _VideoNote() """Messages that contain :class:`telegram.VideoNote`.""" class _Contact(BaseFilter): name = 'Filters.contact' def filter(self, message): return bool(message.contact) contact = _Contact() """Messages that contain :class:`telegram.Contact`.""" class _Location(BaseFilter): name = 'Filters.location' def filter(self, message): return bool(message.location) location = _Location() """Messages that contain :class:`telegram.Location`.""" class _Venue(BaseFilter): name = 'Filters.venue' def filter(self, message): return bool(message.venue) venue = _Venue() """Messages that contain :class:`telegram.Venue`.""" class _StatusUpdate(BaseFilter): """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. """ update_filter = True class _NewChatMembers(BaseFilter): name = 'Filters.status_update.new_chat_members' def filter(self, message): return bool(message.new_chat_members) new_chat_members = _NewChatMembers() """Messages that contain :attr:`telegram.Message.new_chat_members`.""" class _LeftChatMember(BaseFilter): name = 'Filters.status_update.left_chat_member' def filter(self, message): return bool(message.left_chat_member) left_chat_member = _LeftChatMember() """Messages that contain :attr:`telegram.Message.left_chat_member`.""" class _NewChatTitle(BaseFilter): name = 'Filters.status_update.new_chat_title' def filter(self, message): return bool(message.new_chat_title) new_chat_title = _NewChatTitle() """Messages that contain :attr:`telegram.Message.new_chat_title`.""" class _NewChatPhoto(BaseFilter): name = 'Filters.status_update.new_chat_photo' def filter(self, message): return bool(message.new_chat_photo) new_chat_photo = _NewChatPhoto() """Messages that contain :attr:`telegram.Message.new_chat_photo`.""" class _DeleteChatPhoto(BaseFilter): name = 'Filters.status_update.delete_chat_photo' def filter(self, message): return bool(message.delete_chat_photo) delete_chat_photo = _DeleteChatPhoto() """Messages that contain :attr:`telegram.Message.delete_chat_photo`.""" class _ChatCreated(BaseFilter): name = 'Filters.status_update.chat_created' def filter(self, message): 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 _Migrate(BaseFilter): name = 'Filters.status_update.migrate' def filter(self, message): 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(BaseFilter): name = 'Filters.status_update.pinned_message' def filter(self, message): return bool(message.pinned_message) pinned_message = _PinnedMessage() """Messages that contain :attr:`telegram.Message.pinned_message`.""" class _ConnectedWebsite(BaseFilter): name = 'Filters.status_update.connected_website' def filter(self, message): return bool(message.connected_website) connected_website = _ConnectedWebsite() """Messages that contain :attr:`telegram.Message.connected_website`.""" name = 'Filters.status_update' def filter(self, message): 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.migrate(message) or self.pinned_message(message) or self.connected_website(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`. 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_from_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`. pinned_message: Messages that contain :attr:`telegram.Message.pinned_message`. """ class _Forwarded(BaseFilter): name = 'Filters.forwarded' def filter(self, message): return bool(message.forward_date) forwarded = _Forwarded() """Messages that are forwarded.""" class _Game(BaseFilter): name = 'Filters.game' def filter(self, message): return bool(message.game) game = _Game() """Messages that contain :class:`telegram.Game`.""" class entity(BaseFilter): """ 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`. """ def __init__(self, entity_type): self.entity_type = entity_type self.name = 'Filters.entity({})'.format(self.entity_type) def filter(self, message): """""" # remove method from docs return any(entity.type == self.entity_type for entity in message.entities) class caption_entity(BaseFilter): """ 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`. """ def __init__(self, entity_type): self.entity_type = entity_type self.name = 'Filters.caption_entity({})'.format(self.entity_type) def filter(self, message): """""" # remove method from docs return any(entity.type == self.entity_type for entity in message.caption_entities) class _Private(BaseFilter): name = 'Filters.private' def filter(self, message): return message.chat.type == Chat.PRIVATE private = _Private() """Messages sent in a private chat.""" class _Group(BaseFilter): name = 'Filters.group' def filter(self, message): return message.chat.type in [Chat.GROUP, Chat.SUPERGROUP] group = _Group() """Messages sent in a group chat.""" class user(BaseFilter): """Filters messages to allow only those which are from specified user ID. Examples: ``MessageHandler(Filters.user(1234), callback_method)`` Args: user_id(:obj:`int` | List[:obj:`int`], optional): Which user ID(s) to allow through. username(:obj:`str` | List[:obj:`str`], optional): Which username(s) to allow through. If username starts with '@' symbol, it will be ignored. Raises: ValueError: If chat_id and username are both present, or neither is. """ def __init__(self, user_id=None, username=None): if not (bool(user_id) ^ bool(username)): raise ValueError('One and only one of user_id or username must be used') if user_id is not None and isinstance(user_id, int): self.user_ids = [user_id] else: self.user_ids = user_id if username is None: self.usernames = username elif isinstance(username, string_types): self.usernames = [username.replace('@', '')] else: self.usernames = [user.replace('@', '') for user in username] def filter(self, message): """""" # remove method from docs if self.user_ids is not None: return bool(message.from_user and message.from_user.id in self.user_ids) else: # self.usernames is not None return bool(message.from_user and message.from_user.username and message.from_user.username in self.usernames) class chat(BaseFilter): """Filters messages to allow only those which are from specified chat ID. Examples: ``MessageHandler(Filters.chat(-1234), callback_method)`` Args: chat_id(:obj:`int` | List[:obj:`int`], optional): Which chat ID(s) to allow through. username(:obj:`str` | List[:obj:`str`], optional): Which username(s) to allow through. If username start swith '@' symbol, it will be ignored. Raises: ValueError: If chat_id and username are both present, or neither is. """ def __init__(self, chat_id=None, username=None): if not (bool(chat_id) ^ bool(username)): raise ValueError('One and only one of chat_id or username must be used') if chat_id is not None and isinstance(chat_id, int): self.chat_ids = [chat_id] else: self.chat_ids = chat_id if username is None: self.usernames = username elif isinstance(username, string_types): self.usernames = [username.replace('@', '')] else: self.usernames = [chat.replace('@', '') for chat in username] def filter(self, message): """""" # remove method from docs if self.chat_ids is not None: return bool(message.chat_id in self.chat_ids) else: # self.usernames is not None return bool(message.chat.username and message.chat.username in self.usernames) class _Invoice(BaseFilter): name = 'Filters.invoice' def filter(self, message): return bool(message.invoice) invoice = _Invoice() """Messages that contain :class:`telegram.Invoice`.""" class _SuccessfulPayment(BaseFilter): name = 'Filters.successful_payment' def filter(self, message): return bool(message.successful_payment) successful_payment = _SuccessfulPayment() """Messages that confirm a :class:`telegram.SuccessfulPayment`.""" class _PassportData(BaseFilter): name = 'Filters.passport_data' def filter(self, message): return bool(message.passport_data) passport_data = _PassportData() """Messages that contain a :class:`telegram.PassportData`""" class _Poll(BaseFilter): name = 'Filters.poll' def filter(self, message): return bool(message.poll) poll = _Poll() """Messages that contain a :class:`telegram.Poll`.""" class language(BaseFilter): """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 (:obj:`str` | List[:obj:`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'. """ def __init__(self, lang): if isinstance(lang, string_types): self.lang = [lang] else: self.lang = lang self.name = 'Filters.language({})'.format(self.lang) def filter(self, message): """""" # remove method from docs return message.from_user.language_code and any( [message.from_user.language_code.startswith(x) for x in self.lang]) class _UpdateType(BaseFilter): update_filter = True name = 'Filters.update' class _Message(BaseFilter): name = 'Filters.update.message' update_filter = True def filter(self, update): return update.message is not None message = _Message() class _EditedMessage(BaseFilter): name = 'Filters.update.edited_message' update_filter = True def filter(self, update): return update.edited_message is not None edited_message = _EditedMessage() class _Messages(BaseFilter): name = 'Filters.update.messages' update_filter = True def filter(self, update): return update.message is not None or update.edited_message is not None messages = _Messages() class _ChannelPost(BaseFilter): name = 'Filters.update.channel_post' update_filter = True def filter(self, update): return update.channel_post is not None channel_post = _ChannelPost() class _EditedChannelPost(BaseFilter): update_filter = True name = 'Filters.update.edited_channel_post' def filter(self, update): return update.edited_channel_post is not None edited_channel_post = _EditedChannelPost() class _ChannelPosts(BaseFilter): update_filter = True name = 'Filters.update.channel_posts' def filter(self, update): return update.channel_post is not None or update.edited_channel_post is not None channel_posts = _ChannelPosts() def filter(self, update): return 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-12.4.2/telegram/ext/handler.py000066400000000000000000000172031362023133600220720ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" class Handler(object): """The base class for all update handlers. Create custom handlers by inheriting from it. 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. 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. 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 ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``user_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``chat_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. """ def __init__(self, callback, pass_update_queue=False, pass_job_queue=False, pass_user_data=False, pass_chat_data=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 def check_update(self, update): """ This method is called to determine if an update should be handled by this handler instance. It should always be overridden. Args: update (:obj:`str` | :class:`telegram.Update`): The update to be tested. Returns: Either ``None`` or ``False`` if the update should not be handled. Otherwise an object that will be passed to :attr:`handle_update` and :attr:`collect_additional_context` when the update gets handled. """ raise NotImplementedError def handle_update(self, update, dispatcher, check_result, context=None): """ This method is called if it was determined that an update should indeed be handled by this instance. Calls :attr:`self.callback` along with its respectful arguments. To work with the :class:`telegram.ext.ConversationHandler`, this method returns the value returned from ``self.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: The result from :attr:`check_update`. """ if context: self.collect_additional_context(context, update, dispatcher, check_result) return self.callback(update, context) else: optional_args = self.collect_optional_args(dispatcher, update, check_result) return self.callback(dispatcher.bot, update, **optional_args) def collect_additional_context(self, context, update, dispatcher, check_result): """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`. """ pass def collect_optional_args(self, dispatcher, update=None, check_result=None): """ 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() 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: user = update.effective_user optional_args['user_data'] = dispatcher.user_data[user.id if user else None] if self.pass_chat_data: chat = update.effective_chat optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None] return optional_args python-telegram-bot-12.4.2/telegram/ext/inlinequeryhandler.py000066400000000000000000000162771362023133600243710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 future.utils import string_types from telegram import Update from .handler import Handler class InlineQueryHandler(Handler): """ Handler class to handle Telegram inline queries. Optionally based on a regex. Read the documentation of the ``re`` module for more information. 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. 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. 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. 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 ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pattern (:obj:`str` | :obj:`Pattern`, optional): Regex pattern. If not ``None``, ``re.match`` is used on :attr:`telegram.InlineQuery.query` to determine if an update should be handled by this handler. 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 ``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 ``False`` DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``user_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``chat_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. """ def __init__(self, callback, pass_update_queue=False, pass_job_queue=False, pattern=None, pass_groups=False, pass_groupdict=False, pass_user_data=False, pass_chat_data=False): super(InlineQueryHandler, self).__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) if isinstance(pattern, string_types): pattern = re.compile(pattern) self.pattern = pattern self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict def check_update(self, update): """ Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update`): Incoming telegram update. Returns: :obj:`bool` """ if isinstance(update, Update) and update.inline_query: if self.pattern: if update.inline_query.query: match = re.match(self.pattern, update.inline_query.query) if match: return match else: return True def collect_optional_args(self, dispatcher, update=None, check_result=None): optional_args = super(InlineQueryHandler, self).collect_optional_args(dispatcher, update, check_result) if self.pattern: 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, update, dispatcher, check_result): if self.pattern: context.matches = [check_result] python-telegram-bot-12.4.2/telegram/ext/jobqueue.py000066400000000000000000000516731362023133600223050ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 import time import warnings import weakref from numbers import Number from queue import PriorityQueue, Empty from threading import Thread, Lock, Event from telegram.ext.callbackcontext import CallbackContext from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import to_float_timestamp, _UTC class Days(object): MON, TUE, WED, THU, FRI, SAT, SUN = range(7) EVERY_DAY = tuple(range(7)) class JobQueue(object): """This class allows you to periodically perform tasks with the bot. Attributes: _queue (:obj:`PriorityQueue`): The queue that holds the Jobs. bot (:class:`telegram.Bot`): The bot instance that should be passed to the jobs. DEPRECATED: Use :attr:`set_dispatcher` instead. """ def __init__(self, bot=None): self._queue = PriorityQueue() if bot: warnings.warn("Passing bot to jobqueue is deprecated. Please use set_dispatcher " "instead!", TelegramDeprecationWarning, stacklevel=2) class MockDispatcher(object): def __init__(self): self.bot = bot self.use_context = False self._dispatcher = MockDispatcher() else: self._dispatcher = None self.logger = logging.getLogger(self.__class__.__name__) self.__start_lock = Lock() self.__next_peek_lock = Lock() # to protect self._next_peek & self.__tick self.__tick = Event() self.__thread = None self._next_peek = None self._running = False def set_dispatcher(self, dispatcher): """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 def _put(self, job, time_spec=None, previous_t=None): """ Enqueues the job, scheduling its next run at the correct time. Args: job (telegram.ext.Job): job to enqueue time_spec (optional): Specification of the time for which the job should be scheduled. The precise semantics of this parameter depend on its type (see :func:`telegram.ext.JobQueue.run_repeating` for details). Defaults to now + ``job.interval``. previous_t (optional): Time at which the job last ran (``None`` if it hasn't run yet). """ # get time at which to run: if time_spec is None: time_spec = job.interval if time_spec is None: raise ValueError("no time specification given for scheduling non-repeating job") next_t = to_float_timestamp(time_spec, reference_timestamp=previous_t) # enqueue: self.logger.debug('Putting job %s with t=%s', job.name, time_spec) self._queue.put((next_t, job)) # Wake up the loop if this job should be executed next self._set_next_peek(next_t) def run_once(self, callback, when, context=None, name=None): """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. * :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. 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. """ job = Job(callback, repeat=False, context=context, name=name, job_queue=self) self._put(job, time_spec=when) return job def run_repeating(self, callback, interval, first=None, context=None, name=None): """Creates a new ``Job`` that runs at specified intervals 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. 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. * :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. Defaults to ``interval`` 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. Notes: `interval` is always respected "as-is". That means that if DST changes during that interval, the job might not run at the time one would expect. It is always recommended to pin servers to UTC time, then time related behaviour can always be expected. """ job = Job(callback, interval=interval, repeat=True, context=context, name=name, job_queue=self) self._put(job, time_spec=first) return job def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None): """Creates a new ``Job`` that runs on a daily 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. time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone (``time.tzinfo``) is ``None``, UTC will be assumed. days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should run. 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 ``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. Notes: Daily is just an alias for "24 Hours". That means that if DST changes during that interval, the job might not run at the time one would expect. It is always recommended to pin servers to UTC time, then time related behaviour can always be expected. """ job = Job(callback, interval=datetime.timedelta(days=1), repeat=True, days=days, tzinfo=time.tzinfo, context=context, name=name, job_queue=self) self._put(job, time_spec=time) return job def _set_next_peek(self, t): # """ # Set next peek if not defined or `t` is before next peek. # In case the next peek was set, also trigger the `self.__tick` event. # """ with self.__next_peek_lock: if not self._next_peek or self._next_peek > t: self._next_peek = t self.__tick.set() def tick(self): """Run all jobs that are due and re-enqueue them with their interval.""" now = time.time() self.logger.debug('Ticking jobs with t=%f', now) while True: try: t, job = self._queue.get(False) except Empty: break self.logger.debug('Peeked at %s with t=%f', job.name, t) if t > now: # We can get here in two conditions: # 1. At the second or later pass of the while loop, after we've already # processed the job(s) we were supposed to at this time. # 2. At the first iteration of the loop only if `self.put()` had triggered # `self.__tick` because `self._next_peek` wasn't set self.logger.debug("Next task isn't due yet. Finished!") self._queue.put((t, job)) self._set_next_peek(t) break if job.removed: self.logger.debug('Removing job %s', job.name) continue if job.enabled: try: current_week_day = datetime.datetime.now(job.tzinfo).date().weekday() if any(day == current_week_day for day in job.days): self.logger.debug('Running job %s', job.name) job.run(self._dispatcher) except Exception: self.logger.exception('An uncaught error was raised while executing job %s', job.name) else: self.logger.debug('Skipping disabled job %s', job.name) if job.repeat and not job.removed: self._put(job, previous_t=t) else: self.logger.debug('Dropping non-repeating or removed job %s', job.name) def start(self): """Starts the job_queue thread.""" self.__start_lock.acquire() if not self._running: self._running = True self.__start_lock.release() self.__thread = Thread(target=self._main_loop, name="Bot:{}:job_queue".format(self._dispatcher.bot.id)) self.__thread.start() self.logger.debug('%s thread started', self.__class__.__name__) else: self.__start_lock.release() def _main_loop(self): """ Thread target of thread ``job_queue``. Runs in background and performs ticks on the job queue. """ while self._running: # self._next_peek may be (re)scheduled during self.tick() or self.put() with self.__next_peek_lock: tmout = self._next_peek - time.time() if self._next_peek else None self._next_peek = None self.__tick.clear() self.__tick.wait(tmout) # If we were woken up by self.stop(), just bail out if not self._running: break self.tick() self.logger.debug('%s thread stopped', self.__class__.__name__) def stop(self): """Stops the thread.""" with self.__start_lock: self._running = False self.__tick.set() if self.__thread is not None: self.__thread.join() def jobs(self): """Returns a tuple of all jobs that are currently in the ``JobQueue``.""" with self._queue.mutex: return tuple(job[1] for job in self._queue.queue if job) def get_jobs_by_name(self, name): """Returns a tuple of jobs with the given name that are currently in the ``JobQueue``""" with self._queue.mutex: return tuple(job[1] for job in self._queue.queue if job and job[1].name == name) class Job(object): """This class encapsulates a Job. 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. 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. interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`, optional): The time interval between executions of the job. If it is an :obj:`int` or a :obj:`float`, it will be interpreted as seconds. If you don't set this value, you must set :attr:`repeat` to ``False`` and specify :attr:`time_spec` when you put the job into the job queue. repeat (:obj:`bool`, optional): If this job should be periodically execute its callback function (``True``) or only once (``False``). Defaults to ``True``. 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__``. days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should run. Defaults to ``Days.EVERY_DAY`` job_queue (:class:`telegram.ext.JobQueue`, optional): The ``JobQueue`` this job belongs to. Only optional for backward compatibility with ``JobQueue.put()``. tzinfo (:obj:`datetime.tzinfo`, optional): timezone associated to this job. Used when checking the day of the week to determine whether a job should run (only relevant when ``days is not Days.EVERY_DAY``). Defaults to UTC. """ def __init__(self, callback, interval=None, repeat=True, context=None, days=Days.EVERY_DAY, name=None, job_queue=None, tzinfo=_UTC): self.callback = callback self.context = context self.name = name or callback.__name__ self._repeat = None self._interval = None self.interval = interval self.repeat = repeat self._days = None self.days = days self.tzinfo = tzinfo self._job_queue = weakref.proxy(job_queue) if job_queue is not None else None self._remove = Event() self._enabled = Event() self._enabled.set() def run(self, dispatcher): """Executes the callback function.""" if dispatcher.use_context: self.callback(CallbackContext.from_job(self, dispatcher)) else: self.callback(dispatcher.bot, self) def schedule_removal(self): """ Schedules this job for removal from the ``JobQueue``. It will be removed without executing its callback function again. """ self._remove.set() @property def removed(self): """:obj:`bool`: Whether this job is due to be removed.""" return self._remove.is_set() @property def enabled(self): """:obj:`bool`: Whether this job is enabled.""" return self._enabled.is_set() @enabled.setter def enabled(self, status): if status: self._enabled.set() else: self._enabled.clear() @property def interval(self): """ :obj:`int` | :obj:`float` | :obj:`datetime.timedelta`: Optional. The interval in which the job will run. """ return self._interval @interval.setter def interval(self, interval): if interval is None and self.repeat: raise ValueError("The 'interval' can not be 'None' when 'repeat' is set to 'True'") if not (interval is None or isinstance(interval, (Number, datetime.timedelta))): raise ValueError("The 'interval' must be of type 'datetime.timedelta'," " 'int' or 'float'") self._interval = interval @property def interval_seconds(self): """:obj:`int`: The interval for this job in seconds.""" interval = self.interval if isinstance(interval, datetime.timedelta): return interval.total_seconds() else: return interval @property def repeat(self): """:obj:`bool`: Optional. If this job should periodically execute its callback function.""" return self._repeat @repeat.setter def repeat(self, repeat): if self.interval is None and repeat: raise ValueError("'repeat' can not be set to 'True' when no 'interval' is set") self._repeat = repeat @property def days(self): """Tuple[:obj:`int`]: Optional. Defines on which days of the week the job should run.""" return self._days @days.setter def days(self, days): if not isinstance(days, tuple): raise ValueError("The 'days' argument should be of type 'tuple'") if not all(isinstance(day, int) for day in days): raise ValueError("The elements of the 'days' argument should be of type 'int'") if not all(0 <= day <= 6 for day in days): raise ValueError("The elements of the 'days' argument should be from 0 up to and " "including 6") self._days = days @property def job_queue(self): """:class:`telegram.ext.JobQueue`: Optional. The ``JobQueue`` this job belongs to.""" return self._job_queue @job_queue.setter def job_queue(self, job_queue): # Property setter for backward compatibility with JobQueue.put() if not self._job_queue: self._job_queue = weakref.proxy(job_queue) else: raise RuntimeError("The 'job_queue' attribute can only be set once.") def __lt__(self, other): return False python-telegram-bot-12.4.2/telegram/ext/messagehandler.py000066400000000000000000000205641362023133600234430ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 telegram.utils.deprecate import TelegramDeprecationWarning from telegram import Update from telegram.ext import Filters from .handler import Handler class MessageHandler(Handler): """Handler class to handle telegram messages. They might contain text, media or status updates. 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 ``None``. channel_post_updates (:obj:`bool`): Should channel posts updates be handled? Default is ``None``. edited_updates (:obj:`bool`): Should "edited" message updates be handled? Default is ``None``. 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. 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 ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``user_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``chat_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. message_updates (:obj:`bool`, optional): Should "normal" message updates be handled? Default is ``None``. DEPRECATED: Please switch to filters for update filtering. channel_post_updates (:obj:`bool`, optional): Should channel posts updates be handled? Default is ``None``. DEPRECATED: Please switch to filters for update filtering. edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default is ``None``. DEPRECATED: Please switch to filters for update filtering. Raises: ValueError """ def __init__(self, filters, callback, pass_update_queue=False, pass_job_queue=False, pass_user_data=False, pass_chat_data=False, message_updates=None, channel_post_updates=None, edited_updates=None): super(MessageHandler, self).__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) 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') self.filters = filters if self.filters is not None: self.filters &= Filters.update 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): """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update`): Incoming telegram update. Returns: :obj:`bool` """ if isinstance(update, Update) and update.effective_message: return self.filters(update) def collect_additional_context(self, context, update, dispatcher, check_result): if isinstance(check_result, dict): context.update(check_result) python-telegram-bot-12.4.2/telegram/ext/messagequeue.py000066400000000000000000000326731362023133600231560ustar00rootroot00000000000000#!/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-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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.""" from telegram.utils import promise import functools import sys import time import threading if sys.version_info.major > 2: import queue as q else: import Queue as q # We need to count < 1s intervals, so the most accurate timer is needed # Starting from Python 3.3 we have time.perf_counter which is the clock # with the highest resolution available to the system, so let's use it there. # In Python 2.7, there's no perf_counter yet, so fallback on what we have: # on Windows, the best available is time.clock while time.time is on # another platforms (M. Lutz, "Learning Python," 4ed, p.630-634) if sys.version_info.major == 3 and sys.version_info.minor >= 3: curtime = time.perf_counter # pylint: disable=E1101 else: curtime = time.clock if sys.platform[:3] == 'win' else time.time class DelayQueueError(RuntimeError): """Indicates processing errors.""" pass class DelayQueue(threading.Thread): """ Processes callbacks from queue with specified throughput limits. Creates a separate thread to process callbacks with delays. 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. 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 True, processor is started immediately after object's creation; if ``False``, should be started manually by `start` method. Defaults to True. name (:obj:`str`, optional): Thread's name. Defaults to ``'DelayQueue-N'``, where N is sequential number of object created. """ _instcnt = 0 # instance counter def __init__(self, queue=None, burst_limit=30, time_limit_ms=1000, exc_route=None, autostart=True, name=None): 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 = '%s-%s' % (self.__class__.__name__, self.__class__._instcnt) super(DelayQueue, self).__init__(name=name) self.daemon = False if autostart: # immediately start processing super(DelayQueue, self).start() def run(self): """ Do not use the method except for unthreaded testing purposes, the method normally is automatically called by autostart argument. """ times = [] # used to store each callable processing time while True: item = self._queue.get() if self.__exit_req: return # shutdown thread # delay routine now = curtime() 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=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 None, blocks until processor is shut down. Defaults to None. """ self.__exit_req = True # gently request self._queue.put(None) # put something to unfreeze if frozen super(DelayQueue, self).join(timeout=timeout) @staticmethod def _default_exception_handler(exc): """ Dummy exception handler which re-raises exception in thread. Could be possibly overwritten by subclasses. """ raise exc def __call__(self, func, *args, **kwargs): """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 sequenital 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(object): """ 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. 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 True, processors are started immediately after object's creation; if ``False``, should be started manually by :attr:`start` method. Defaults to ``True``. """ def __init__(self, all_burst_limit=30, all_time_limit_ms=1000, group_burst_limit=20, group_time_limit_ms=60000, exc_route=None, autostart=True): # create accoring 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): """Method is used to manually start the ``MessageQueue`` processing.""" self._all_delayq.start() self._group_delayq.start() def stop(self, timeout=None): self._group_delayq.stop(timeout=timeout) self._all_delayq.stop(timeout=timeout) stop.__doc__ = DelayQueue.stop.__doc__ or '' # reuse docsting if any def __call__(self, promise, is_group_msg=False): """ Processes callables in troughput-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 ``True``), or only through *all* ``DelayQueue`` (if set to ``False``), resulting in needed delays to avoid hitting specified limits. Defaults to ``False``. Notes: 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): """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 ``True``, the ``MessageQueue`` is used to process output messages. Defaults to `self._is_queued_out`. isgroup (:obj:`bool`, optional): If set to ``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 ``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, *args, **kwargs): queued = kwargs.pop('queued', self._is_messages_queued_default) isgroup = kwargs.pop('isgroup', False) if queued: prom = promise.Promise(method, (self, ) + args, kwargs) return self._msg_queue(prom, isgroup) return method(self, *args, **kwargs) return wrapped python-telegram-bot-12.4.2/telegram/ext/picklepersistence.py000066400000000000000000000266511362023133600242000ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 copy import deepcopy from telegram.ext import BasePersistence class PicklePersistence(BasePersistence): """Using python's builtin pickle for making you bot persistent. Attributes: filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` is 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 user_data should be saved by this persistence class. store_bot_data (:obj:`bool`): Optional. Whether bot_data should be saved by this persistence class. single_file (:obj:`bool`): Optional. When ``False`` will store 3 sperate files of `filename_user_data`, `filename_chat_data` and `filename_conversations`. Default is ``True``. on_flush (:obj:`bool`, optional): When ``True`` will only save to file when :meth:`flush` is called and keep data in memory until that happens. When ``False`` will store data on any transaction *and* on call fo :meth:`flush`. Default is ``False``. Args: filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file` is 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 ``True``. store_chat_data (:obj:`bool`, optional): Whether user_data should be saved by this persistence class. Default is ``True``. store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this persistence class. Default is ``True`` . single_file (:obj:`bool`, optional): When ``False`` will store 3 sperate files of `filename_user_data`, `filename_chat_data` and `filename_conversations`. Default is ``True``. on_flush (:obj:`bool`, optional): When ``True`` will only save to file when :meth:`flush` is called and keep data in memory until that happens. When ``False`` will store data on any transaction *and* on call fo :meth:`flush`. Default is ``False``. """ def __init__(self, filename, store_user_data=True, store_chat_data=True, store_bot_data=True, single_file=True, on_flush=False): super(PicklePersistence, self).__init__(store_user_data=store_user_data, store_chat_data=store_chat_data, store_bot_data=store_bot_data) self.filename = filename self.single_file = single_file self.on_flush = on_flush self.user_data = None self.chat_data = None self.bot_data = None self.conversations = None def load_singlefile(self): try: filename = self.filename with open(self.filename, "rb") as f: all = pickle.load(f) self.user_data = defaultdict(dict, all['user_data']) self.chat_data = defaultdict(dict, all['chat_data']) # For backwards compatibility with files not containing bot data self.bot_data = all.get('bot_data', {}) self.conversations = all['conversations'] except IOError: self.conversations = {} self.user_data = defaultdict(dict) self.chat_data = defaultdict(dict) self.bot_data = {} except pickle.UnpicklingError: raise TypeError("File {} does not contain valid pickle data".format(filename)) except Exception: raise TypeError("Something went wrong unpickling {}".format(filename)) def load_file(self, filename): try: with open(filename, "rb") as f: return pickle.load(f) except IOError: return None except pickle.UnpicklingError: raise TypeError("File {} does not contain valid pickle data".format(filename)) except Exception: raise TypeError("Something went wrong unpickling {}".format(filename)) def dump_singlefile(self): with open(self.filename, "wb") as f: all = {'conversations': self.conversations, 'user_data': self.user_data, 'chat_data': self.chat_data, 'bot_data': self.bot_data} pickle.dump(all, f) def dump_file(self, filename, data): with open(filename, "wb") as f: pickle.dump(data, f) def get_user_data(self): """Returns the user_data from the pickle file if it exsists or an empty defaultdict. Returns: :obj:`defaultdict`: The restored user data. """ if self.user_data: pass elif not self.single_file: filename = "{}_user_data".format(self.filename) data = self.load_file(filename) if not data: data = defaultdict(dict) else: data = defaultdict(dict, data) self.user_data = data else: self.load_singlefile() return deepcopy(self.user_data) def get_chat_data(self): """Returns the chat_data from the pickle file if it exsists or an empty defaultdict. Returns: :obj:`defaultdict`: The restored chat data. """ if self.chat_data: pass elif not self.single_file: filename = "{}_chat_data".format(self.filename) data = self.load_file(filename) if not data: data = defaultdict(dict) else: data = defaultdict(dict, data) self.chat_data = data else: self.load_singlefile() return deepcopy(self.chat_data) def get_bot_data(self): """Returns the bot_data from the pickle file if it exsists or an empty dict. Returns: :obj:`defaultdict`: The restored bot data. """ if self.bot_data: pass elif not self.single_file: filename = "{}_bot_data".format(self.filename) data = self.load_file(filename) if not data: data = {} self.bot_data = data else: self.load_singlefile() return deepcopy(self.bot_data) def get_conversations(self, name): """Returns the conversations from the pickle file if it exsists or an empty defaultdict. 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 = "{}_conversations".format(self.filename) data = self.load_file(filename) if not data: data = {name: {}} self.conversations = data else: self.load_singlefile() return self.conversations.get(name, {}).copy() def update_conversation(self, name, key, new_state): """Will update the conversations for the given handler and depending on :attr:`on_flush` save the pickle file. Args: name (:obj:`str`): The handlers 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 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 = "{}_conversations".format(self.filename) self.dump_file(filename, self.conversations) else: self.dump_singlefile() def update_user_data(self, user_id, data): """Will update the user_data (if changed) and depending on :attr:`on_flush` save the pickle file. Args: user_id (:obj:`int`): The user the data might have been changed for. data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` [user_id]. """ 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 = "{}_user_data".format(self.filename) self.dump_file(filename, self.user_data) else: self.dump_singlefile() def update_chat_data(self, chat_id, data): """Will update the chat_data (if changed) and depending on :attr:`on_flush` save the pickle file. Args: chat_id (:obj:`int`): The chat the data might have been changed for. data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id]. """ 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 = "{}_chat_data".format(self.filename) self.dump_file(filename, self.chat_data) else: self.dump_singlefile() def update_bot_data(self, data): """Will update the bot_data (if changed) and depending on :attr:`on_flush` save the pickle file. Args: data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.bot_data`. """ if self.bot_data == data: return self.bot_data = data.copy() if not self.on_flush: if not self.single_file: filename = "{}_bot_data".format(self.filename) self.dump_file(filename, self.bot_data) else: self.dump_singlefile() def flush(self): """ Will save all data in memory to pickle file(s). """ if self.single_file: if self.user_data or self.chat_data or self.conversations: self.dump_singlefile() else: if self.user_data: self.dump_file("{}_user_data".format(self.filename), self.user_data) if self.chat_data: self.dump_file("{}_chat_data".format(self.filename), self.chat_data) if self.bot_data: self.dump_file("{}_bot_data".format(self.filename), self.bot_data) if self.conversations: self.dump_file("{}_conversations".format(self.filename), self.conversations) python-telegram-bot-12.4.2/telegram/ext/precheckoutqueryhandler.py000066400000000000000000000103431362023133600254130ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 class PreCheckoutQueryHandler(Handler): """Handler class to handle Telegram PreCheckout callback queries. 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. 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. 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 ``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 ``False``. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``user_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``chat_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. """ def check_update(self, update): """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update`): Incoming telegram update. Returns: :obj:`bool` """ return isinstance(update, Update) and update.pre_checkout_query python-telegram-bot-12.4.2/telegram/ext/regexhandler.py000066400000000000000000000152251362023133600231270ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 telegram.utils.deprecate import TelegramDeprecationWarning from telegram.ext import MessageHandler, Filters 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. 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. Note: This handler is being deprecated. For the same usecase use: ``MessageHandler(Filters.regex(r'pattern'), callback)`` 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 ``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 ``False`` pass_update_queue (:obj:`bool`, optional): If set to ``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 ``False``. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``user_data`` will be passed to the callback function. Default is ``False``. pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``chat_data`` will be passed to the callback function. Default is ``False``. message_updates (:obj:`bool`, optional): Should "normal" message updates be handled? Default is ``True``. channel_post_updates (:obj:`bool`, optional): Should channel posts updates be handled? Default is ``True``. edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default is ``False``. Raises: ValueError """ def __init__(self, pattern, callback, pass_groups=False, pass_groupdict=False, pass_update_queue=False, pass_job_queue=False, pass_user_data=False, pass_chat_data=False, allow_edited=False, message_updates=True, channel_post_updates=False, edited_updates=False): warnings.warn('RegexHandler is deprecated. See https://git.io/fxJuV for more info', TelegramDeprecationWarning, stacklevel=2) super(RegexHandler, self).__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) self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict def collect_optional_args(self, dispatcher, update=None, check_result=None): optional_args = super(RegexHandler, self).collect_optional_args(dispatcher, update, check_result) 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-12.4.2/telegram/ext/shippingqueryhandler.py000066400000000000000000000103261362023133600247210ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 class ShippingQueryHandler(Handler): """Handler class to handle Telegram shipping callback queries. 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. 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. 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 ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``user_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called ``chat_data`` will be passed to the callback function. Default is ``False``. DEPRECATED: Please switch to context based callbacks. """ def check_update(self, update): """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update`): Incoming telegram update. Returns: :obj:`bool` """ return isinstance(update, Update) and update.shipping_query python-telegram-bot-12.4.2/telegram/ext/stringcommandhandler.py000066400000000000000000000116411362023133600246600ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 future.utils import string_types from .handler import Handler class StringCommandHandler(Handler): """Handler class to handle string commands. Commands are string updates that start with ``/``. 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. 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. 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_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 ``False`` DEPRECATED: Please switch to context based callbacks. pass_update_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. """ def __init__(self, command, callback, pass_args=False, pass_update_queue=False, pass_job_queue=False): super(StringCommandHandler, self).__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue) self.command = command self.pass_args = pass_args def check_update(self, update): """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:obj:`str`): An incoming command. Returns: :obj:`bool` """ if isinstance(update, string_types) and update.startswith('/'): args = update[1:].split(' ') if args[0] == self.command: return args[1:] def collect_optional_args(self, dispatcher, update=None, check_result=None): optional_args = super(StringCommandHandler, self).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, update, dispatcher, check_result): context.args = check_result python-telegram-bot-12.4.2/telegram/ext/stringregexhandler.py000066400000000000000000000132411362023133600243520ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 future.utils import string_types from .handler import Handler class StringRegexHandler(Handler): """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. 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. 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 ``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 ``False`` DEPRECATED: Please switch to context based callbacks. pass_update_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. """ def __init__(self, pattern, callback, pass_groups=False, pass_groupdict=False, pass_update_queue=False, pass_job_queue=False): super(StringRegexHandler, self).__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue) if isinstance(pattern, string_types): pattern = re.compile(pattern) self.pattern = pattern self.pass_groups = pass_groups self.pass_groupdict = pass_groupdict def check_update(self, update): """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:obj:`str`): An incoming command. Returns: :obj:`bool` """ if isinstance(update, string_types): match = re.match(self.pattern, update) if match: return match def collect_optional_args(self, dispatcher, update=None, check_result=None): optional_args = super(StringRegexHandler, self).collect_optional_args(dispatcher, update, check_result) if self.pattern: 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, update, dispatcher, check_result): if self.pattern: context.matches = [check_result] python-telegram-bot-12.4.2/telegram/ext/typehandler.py000066400000000000000000000074721362023133600230030ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 .handler import Handler class TypeHandler(Handler): """Handler class to handle updates of custom types. 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 ``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. 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 ``False`` pass_update_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. pass_job_queue (:obj:`bool`, optional): If set to ``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 ``False``. DEPRECATED: Please switch to context based callbacks. """ def __init__(self, type, callback, strict=False, pass_update_queue=False, pass_job_queue=False): super(TypeHandler, self).__init__( callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue) self.type = type self.strict = strict def check_update(self, update): """Determines whether an update should be passed to this handlers :attr:`callback`. Args: update (:class:`telegram.Update`): Incoming telegram update. Returns: :obj:`bool` """ if not self.strict: return isinstance(update, self.type) else: return type(update) is self.type python-telegram-bot-12.4.2/telegram/ext/updater.py000066400000000000000000000627231362023133600221300ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 from threading import Thread, Lock, current_thread, Event from time import sleep from signal import signal, SIGINT, SIGTERM, SIGABRT from queue import Queue from telegram import Bot, TelegramError from telegram.ext import Dispatcher, JobQueue from telegram.error import Unauthorized, InvalidToken, RetryAfter, TimedOut from telegram.utils.helpers import get_signal_name from telegram.utils.request import Request from telegram.utils.webhookhandler import (WebhookServer, WebhookAppClass) logging.getLogger(__name__).addHandler(logging.NullHandler()) class Updater(object): """ 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. Attributes: bot (:class:`telegram.Bot`): The bot used with this Updater. user_sig_handler (:obj:`signal`): signals the updater will respond to. 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): ``True`` if using context based callbacks. 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) setable 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 ``True`` Use the context based callback API (ignored if `dispatcher` argument is used). During the deprecation period of the old API the default is ``False``. **New users**: set this to ``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. Note: You must supply either a :attr:`bot` or a :attr:`token` argument. Raises: ValueError: If both :attr:`token` and :attr:`bot` are passed or none of them. """ _request = None def __init__(self, token=None, base_url=None, workers=4, bot=None, private_key=None, private_key_password=None, user_sig_handler=None, request_kwargs=None, persistence=None, defaults=None, use_context=False, dispatcher=None, base_file_url=None): 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 workers is not None: raise ValueError('`dispatcher` and `workers` are mutually exclusive') if use_context != dispatcher.use_context: raise ValueError('`dispatcher` and `use_context` are mutually exclusive') self.logger = logging.getLogger(__name__) 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 = Bot(token, base_url, base_file_url=base_file_url, request=self._request, private_key=private_key, private_key_password=private_key_password, defaults=defaults) self.update_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) 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 = [] # Just for passing to WebhookAppClass self._default_quote = defaults.quote if defaults else None def _init_thread(self, target, name, *args, **kwargs): thr = Thread(target=self._thread_wrapper, name="Bot:{}:{}".format(self.bot.id, name), args=(target,) + args, kwargs=kwargs) thr.start() self.__threads.append(thr) def _thread_wrapper(self, target, *args, **kwargs): thr_name = current_thread().name self.logger.debug('{0} - started'.format(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('{0} - ended'.format(thr_name)) def start_polling(self, poll_interval=0.0, timeout=10, clean=False, bootstrap_retries=-1, read_latency=2., allowed_updates=None): """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 :attr:`telegram.Bot.get_updates`. clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers before actually starting to poll. Default is False. bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the `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 :attr:`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. """ with self.__lock: if not self.running: self.running = True # Create & start threads self.job_queue.start() dispatcher_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, clean, allowed_updates) dispatcher_ready.wait() # Return the update queue so the main thread can insert updates return self.update_queue def start_webhook(self, listen='127.0.0.1', port=80, url_path='', cert=None, key=None, clean=False, bootstrap_retries=0, webhook_url=None, allowed_updates=None): """ Starts a small http server to listen for updates via webhook. If cert and 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 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. clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers before actually starting the webhook. Default is ``False``. bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the `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`. allowed_updates (List[:obj:`str`], optional): Passed to :attr:`telegram.Bot.set_webhook`. Returns: :obj:`Queue`: The update queue that can be filled from the main thread. """ with self.__lock: if not self.running: self.running = True # Create & start threads self.job_queue.start() self._init_thread(self.dispatcher.start, "dispatcher"), self._init_thread(self._start_webhook, "updater", listen, port, url_path, cert, key, bootstrap_retries, clean, webhook_url, allowed_updates) # Return the update queue so the main thread can insert updates return self.update_queue def _start_polling(self, poll_interval, timeout, read_latency, bootstrap_retries, clean, allowed_updates): # 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, clean=clean, 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) self._network_loop_retry(polling_action_cb, polling_onerr_cb, 'getting Updates', poll_interval) 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 False or return value of `action_cb` evaluates 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 e: self.logger.info('%s', e) cur_interval = 0.5 + e.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 te: self.logger.error('Error while %s: %s', description, te) onerr_cb(te) 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): # increase waiting times on subsequent errors up to 30secs if current_interval == 0: current_interval = 1 elif current_interval < 30: current_interval += current_interval / 2 elif current_interval > 30: current_interval = 30 return current_interval def _start_webhook(self, listen, port, url_path, cert, key, bootstrap_retries, clean, webhook_url, allowed_updates): self.logger.debug('Updater thread started (webhook)') use_ssl = cert is not None and key is not None if not url_path.startswith('/'): url_path = '/{0}'.format(url_path) # Create Tornado app instance app = WebhookAppClass(url_path, self.bot, self.update_queue, default_quote=self._default_quote) # 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: raise TelegramError('Invalid SSL Certificate') else: ssl_ctx = None # Create and start server self.httpd = WebhookServer(listen, port, app, ssl_ctx) if use_ssl: # DO NOT CHANGE: Only set webhook if SSL is handled by library if not webhook_url: webhook_url = self._gen_webhook_url(listen, port, url_path) self._bootstrap(max_retries=bootstrap_retries, clean=clean, webhook_url=webhook_url, cert=open(cert, 'rb'), allowed_updates=allowed_updates) elif clean: self.logger.warning("cleaning updates is not supported if " "SSL-termination happens elsewhere; skipping") self.httpd.serve_forever() @staticmethod def _gen_webhook_url(listen, port, url_path): return 'https://{listen}:{port}{path}'.format(listen=listen, port=port, path=url_path) def _bootstrap(self, max_retries, clean, webhook_url, allowed_updates, cert=None, bootstrap_interval=5): retries = [0] def bootstrap_del_webhook(): self.bot.delete_webhook() return False def bootstrap_clean_updates(): self.logger.debug('Cleaning updates from Telegram server') updates = self.bot.get_updates() while updates: updates = self.bot.get_updates(updates[-1].update_id + 1) return False def bootstrap_set_webhook(): self.bot.set_webhook(url=webhook_url, certificate=cert, allowed_updates=allowed_updates) 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 # Cleaning pending messages is done by polling for them - so we need to delete webhook if # one is configured. # We also take this chance to delete pre-configured webhook if this is a polling Updater. # NOTE: We don't know ahead if a webhook is configured, so we just delete. if clean or not webhook_url: self._network_loop_retry(bootstrap_del_webhook, bootstrap_onerr_cb, 'bootstrap del webhook', bootstrap_interval) retries[0] = 0 # Clean pending messages, if requested. if clean: self._network_loop_retry(bootstrap_clean_updates, bootstrap_onerr_cb, 'bootstrap clean updates', bootstrap_interval) retries[0] = 0 sleep(1) # 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): """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() def _stop_httpd(self): 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 def _stop_dispatcher(self): self.logger.debug('Requesting Dispatcher to stop...') self.dispatcher.stop() def _join_threads(self): for thr in self.__threads: self.logger.debug('Waiting for {0} thread to end'.format(thr.name)) thr.join() self.logger.debug('{0} thread has ended'.format(thr.name)) self.__threads = [] def signal_handler(self, signum, frame): self.is_idle = False if self.running: self.logger.info('Received signal {} ({}), stopping...'.format( signum, get_signal_name(signum))) if self.persistence: # Update user_data and chat_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!') import os os._exit(1) def idle(self, stop_signals=(SIGINT, SIGTERM, SIGABRT)): """Blocks until one of the signals are received and stops the updater. Args: stop_signals (:obj:`iterable`): Iterable containing signals from the signal module that should be subscribed to. 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-12.4.2/telegram/files/000077500000000000000000000000001362023133600204025ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/files/__init__.py000066400000000000000000000000001362023133600225010ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/files/animation.py000066400000000000000000000100241362023133600227300ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import PhotoSize from telegram import TelegramObject class Animation(TelegramObject): """This object represents an animation file to be displayed in the message containing a game. Attributes: file_id (:obj:`str`): Unique file identifier. 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. Args: file_id (:obj:`str`): Unique file identifier. 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. """ def __init__(self, file_id, width, height, duration, thumb=None, file_name=None, mime_type=None, file_size=None, bot=None, **kwargs): # Required self.file_id = str(file_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_id,) @classmethod def de_json(cls, data, bot): if not data: return None data = super(Animation, cls).de_json(data, bot) data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) def get_file(self, timeout=None, **kwargs): """Convenience wrapper over :attr:`telegram.Bot.get_file` 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) python-telegram-bot-12.4.2/telegram/files/audio.py000066400000000000000000000076471362023133600220730ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject, PhotoSize class Audio(TelegramObject): """This object represents an audio file to be treated as music by the Telegram clients. Attributes: file_id (:obj:`str`): Unique identifier for this 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. 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. Args: file_id (:obj:`str`): Unique identifier for this 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. 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. """ def __init__(self, file_id, duration, performer=None, title=None, mime_type=None, file_size=None, thumb=None, bot=None, **kwargs): # Required self.file_id = str(file_id) self.duration = int(duration) # Optionals self.performer = performer self.title = title self.mime_type = mime_type self.file_size = file_size self.thumb = thumb self.bot = bot self._id_attrs = (self.file_id,) @classmethod def de_json(cls, data, bot): if not data: return None data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) def get_file(self, timeout=None, **kwargs): """Convenience wrapper over :attr:`telegram.Bot.get_file` 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) python-telegram-bot-12.4.2/telegram/files/chatphoto.py000066400000000000000000000066301362023133600227520ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class ChatPhoto(TelegramObject): """This object represents a chat photo. Attributes: small_file_id (:obj:`str`): File identifier of small (160x160) chat photo. big_file_id (:obj:`str`): File identifier of big (640x640) chat photo. Args: 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. 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, small_file_id, big_file_id, bot=None, **kwargs): self.small_file_id = small_file_id self.big_file_id = big_file_id self.bot = bot self._id_attrs = (self.small_file_id, self.big_file_id) @classmethod def de_json(cls, data, bot): if not data: return None return cls(bot=bot, **data) def get_small_file(self, timeout=None, **kwargs): """Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the small (160x160) chat photo 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ return self.bot.get_file(self.small_file_id, timeout=timeout, **kwargs) def get_big_file(self, timeout=None, **kwargs): """Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the big (640x640) chat photo 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ return self.bot.get_file(self.big_file_id, timeout=timeout, **kwargs) python-telegram-bot-12.4.2/telegram/files/contact.py000066400000000000000000000044031362023133600224100ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class Contact(TelegramObject): """This object represents a phone contact. 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. 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. """ def __init__(self, phone_number, first_name, last_name=None, user_id=None, vcard=None, **kwargs): # 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,) @classmethod def de_json(cls, data, bot): if not data: return None return cls(**data) python-telegram-bot-12.4.2/telegram/files/document.py000066400000000000000000000065321362023133600226000ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import PhotoSize, TelegramObject class Document(TelegramObject): """This object represents a general file (as opposed to photos, voice messages and audio files). Attributes: file_id (:obj:`str`): Unique file identifier. 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. Args: file_id (:obj:`str`): Unique file identifier 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. """ _id_keys = ('file_id',) def __init__(self, file_id, thumb=None, file_name=None, mime_type=None, file_size=None, bot=None, **kwargs): # Required self.file_id = str(file_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_id,) @classmethod def de_json(cls, data, bot): if not data: return None data = super(Document, cls).de_json(data, bot) data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) def get_file(self, timeout=None, **kwargs): """Convenience wrapper over :attr:`telegram.Bot.get_file` 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) python-telegram-bot-12.4.2/telegram/files/file.py000066400000000000000000000140641362023133600217000ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" from base64 import b64decode from os.path import basename import os from future.backports.urllib import parse as urllib_parse from telegram import TelegramObject from telegram.passport.credentials import decrypt 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 getFile. Note: Maximum file size to download is 20 MB Attributes: file_id (:obj:`str`): Unique identifier for this file. file_size (:obj:`str`): Optional. File size. file_path (:obj:`str`): Optional. File path. Use :attr:`download` to get the file. Args: file_id (:obj:`str`): Unique identifier for this 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. Note: 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()`. """ def __init__(self, file_id, bot=None, file_size=None, file_path=None, **kwargs): # Required self.file_id = str(file_id) # Optionals self.file_size = file_size self.file_path = file_path self.bot = bot self._credentials = None self._id_attrs = (self.file_id,) @classmethod def de_json(cls, data, bot): if not data: return None return cls(bot=bot, **data) def download(self, custom_path=None, out=None, timeout=None): """ 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. 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. 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') # Convert any UTF-8 char into a url encoded ASCII string. url = self._get_encoded_url() if out: 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 else: if custom_path: filename = custom_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): """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=None): """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() buf.extend(self.bot.request.retrieve(self._get_encoded_url())) return buf def set_credentials(self, credentials): self._credentials = credentials python-telegram-bot-12.4.2/telegram/files/inputfile.py000066400000000000000000000065231362023133600227610ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=W0622,E0611 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 mimetypes import os from uuid import uuid4 from telegram import TelegramError DEFAULT_MIME_TYPE = 'application/octet-stream' class InputFile(object): """This object represents a Telegram InputFile. Attributes: input_file_content (:obj:`bytes`): The binaray 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. Args: obj (:obj:`File handler`): An open file descriptor. 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 """ def __init__(self, obj, filename=None, attach=None): self.filename = None 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) # py3 and obj.name != ''): # py2 # on py2.7, pylint fails to understand this properly # pylint: disable=E1101 self.filename = os.path.basename(obj.name) try: self.mimetype = self.is_image(self.input_file_content) except TelegramError: if 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('/', '.') @property def field_tuple(self): return self.filename, self.input_file_content, self.mimetype @staticmethod def is_image(stream): """Check if the content file is an image by analyzing its headers. Args: stream (:obj:`str`): A str representing the content of a file. Returns: :obj:`str`: The str mime-type of an image. """ image = imghdr.what(None, stream) if image: return 'image/%s' % image raise TelegramError('Could not parse file content') @staticmethod def is_file(obj): return hasattr(obj, 'read') def to_dict(self): if self.attach: return 'attach://' + self.attach python-telegram-bot-12.4.2/telegram/files/inputmedia.py000066400000000000000000000427331362023133600231240ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 telegram import TelegramObject, InputFile, PhotoSize, Animation, Video, Audio, Document from telegram.utils.helpers import DEFAULT_NONE 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. """ pass class InputMediaAnimation(InputMedia): """Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. Attributes: type (:obj:`str`): ``animation``. media (:obj:`str` | `filelike object` | :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. thumb (`filelike object`): Optional. Thumbnail of the file sent. 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 is passed as a string or file_id. caption (:obj:`str`): Optional. Caption of the animation to be sent, 0-1024 characters. 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. width (:obj:`int`): Optional. Animation width. height (:obj:`int`): Optional. Animation height. duration (:obj:`int`): Optional. Animation duration. Args: media (:obj:`str`): 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. thumb (`filelike object`, optional): Thumbnail of the file sent. 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 is passed as a string or file_id. caption (:obj:`str`, optional): Caption of the animation to be sent, 0-1024 characters. 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. width (:obj:`int`, optional): Animation width. height (:obj:`int`, optional): Animation height. duration (:obj:`int`, optional): Animation duration. 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. """ def __init__(self, media, thumb=None, caption=None, parse_mode=DEFAULT_NONE, width=None, height=None, duration=None): self.type = 'animation' if isinstance(media, Animation): self.media = media.file_id self.width = media.width self.height = media.height self.duration = media.duration elif InputFile.is_file(media): self.media = InputFile(media, attach=True) else: self.media = media if thumb: self.thumb = thumb if InputFile.is_file(self.thumb): self.thumb = InputFile(self.thumb, attach=True) if caption: self.caption = caption self.parse_mode = parse_mode if width: self.width = width if height: self.height = height if duration: self.duration = duration class InputMediaPhoto(InputMedia): """Represents a photo to be sent. Attributes: type (:obj:`str`): ``photo``. media (:obj:`str` | `filelike object` | :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. caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-1024 characters. 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. Args: media (:obj:`str`): 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. caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-1024 characters. 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. """ def __init__(self, media, caption=None, parse_mode=DEFAULT_NONE): self.type = 'photo' if isinstance(media, PhotoSize): self.media = media.file_id elif InputFile.is_file(media): self.media = InputFile(media, attach=True) else: self.media = media if caption: self.caption = caption self.parse_mode = parse_mode class InputMediaVideo(InputMedia): """Represents a video to be sent. Attributes: type (:obj:`str`): ``video``. media (:obj:`str` | `filelike object` | :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. caption (:obj:`str`): Optional. Caption of the video to be sent, 0-1024 characters. 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. width (:obj:`int`): Optional. Video width. height (:obj:`int`): Optional. Video height. duration (:obj:`int`): Optional. Video duration. supports_streaming (:obj:`bool`): Optional. Pass True, if the uploaded video is suitable for streaming. thumb (`filelike object`): Optional. Thumbnail of the file sent. 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 is passed as a string or file_id. Args: media (:obj:`str`): 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. caption (:obj:`str`, optional): Caption of the video to be sent, 0-1024 characters. 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. width (:obj:`int`, optional): Video width. height (:obj:`int`, optional): Video height. duration (:obj:`int`, optional): Video duration. supports_streaming (:obj:`bool`, optional): Pass True, if the uploaded video is suitable for streaming. thumb (`filelike object`, optional): Thumbnail of the file sent. 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 is passed as a string or file_id. 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. """ def __init__(self, media, caption=None, width=None, height=None, duration=None, supports_streaming=None, parse_mode=DEFAULT_NONE, thumb=None): self.type = 'video' if isinstance(media, Video): self.media = media.file_id self.width = media.width self.height = media.height self.duration = media.duration elif InputFile.is_file(media): self.media = InputFile(media, attach=True) else: self.media = media if thumb: self.thumb = thumb if InputFile.is_file(self.thumb): self.thumb = InputFile(self.thumb, attach=True) if caption: self.caption = caption self.parse_mode = parse_mode 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. Attributes: type (:obj:`str`): ``audio``. media (:obj:`str` | `filelike object` | :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. caption (:obj:`str`): Optional. Caption of the audio to be sent, 0-1024 characters. 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. 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 (`filelike object`): Optional. Thumbnail of the file sent. 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 is passed as a string or file_id. Args: media (:obj:`str`): 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. caption (:obj:`str`, optional): Caption of the audio to be sent, 0-1024 characters. 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. 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`, optional): Thumbnail of the file sent. 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 is passed as a string or file_id. 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. """ def __init__(self, media, thumb=None, caption=None, parse_mode=DEFAULT_NONE, duration=None, performer=None, title=None): self.type = 'audio' if isinstance(media, Audio): self.media = media.file_id self.duration = media.duration self.performer = media.performer self.title = media.title elif InputFile.is_file(media): self.media = InputFile(media, attach=True) else: self.media = media if thumb: self.thumb = thumb if InputFile.is_file(self.thumb): self.thumb = InputFile(self.thumb, attach=True) if caption: self.caption = caption self.parse_mode = parse_mode 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. Attributes: type (:obj:`str`): ``document``. media (:obj:`str` | `filelike object` | :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. caption (:obj:`str`): Optional. Caption of the document to be sent, 0-1024 characters. 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. thumb (`filelike object`): Optional. Thumbnail of the file sent. 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 is passed as a string or file_id. Args: media (:obj:`str`): 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. caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters. 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. thumb (`filelike object`, optional): Thumbnail of the file sent. 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 is passed as a string or file_id. """ def __init__(self, media, thumb=None, caption=None, parse_mode=DEFAULT_NONE): self.type = 'document' if isinstance(media, Document): self.media = media.file_id elif InputFile.is_file(media): self.media = InputFile(media, attach=True) else: self.media = media if thumb: self.thumb = thumb if InputFile.is_file(self.thumb): self.thumb = InputFile(self.thumb, attach=True) if caption: self.caption = caption self.parse_mode = parse_mode python-telegram-bot-12.4.2/telegram/files/location.py000066400000000000000000000032451362023133600225700ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class Location(TelegramObject): """This object represents a point on the map. Attributes: longitude (:obj:`float`): Longitude as defined by sender. latitude (:obj:`float`): Latitude as defined by sender. Args: longitude (:obj:`float`): Longitude as defined by sender. latitude (:obj:`float`): Latitude as defined by sender. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, longitude, latitude, **kwargs): # Required self.longitude = float(longitude) self.latitude = float(latitude) self._id_attrs = (self.longitude, self.latitude) @classmethod def de_json(cls, data, bot): if not data: return None return cls(**data) python-telegram-bot-12.4.2/telegram/files/photosize.py000066400000000000000000000057011362023133600230030ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class PhotoSize(TelegramObject): """This object represents one size of a photo or a file/sticker thumbnail. Attributes: file_id (:obj:`str`): Unique identifier for this 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. Args: file_id (:obj:`str`): Unique identifier for this 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. """ def __init__(self, file_id, width, height, file_size=None, bot=None, **kwargs): # Required self.file_id = str(file_id) self.width = int(width) self.height = int(height) # Optionals self.file_size = file_size self.bot = bot self._id_attrs = (self.file_id,) @classmethod def de_json(cls, data, bot): if not data: return None return cls(bot=bot, **data) @classmethod def de_list(cls, data, bot): if not data: return [] photos = list() for photo in data: photos.append(cls.de_json(photo, bot)) return photos def get_file(self, timeout=None, **kwargs): """Convenience wrapper over :attr:`telegram.Bot.get_file` 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) python-telegram-bot-12.4.2/telegram/files/sticker.py000066400000000000000000000175201362023133600224250ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import PhotoSize, TelegramObject class Sticker(TelegramObject): """This object represents a sticker. Attributes: file_id (:obj:`str`): Unique identifier for this file. width (:obj:`int`): Sticker width. height (:obj:`int`): Sticker height. is_animated (:obj:`bool`): True, if the sticker is animated. 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. Args: file_id (:obj:`str`): Unique identifier for this file. width (:obj:`int`): Sticker width. height (:obj:`int`): Sticker height. is_animated (:obj:`bool`): True, if the sticker is animated. 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. **kwargs (obj:`dict`): Arbitrary keyword arguments.7 bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. """ def __init__(self, file_id, width, height, is_animated, thumb=None, emoji=None, file_size=None, set_name=None, mask_position=None, bot=None, **kwargs): # Required self.file_id = str(file_id) self.width = int(width) self.height = int(height) self.is_animated = is_animated # 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_id,) @classmethod def de_json(cls, data, bot): if not data: return None data = super(Sticker, cls).de_json(data, bot) 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) @classmethod def de_list(cls, data, bot): if not data: return list() return [cls.de_json(d, bot) for d in data] def get_file(self, timeout=None, **kwargs): """Convenience wrapper over :attr:`telegram.Bot.get_file` 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) class StickerSet(TelegramObject): """This object represents a sticker set. Attributes: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. is_animated (:obj:`bool`): True, if the sticker set contains animated stickers. contains_masks (:obj:`bool`): True, if the sticker set contains masks. stickers (List[:class:`telegram.Sticker`]): List of all set stickers. Args: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. is_animated (:obj:`bool`): True, if the sticker set contains animated stickers. contains_masks (:obj:`bool`): True, if the sticker set contains masks. stickers (List[:class:`telegram.Sticker`]): List of all set stickers. """ def __init__(self, name, title, is_animated, contains_masks, stickers, bot=None, **kwargs): self.name = name self.title = title self.is_animated = is_animated self.contains_masks = contains_masks self.stickers = stickers self._id_attrs = (self.name,) @staticmethod def de_json(data, bot): if not data: return None data = super(StickerSet, StickerSet).de_json(data, bot) data['stickers'] = Sticker.de_list(data.get('stickers'), bot) return StickerSet(bot=bot, **data) def to_dict(self): data = super(StickerSet, self).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. Attributes: point (:obj:`str`): The part of the face relative to which the mask should be placed. 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. Notes: :attr:`type` should be one of the following: `forehead`, `eyes`, `mouth` or `chin`. You can use the classconstants for those. Args: point (:obj:`str`): The part of the face relative to which the mask should be placed. 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. """ FOREHEAD = 'forehead' """:obj:`str`: 'forehead'""" EYES = 'eyes' """:obj:`str`: 'eyes'""" MOUTH = 'mouth' """:obj:`str`: 'mouth'""" CHIN = 'chin' """:obj:`str`: 'chin'""" def __init__(self, point, x_shift, y_shift, scale, **kwargs): self.point = point self.x_shift = x_shift self.y_shift = y_shift self.scale = scale @classmethod def de_json(cls, data, bot): if data is None: return None return cls(**data) python-telegram-bot-12.4.2/telegram/files/venue.py000066400000000000000000000050441362023133600221010ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject, Location class Venue(TelegramObject): """This object represents a venue. Attributes: location (:class:`telegram.Location`): Venue location. title (:obj:`str`): Name of the venue. address (:obj:`str`): Address of the venue. foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue. foursquare_type (:obj:`str`): Optional. Foursquare type of the venue. (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".) 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".) **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, location, title, address, foursquare_id=None, foursquare_type=None, **kwargs): # Required self.location = location self.title = title self.address = address # Optionals self.foursquare_id = foursquare_id self.foursquare_type = foursquare_type self._id_attrs = (self.location, self.title) @classmethod def de_json(cls, data, bot): data = super(Venue, cls).de_json(data, bot) if not data: return None data['location'] = Location.de_json(data.get('location'), bot) return cls(**data) python-telegram-bot-12.4.2/telegram/files/video.py000066400000000000000000000072301362023133600220640ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import PhotoSize, TelegramObject class Video(TelegramObject): """This object represents a video file. Attributes: file_id (:obj:`str`): Unique identifier for this 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. 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. Args: file_id (:obj:`str`): Unique identifier for this 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. 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. """ def __init__(self, file_id, width, height, duration, thumb=None, mime_type=None, file_size=None, bot=None, **kwargs): # Required self.file_id = str(file_id) self.width = int(width) self.height = int(height) self.duration = int(duration) # Optionals self.thumb = thumb self.mime_type = mime_type self.file_size = file_size self.bot = bot self._id_attrs = (self.file_id,) @classmethod def de_json(cls, data, bot): if not data: return None data = super(Video, cls).de_json(data, bot) data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) def get_file(self, timeout=None, **kwargs): """Convenience wrapper over :attr:`telegram.Bot.get_file` 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) python-telegram-bot-12.4.2/telegram/files/videonote.py000066400000000000000000000063151362023133600227550ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import PhotoSize, TelegramObject class VideoNote(TelegramObject): """This object represents a video message (available in Telegram apps as of v.4.0). Attributes: file_id (:obj:`str`): Unique identifier for this 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. Args: file_id (:obj:`str`): Unique identifier for this 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, file_id, length, duration, thumb=None, file_size=None, bot=None, **kwargs): # Required self.file_id = str(file_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_id,) @classmethod def de_json(cls, data, bot): if not data: return None data = super(VideoNote, cls).de_json(data, bot) data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) return cls(bot=bot, **data) def get_file(self, timeout=None, **kwargs): """Convenience wrapper over :attr:`telegram.Bot.get_file` 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) python-telegram-bot-12.4.2/telegram/files/voice.py000066400000000000000000000056731362023133600220740ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class Voice(TelegramObject): """This object represents a voice note. Attributes: file_id (:obj:`str`): Unique identifier for this 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. Args: file_id (:obj:`str`): Unique identifier for this 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. """ def __init__(self, file_id, duration, mime_type=None, file_size=None, bot=None, **kwargs): # Required self.file_id = str(file_id) self.duration = int(duration) # Optionals self.mime_type = mime_type self.file_size = file_size self.bot = bot self._id_attrs = (self.file_id,) @classmethod def de_json(cls, data, bot): if not data: return None data = super(Voice, cls).de_json(data, bot) return cls(bot=bot, **data) def get_file(self, timeout=None, **kwargs): """Convenience wrapper over :attr:`telegram.Bot.get_file` 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ return self.bot.get_file(self.file_id, timeout=timeout, **kwargs) python-telegram-bot-12.4.2/telegram/forcereply.py000066400000000000000000000040141362023133600220230ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 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. Attributes: force_reply (:obj:`True`): Shows reply interface to the user. selective (:obj:`bool`): Optional. Force reply from specific users only. 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 text of the 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. """ def __init__(self, force_reply=True, selective=False, **kwargs): # Required self.force_reply = bool(force_reply) # Optionals self.selective = bool(selective) python-telegram-bot-12.4.2/telegram/games/000077500000000000000000000000001362023133600203745ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/games/__init__.py000066400000000000000000000000001362023133600224730ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/games/callbackgame.py000066400000000000000000000020361362023133600233350ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" python-telegram-bot-12.4.2/telegram/games/game.py000066400000000000000000000143571362023133600216710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import MessageEntity, TelegramObject, Animation, PhotoSize class Game(TelegramObject): """ This object represents a game. Use BotFather to create and edit games, their short names will act as unique identifiers. 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 set_game_score, or manually edited using 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. 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 set_game_score, or manually edited using 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. """ def __init__(self, title, description, photo, text=None, text_entities=None, animation=None, **kwargs): self.title = title self.description = description self.photo = photo self.text = text self.text_entities = text_entities or list() self.animation = animation @classmethod def de_json(cls, data, bot): if not data: return None data = super(Game, cls).de_json(data, bot) 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): data = super(Game, self).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): """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. """ # 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] else: 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=None): """ 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 if entity.type in types } python-telegram-bot-12.4.2/telegram/games/gamehighscore.py000066400000000000000000000033501362023133600235540ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject, User class GameHighScore(TelegramObject): """This object represents one row of the high scores table for a game. Attributes: position (:obj:`int`): Position in high score table for the game. user (:class:`telegram.User`): User. score (:obj:`int`): Score. Args: position (:obj:`int`): Position in high score table for the game. user (:class:`telegram.User`): User. score (:obj:`int`): Score. """ def __init__(self, position, user, score): self.position = position self.user = user self.score = score @classmethod def de_json(cls, data, bot): if not data: return None data = super(GameHighScore, cls).de_json(data, bot) data['user'] = User.de_json(data.get('user'), bot) return cls(**data) python-telegram-bot-12.4.2/telegram/inline/000077500000000000000000000000001362023133600205565ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/inline/__init__.py000066400000000000000000000000001362023133600226550ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/inline/inlinekeyboardbutton.py000066400000000000000000000120041362023133600253600ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class InlineKeyboardButton(TelegramObject): """This object represents one button of an inline keyboard. 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. Attributes: text (:obj:`str`): Label text on the button. url (:obj:`str`): Optional. HTTP url to be opened when button is pressed. login_url (:class:`telegram.LoginUrl`) Optional. An HTTP URL used to automatically authorize the user. callback_data (:obj:`str`): 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. 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. 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 True, to send a Pay button. Args: text (:obj:`str`): Label text on the button. url (:obj:`str`): HTTP url to be opened when button is pressed. login_url (:class:`telegram.LoginUrl`, optional) An HTTP URL used to automatically authorize the user. callback_data (:obj:`str`, optional): Data to be sent in a callback query to the bot when button is pressed, 1-64 UTF-8 bytes. 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 True, to send a Pay button. This type of button must always be the ``first`` button in the first row. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, text, url=None, callback_data=None, switch_inline_query=None, switch_inline_query_current_chat=None, callback_game=None, pay=None, login_url=None, **kwargs): # 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 @classmethod def de_json(cls, data, bot): if not data: return None return cls(**data) python-telegram-bot-12.4.2/telegram/inline/inlinekeyboardmarkup.py000066400000000000000000000073271362023133600253600ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import ReplyMarkup, InlineKeyboardButton class InlineKeyboardMarkup(ReplyMarkup): """ This object represents an inline keyboard that appears right next to the message it belongs to. Attributes: inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): Array of button rows, each represented by an Array of InlineKeyboardButton objects. Args: inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): Array of button rows, each represented by an Array of InlineKeyboardButton objects. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, inline_keyboard, **kwargs): # Required self.inline_keyboard = inline_keyboard def to_dict(self): data = super(InlineKeyboardMarkup, self).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, bot): if not data: return None keyboard = [] for row in data['inline_keyboard']: tmp = [] for col in row: tmp.append(InlineKeyboardButton.de_json(col, bot)) keyboard.append(tmp) return cls(keyboard) @classmethod def from_button(cls, button, **kwargs): """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, **kwargs): """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, **kwargs): """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) python-telegram-bot-12.4.2/telegram/inline/inlinequery.py000066400000000000000000000106771362023133600235070ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0902,R0912,R0913 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject, User, Location 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. Note: * In Python `from` is a reserved word, use `from_user` instead. Attributes: id (:obj:`str`): Unique identifier for this query. from_user (:class:`telegram.User`): Sender. location (:class:`telegram.Location`): Optional. Sender location, only for bots that request user location. 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. Args: id (:obj:`str`): Unique identifier for this query. from_user (:class:`telegram.User`): Sender. location (:class:`telegram.Location`, optional): Sender location, only for bots that request user location. 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, id, from_user, query, offset, location=None, bot=None, **kwargs): # Required self.id = id self.from_user = from_user self.query = query self.offset = offset # Optional self.location = location self.bot = bot self._id_attrs = (self.id,) @classmethod def de_json(cls, data, bot): data = super(InlineQuery, cls).de_json(data, bot) 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, *args, **kwargs): """Shortcut for:: bot.answer_inline_query(update.inline_query.id, *args, **kwargs) Args: results (List[:class:`telegram.InlineQueryResult`]): A list of results for the inline query. 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 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. """ return self.bot.answer_inline_query(self.id, *args, **kwargs) python-telegram-bot-12.4.2/telegram/inline/inlinequeryresult.py000066400000000000000000000032651362023133600247410ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 InlineQueryResult.""" from telegram import TelegramObject class InlineQueryResult(TelegramObject): """Baseclass for the InlineQueryResult* classes. Attributes: type (:obj:`str`): Type of the result. id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. 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. """ def __init__(self, type, id, **kwargs): # Required self.type = str(type) self.id = str(id) self._id_attrs = (self.id,) @property def _has_parse_mode(self): return hasattr(self, 'parse_mode') @property def _has_input_message_content(self): return hasattr(self, 'input_message_content') python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultarticle.py000066400000000000000000000070571362023133600263100ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult class InlineQueryResultArticle(InlineQueryResult): """This object represents a Telegram InlineQueryResultArticle. 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 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. 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 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. """ def __init__(self, id, title, input_message_content, reply_markup=None, url=None, hide_url=None, description=None, thumb_url=None, thumb_width=None, thumb_height=None, **kwargs): # Required super(InlineQueryResultArticle, self).__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-12.4.2/telegram/inline/inlinequeryresultaudio.py000066400000000000000000000076221362023133600257640ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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. Caption, 0-200 characters. audio_duration (:obj:`str`): Optional. Performer. caption (:obj:`str`): Optional. Audio duration in seconds. 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. 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. 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): Caption, 0-200 characters. audio_duration (:obj:`str`, optional): Performer. caption (:obj:`str`, optional): Audio duration in seconds. 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. 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. """ def __init__(self, id, audio_url, title, performer=None, audio_duration=None, caption=None, reply_markup=None, input_message_content=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultAudio, self).__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.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultcachedaudio.py000066400000000000000000000067371362023133600271220ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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 amessage with the specified content instead of the audio. 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 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. 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. 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 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. 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. """ def __init__(self, id, audio_file_id, caption=None, reply_markup=None, input_message_content=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultCachedAudio, self).__init__('audio', id) self.audio_file_id = audio_file_id # Optionals self.caption = caption self.parse_mode = parse_mode self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultcacheddocument.py000066400000000000000000000075641362023133600276360ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedDocument.""" from telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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, 0-1024 characters 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. 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. 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, 0-1024 characters 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. 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. """ def __init__(self, id, title, document_file_id, description=None, caption=None, reply_markup=None, input_message_content=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultCachedDocument, self).__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.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultcachedgif.py000066400000000000000000000072721362023133600265610ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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, 0-1024 characters 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. 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. 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, 0-1024 characters 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. 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. """ def __init__(self, id, gif_file_id, title=None, caption=None, reply_markup=None, input_message_content=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultCachedGif, self).__init__('gif', id) self.gif_file_id = gif_file_id # Optionals self.title = title self.caption = caption self.parse_mode = parse_mode self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultcachedmpeg4gif.py000066400000000000000000000073531362023133600275160ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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, 0-1024 characters parse_mode (:obj:`str`): 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. 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. 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, 0-1024 characters 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. 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. """ def __init__(self, id, mpeg4_file_id, title=None, caption=None, reply_markup=None, input_message_content=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultCachedMpeg4Gif, self).__init__('mpeg4_gif', id) self.mpeg4_file_id = mpeg4_file_id # Optionals self.title = title self.caption = caption self.parse_mode = parse_mode self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultcachedphoto.py000066400000000000000000000075571362023133600271530ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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, 0-1024 characters 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. 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. 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, 0-1024 characters 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. 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. """ def __init__(self, id, photo_file_id, title=None, description=None, caption=None, reply_markup=None, input_message_content=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultCachedPhoto, self).__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.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultcachedsticker.py000066400000000000000000000050711362023133600274530ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult 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. 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. Args: id (:obj:`str`): sticker_file_id (:obj:`str`): 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. """ def __init__(self, id, sticker_file_id, reply_markup=None, input_message_content=None, **kwargs): # Required super(InlineQueryResultCachedSticker, self).__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-12.4.2/telegram/inline/inlinequeryresultcachedvideo.py000066400000000000000000000075651362023133600271270ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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, 0-1024 characters. 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. 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. 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, 0-1024 characters. 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. 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. """ def __init__(self, id, video_file_id, title, description=None, caption=None, reply_markup=None, input_message_content=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultCachedVideo, self).__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.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultcachedvoice.py000066400000000000000000000072061362023133600271160ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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. 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. 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. 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. 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. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, id, voice_file_id, title, caption=None, reply_markup=None, input_message_content=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultCachedVoice, self).__init__('voice', id) self.voice_file_id = voice_file_id self.title = title # Optionals self.caption = caption self.parse_mode = parse_mode self.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultcontact.py000066400000000000000000000075111362023133600263130ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult 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. 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. 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. """ def __init__(self, id, phone_number, first_name, last_name=None, reply_markup=None, input_message_content=None, thumb_url=None, thumb_width=None, thumb_height=None, vcard=None, **kwargs): # Required super(InlineQueryResultContact, self).__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-12.4.2/telegram/inline/inlinequeryresultdocument.py000066400000000000000000000114411362023133600264730ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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, 0-1024 characters 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. 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. Args: id (:obj:`str`): Unique identifier for this result, 1-64 bytes. title (:obj:`str`): Title for the result. caption (:obj:`str`, optional): Caption, 0-1024 characters 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. 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. """ def __init__(self, id, document_url, title, mime_type, caption=None, description=None, reply_markup=None, input_message_content=None, thumb_url=None, thumb_width=None, thumb_height=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultDocument, self).__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.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-12.4.2/telegram/inline/inlinequeryresultgame.py000066400000000000000000000036141362023133600255710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult class InlineQueryResultGame(InlineQueryResult): """Represents a Game. 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. 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. """ def __init__(self, id, game_short_name, reply_markup=None, **kwargs): # Required super(InlineQueryResultGame, self).__init__('game', id) self.id = id self.game_short_name = game_short_name self.reply_markup = reply_markup python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultgif.py000066400000000000000000000107331362023133600254250ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultGif.""" from telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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 thumbnail for the result (jpeg or gif). title (:obj:`str`): Optional. Title for the result. caption (:obj:`str`): Optional. Caption, 0-1024 characters 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. 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. 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 thumbnail for the result (jpeg or gif). title (:obj:`str`, optional): Title for the result.caption (:obj:`str`, optional): caption (:obj:`str`, optional): Caption, 0-1024 characters 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. 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. """ def __init__(self, id, gif_url, thumb_url, gif_width=None, gif_height=None, title=None, caption=None, reply_markup=None, input_message_content=None, gif_duration=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultGif, self).__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.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultlocation.py000066400000000000000000000075351362023133600264760ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult 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. 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. live_period (:obj:`int`): Optional. Period in seconds for which the location can be updated, should be between 60 and 86400. 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. 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. live_period (:obj:`int`, optional): Period in seconds for which the location can be updated, should be between 60 and 86400. 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. """ def __init__(self, id, latitude, longitude, title, live_period=None, reply_markup=None, input_message_content=None, thumb_url=None, thumb_width=None, thumb_height=None, **kwargs): # Required super(InlineQueryResultLocation, self).__init__('location', id) self.latitude = latitude self.longitude = 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 python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultmpeg4gif.py000066400000000000000000000110341362023133600263550ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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 thumbnail (jpeg or gif) for the result. title (:obj:`str`): Optional. Title for the result. caption (:obj:`str`): Optional. Caption, 0-1024 characters 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. 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. 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. title (:obj:`str`, optional): Title for the result. caption (:obj:`str`, optional): Caption, 0-1024 characters 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. 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. """ def __init__(self, id, mpeg4_url, thumb_url, mpeg4_width=None, mpeg4_height=None, title=None, caption=None, reply_markup=None, input_message_content=None, mpeg4_duration=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultMpeg4Gif, self).__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.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultphoto.py000066400000000000000000000111341362023133600260050ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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, 0-1024 characters 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. 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. 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, 0-1024 characters 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. 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. """ def __init__(self, id, photo_url, thumb_url, photo_width=None, photo_height=None, title=None, description=None, caption=None, reply_markup=None, input_message_content=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultPhoto, self).__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.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inlinequeryresultvenue.py000066400000000000000000000106441362023133600260030ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult 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. 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. (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".) 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. 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".) 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. """ def __init__(self, id, latitude, longitude, title, address, foursquare_id=None, foursquare_type=None, reply_markup=None, input_message_content=None, thumb_url=None, thumb_width=None, thumb_height=None, **kwargs): # Required super(InlineQueryResultVenue, self).__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.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-12.4.2/telegram/inline/inlinequeryresultvideo.py000066400000000000000000000117011362023133600257620ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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, 0-1024 characters 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. 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. 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. 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. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, id, video_url, mime_type, thumb_url, title, caption=None, video_width=None, video_height=None, video_duration=None, description=None, reply_markup=None, input_message_content=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultVideo, self).__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.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-12.4.2/telegram/inline/inlinequeryresultvoice.py000066400000000000000000000075211362023133600257660ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InlineQueryResult from telegram.utils.helpers import DEFAULT_NONE 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. 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`): Voice message title. caption (:obj:`str`): Optional. Caption, 0-1024 characters. 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. 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. 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`): Voice message title. caption (:obj:`str`, optional): Caption, 0-1024 characters. 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. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, id, voice_url, title, voice_duration=None, caption=None, reply_markup=None, input_message_content=None, parse_mode=DEFAULT_NONE, **kwargs): # Required super(InlineQueryResultVoice, self).__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.reply_markup = reply_markup self.input_message_content = input_message_content python-telegram-bot-12.4.2/telegram/inline/inputcontactmessagecontent.py000066400000000000000000000040131362023133600266010ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InputMessageContent class InputContactMessageContent(InputMessageContent): """Represents the content of a contact message to be sent as the result of an inline query. 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. 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. """ def __init__(self, phone_number, first_name, last_name=None, vcard=None, **kwargs): # Required self.phone_number = phone_number self.first_name = first_name # Optionals self.last_name = last_name self.vcard = vcard python-telegram-bot-12.4.2/telegram/inline/inputlocationmessagecontent.py000066400000000000000000000034461362023133600267670ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InputMessageContent class InputLocationMessageContent(InputMessageContent): """ Represents the content of a location message to be sent as the result of an inline query. Attributes: latitude (:obj:`float`): Latitude of the location in degrees. longitude (:obj:`float`): Longitude of the location in degrees. Args: latitude (:obj:`float`): Latitude of the location in degrees. longitude (:obj:`float`): Longitude of the location in degrees. live_period (:obj:`int`, optional): Period in seconds for which the location can be updated, should be between 60 and 86400. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, latitude, longitude, live_period=None, **kwargs): # Required self.latitude = latitude self.longitude = longitude self.live_period = live_period python-telegram-bot-12.4.2/telegram/inline/inputmessagecontent.py000066400000000000000000000026731362023133600252370ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.InputLocationMessageContent`, :class:`telegram.InputTextMessageContent` and :class:`telegram.InputVenueMessageContent` for more details. """ @property def _has_parse_mode(self): return hasattr(self, 'parse_mode') @property def _has_disable_web_page_preview(self): return hasattr(self, 'disable_web_page_preview') python-telegram-bot-12.4.2/telegram/inline/inputtextmessagecontent.py000066400000000000000000000046301362023133600261370ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InputMessageContent from telegram.utils.helpers import DEFAULT_NONE class InputTextMessageContent(InputMessageContent): """ Represents the content of a text message to be sent as the result of an inline query. Attributes: message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters. 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. disable_web_page_preview (:obj:`bool`): Optional. Disables link previews for links in the sent message. Args: message_text (:obj:`str`): Text of the message to be sent, 1-4096 characters. 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. disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in the sent message. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, message_text, parse_mode=DEFAULT_NONE, disable_web_page_preview=DEFAULT_NONE, **kwargs): # Required self.message_text = message_text # Optionals self.parse_mode = parse_mode self.disable_web_page_preview = disable_web_page_preview python-telegram-bot-12.4.2/telegram/inline/inputvenuemessagecontent.py000066400000000000000000000051351362023133600262760ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import InputMessageContent class InputVenueMessageContent(InputMessageContent): """Represents the content of a venue message to be sent as the result of an inline query. Attributes: latitude (:obj:`float`): Latitude of the location in degrees. longitude (:obj:`float`): Longitude of the location in degrees. title (:obj:`str`): Name of the venue. address (:obj:`str`): Address of the venue. foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue, if known. foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known. (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".) 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".) **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, latitude, longitude, title, address, foursquare_id=None, foursquare_type=None, **kwargs): # Required self.latitude = latitude self.longitude = longitude self.title = title self.address = address # Optionals self.foursquare_id = foursquare_id self.foursquare_type = foursquare_type python-telegram-bot-12.4.2/telegram/keyboardbutton.py000066400000000000000000000045621362023133600227150ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject 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. Note: Optional fields are mutually exclusive. Attributes: text (:obj:`str`): Text of the button. request_contact (:obj:`bool`): Optional. If the user's phone number will be sent. request_location (:obj:`bool`): Optional. If the user's current location will be sent. 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 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 True, the user's current location will be sent when the button is pressed. Available in private chats only. Note: :attr:`request_contact` and :attr:`request_location` options will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. """ def __init__(self, text, request_contact=None, request_location=None, **kwargs): # Required self.text = text # Optionals self.request_contact = request_contact self.request_location = request_location python-telegram-bot-12.4.2/telegram/loginurl.py000066400000000000000000000064231362023133600215120ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 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 `_ 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 True to request the permission for your bot to send messages to the user. 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. 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. 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 True to request the permission for your bot to send messages to the user. """ def __init__(self, url, forward_text=None, bot_username=None, request_write_access=None): # 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-12.4.2/telegram/message.py000066400000000000000000001526351362023133600213120ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0902,R0912,R0913 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 sys from html import escape from telegram import (Animation, Audio, Contact, Document, Chat, Location, PhotoSize, Sticker, TelegramObject, User, Video, Voice, Venue, MessageEntity, Game, Invoice, SuccessfulPayment, VideoNote, PassportData, Poll, InlineKeyboardMarkup) from telegram import ParseMode from telegram.utils.helpers import escape_markdown, to_timestamp, from_timestamp _UNDEFINED = object() class Message(TelegramObject): """This object represents a message. Note: * In Python `from` is a reserved word, use `from_user` instead. Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. from_user (:class:`telegram.User`): Optional. Sender. 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. Information about the original channel. 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. reply_to_message (:class:`telegram.Message`): Optional. The original message. edit_date (:class:`datetime.datetime`): Optional. Date the message was last edited. 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. 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. passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data. poll (:class:`telegram.Poll`): Optional. Message is a native poll, information about the poll. reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. default_quote (:obj:`bool`): Optional. Default setting for the `quote` parameter of the :attr:`reply_text` and friends. Args: message_id (:obj:`int`): Unique message identifier inside this chat. from_user (:class:`telegram.User`, optional): Sender, can be empty for messages sent to channels. 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 a channel, information about the original channel. 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`. reply_to_message (:class:`telegram.Message`, optional): For replies, the original message. Note that the Message object in this field will not contain further ``reply_to_message`` fields even if it itself is a reply. edit_date (:class:`datetime.datetime`, optional): Date the message was last edited in Unix time. Converted to :class:`datetime.datetime`. 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 document, photo or video, 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. 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 change 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. 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): Signature of the post author for messages forwarded from channels. author_signature (:obj:`str`, optional): Signature of the post author for messages in channels. passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data. poll (:class:`telegram.Poll`, optional): Message is a native poll, information about the poll. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons. default_quote (:obj:`bool`, optional): Default setting for the `quote` parameter of the :attr:`reply_text` and friends. """ _effective_attachment = _UNDEFINED ATTACHMENT_TYPES = ['audio', 'game', 'animation', 'document', 'photo', 'sticker', 'video', 'voice', 'video_note', 'contact', 'location', 'venue', 'invoice', 'successful_payment'] MESSAGE_TYPES = ['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', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message', 'passport_data'] + ATTACHMENT_TYPES def __init__(self, message_id, from_user, date, chat, forward_from=None, forward_from_chat=None, forward_from_message_id=None, forward_date=None, reply_to_message=None, edit_date=None, text=None, entities=None, caption_entities=None, audio=None, document=None, game=None, photo=None, sticker=None, video=None, voice=None, video_note=None, new_chat_members=None, caption=None, contact=None, location=None, venue=None, left_chat_member=None, new_chat_title=None, new_chat_photo=None, delete_chat_photo=False, group_chat_created=False, supergroup_chat_created=False, channel_chat_created=False, migrate_to_chat_id=None, migrate_from_chat_id=None, pinned_message=None, invoice=None, successful_payment=None, forward_signature=None, author_signature=None, media_group_id=None, connected_website=None, animation=None, passport_data=None, poll=None, forward_sender_name=None, reply_markup=None, bot=None, default_quote=None, **kwargs): # Required self.message_id = int(message_id) self.from_user = from_user self.date = date self.chat = chat # Optionals self.forward_from = forward_from self.forward_from_chat = forward_from_chat self.forward_date = forward_date self.reply_to_message = reply_to_message self.edit_date = edit_date self.text = text self.entities = entities or list() self.caption_entities = caption_entities or list() self.audio = audio self.game = game self.document = document self.photo = photo or list() 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 list() self.left_chat_member = left_chat_member self.new_chat_title = new_chat_title self.new_chat_photo = new_chat_photo or list() 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.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.reply_markup = reply_markup self.bot = bot self.default_quote = default_quote self._id_attrs = (self.message_id,) @property def chat_id(self): """:obj:`int`: Shortcut for :attr:`telegram.Chat.id` for :attr:`chat`.""" return self.chat.id @property def link(self): """: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 = "c/{}".format(str(self.chat.id)[4:]) return "https://t.me/{}/{}".format(to_link, self.message_id) return None @classmethod def de_json(cls, data, bot): if not data: return None data = super(Message, cls).de_json(data, bot) data['from_user'] = User.de_json(data.get('from'), bot) data['date'] = from_timestamp(data['date']) chat = data.get('chat') if chat: chat['default_quote'] = data.get('default_quote') data['chat'] = Chat.de_json(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) forward_from_chat = data.get('forward_from_chat') if forward_from_chat: forward_from_chat['default_quote'] = data.get('default_quote') data['forward_from_chat'] = Chat.de_json(forward_from_chat, bot) data['forward_date'] = from_timestamp(data.get('forward_date')) reply_to_message = data.get('reply_to_message') if reply_to_message: reply_to_message['default_quote'] = data.get('default_quote') data['reply_to_message'] = Message.de_json(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) pinned_message = data.get('pinned_message') if pinned_message: pinned_message['default_quote'] = data.get('default_quote') data['pinned_message'] = Message.de_json(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['reply_markup'] = InlineKeyboardMarkup.de_json(data.get('reply_markup'), bot) return cls(bot=bot, **data) @property def effective_attachment(self): """ :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 ``None`` if no attachment was sent. """ if self._effective_attachment is not _UNDEFINED: return self._effective_attachment 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 def __getitem__(self, item): if item in self.__dict__.keys(): return self.__dict__[item] elif item == 'chat_id': return self.chat.id def to_dict(self): data = super(Message, self).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, kwargs): """Modify kwargs for replying with or without quoting.""" if 'reply_to_message_id' in kwargs: if 'quote' in kwargs: del kwargs['quote'] elif 'quote' in kwargs: if kwargs['quote']: kwargs['reply_to_message_id'] = self.message_id del kwargs['quote'] else: if ((self.default_quote is None and self.chat.type != Chat.PRIVATE) or self.default_quote): kwargs['reply_to_message_id'] = self.message_id def reply_text(self, *args, **kwargs): """Shortcut for:: bot.send_message(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_message(self.chat_id, *args, **kwargs) def reply_markdown(self, *args, **kwargs): """Shortcut for:: bot.send_message(update.message.chat_id, parse_mode=ParseMode.MARKDOWN, *args, **kwargs) Sends a message with markdown formatting. Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ kwargs['parse_mode'] = ParseMode.MARKDOWN self._quote(kwargs) return self.bot.send_message(self.chat_id, *args, **kwargs) def reply_html(self, *args, **kwargs): """Shortcut for:: bot.send_message(update.message.chat_id, parse_mode=ParseMode.HTML, *args, **kwargs) Sends a message with HTML formatting. Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ kwargs['parse_mode'] = ParseMode.HTML self._quote(kwargs) return self.bot.send_message(self.chat_id, *args, **kwargs) def reply_media_group(self, *args, **kwargs): """Shortcut for:: bot.reply_media_group(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: List[:class:`telegram.Message`]: An array of the sent Messages. Raises: :class:`telegram.TelegramError` """ self._quote(kwargs) return self.bot.send_media_group(self.chat_id, *args, **kwargs) def reply_photo(self, *args, **kwargs): """Shortcut for:: bot.send_photo(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_photo(self.chat_id, *args, **kwargs) def reply_audio(self, *args, **kwargs): """Shortcut for:: bot.send_audio(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_audio(self.chat_id, *args, **kwargs) def reply_document(self, *args, **kwargs): """Shortcut for:: bot.send_document(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_document(self.chat_id, *args, **kwargs) def reply_animation(self, *args, **kwargs): """Shortcut for:: bot.send_animation(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_animation(self.chat_id, *args, **kwargs) def reply_sticker(self, *args, **kwargs): """Shortcut for:: bot.send_sticker(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_sticker(self.chat_id, *args, **kwargs) def reply_video(self, *args, **kwargs): """Shortcut for:: bot.send_video(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_video(self.chat_id, *args, **kwargs) def reply_video_note(self, *args, **kwargs): """Shortcut for:: bot.send_video_note(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_video_note(self.chat_id, *args, **kwargs) def reply_voice(self, *args, **kwargs): """Shortcut for:: bot.send_voice(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_voice(self.chat_id, *args, **kwargs) def reply_location(self, *args, **kwargs): """Shortcut for:: bot.send_location(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_location(self.chat_id, *args, **kwargs) def reply_venue(self, *args, **kwargs): """Shortcut for:: bot.send_venue(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_venue(self.chat_id, *args, **kwargs) def reply_contact(self, *args, **kwargs): """Shortcut for:: bot.send_contact(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_contact(self.chat_id, *args, **kwargs) def reply_poll(self, *args, **kwargs): """Shortcut for:: bot.send_poll(update.message.chat_id, *args, **kwargs) Keyword Args: quote (:obj:`bool`, optional): If set to ``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: ``True`` in group chats and ``False`` in private chats. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ self._quote(kwargs) return self.bot.send_poll(self.chat_id, *args, **kwargs) def forward(self, chat_id, *args, **kwargs): """Shortcut for:: bot.forward_message(chat_id=chat_id, from_chat_id=update.message.chat_id, message_id=update.message.message_id, *args, **kwargs) 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, *args, **kwargs) def edit_text(self, *args, **kwargs): """Shortcut for:: bot.edit_message_text(chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs) Note: You can only edit messages that the bot sent itself, therefore this method can only be used on the return value of the ``bot.send_*`` family of methods. Returns: :class:`telegram.Message`: On success, instance representing the edited message. """ return self.bot.edit_message_text( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) def edit_caption(self, *args, **kwargs): """Shortcut for:: bot.edit_message_caption(chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs) Note: You can only edit messages that the bot sent itself, therefore this method can only be used on the return value of the ``bot.send_*`` family of methods. Returns: :class:`telegram.Message`: On success, instance representing the edited message. """ return self.bot.edit_message_caption( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) def edit_media(self, media, *args, **kwargs): """Shortcut for:: bot.edit_message_media(chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs) Note: You can only edit messages that the bot sent itself, therefore this method can only be used on the return value of the ``bot.send_*`` family of methods. Returns: :class:`telegram.Message`: On success, instance representing the edited message. """ return self.bot.edit_message_media( chat_id=self.chat_id, message_id=self.message_id, media=media, *args, **kwargs) def edit_reply_markup(self, *args, **kwargs): """Shortcut for:: bot.edit_message_reply_markup(chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs) Note: You can only edit messages that the bot sent itself, therefore this method can only be used on the return value of the ``bot.send_*`` family of methods. Returns: :class:`telegram.Message`: On success, instance representing the edited message. """ return self.bot.edit_message_reply_markup( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) def delete(self, *args, **kwargs): """Shortcut for:: bot.delete_message(chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs) Returns: :obj:`bool`: On success, ``True`` is returned. """ return self.bot.delete_message( chat_id=self.chat_id, message_id=self.message_id, *args, **kwargs) def parse_entity(self, entity): """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 """ # 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] else: 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): """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 """ # 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] else: 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=None): """ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. It contains entities from this message filtered by their :attr:`telegram.MessageEntity.type` attribute as the key, and the text that each entity belongs to as the value of the :obj:`dict`. Note: This method should always be used instead of the :attr:`entities` attribute, since it calculates the correct substring from the message text based on UTF-16 codepoints. See :attr:`parse_entity` for more info. Args: types (List[:obj:`str`], optional): List of :class:`telegram.MessageEntity` types as strings. If the ``type`` attribute of an entity is contained in this list, it will be returned. Defaults to a list of all types. All types can be found as constants in :class:`telegram.MessageEntity`. Returns: Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to the text that belongs to them, calculated based on UTF-16 codepoints. """ if types is None: types = MessageEntity.ALL_TYPES return { entity: self.parse_entity(entity) for entity in self.entities if entity.type in types } def parse_caption_entities(self, types=None): """ Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`. It contains entities from this message's caption filtered by their :attr:`telegram.MessageEntity.type` attribute as the key, and the text that each entity belongs to as the value of the :obj:`dict`. Note: This method should always be used instead of the :attr:`caption_entities` attribute, since it calculates the correct substring from the message text based on UTF-16 codepoints. See :attr:`parse_entity` for more info. Args: types (List[:obj:`str`], optional): List of :class:`telegram.MessageEntity` types as strings. If the ``type`` attribute of an entity is contained in this list, it will be returned. Defaults to a list of all types. All types can be found as constants in :class:`telegram.MessageEntity`. Returns: Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to the text that belongs to them, calculated based on UTF-16 codepoints. """ if types is None: types = MessageEntity.ALL_TYPES return { entity: self.parse_caption_entity(entity) for entity in self.caption_entities if entity.type in types } @staticmethod def _parse_html(message_text, entities, urled=False): if message_text is None: return None if not sys.maxunicode == 0xffff: message_text = message_text.encode('utf-16-le') html_text = '' last_offset = 0 for entity, text in sorted(entities.items(), key=(lambda item: item[0].offset)): text = escape(text) if entity.type == MessageEntity.TEXT_LINK: insert = '{}'.format(entity.url, text) elif entity.type == MessageEntity.TEXT_MENTION and entity.user: insert = '{}'.format(entity.user.id, text) elif entity.type == MessageEntity.URL and urled: insert = '{0}'.format(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: insert = '
' + text + '
' else: insert = text if sys.maxunicode == 0xffff: html_text += escape(message_text[last_offset:entity.offset]) + insert else: html_text += escape(message_text[last_offset * 2:entity.offset * 2] .decode('utf-16-le')) + insert last_offset = entity.offset + entity.length if sys.maxunicode == 0xffff: html_text += escape(message_text[last_offset:]) else: html_text += escape(message_text[last_offset * 2:].decode('utf-16-le')) return html_text @property def text_html(self): """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. 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): """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. 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): """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. Returns: :obj:`str`: Message caption with captionentities formatted as HTML. """ return self._parse_html(self.caption, self.parse_caption_entities(), urled=False) @property def caption_html_urled(self): """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. 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, entities, urled=False): if message_text is None: return None if not sys.maxunicode == 0xffff: message_text = message_text.encode('utf-16-le') markdown_text = '' last_offset = 0 for entity, text in sorted(entities.items(), key=(lambda item: item[0].offset)): text = escape_markdown(text) if entity.type == MessageEntity.TEXT_LINK: insert = '[{}]({})'.format(text, entity.url) elif entity.type == MessageEntity.TEXT_MENTION and entity.user: insert = '[{}](tg://user?id={})'.format(text, entity.user.id) elif entity.type == MessageEntity.URL and urled: insert = '[{0}]({0})'.format(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: insert = '```' + text + '```' else: insert = text if sys.maxunicode == 0xffff: markdown_text += escape_markdown(message_text[last_offset:entity.offset]) + insert else: markdown_text += escape_markdown(message_text[last_offset * 2:entity.offset * 2] .decode('utf-16-le')) + insert last_offset = entity.offset + entity.length if sys.maxunicode == 0xffff: markdown_text += escape_markdown(message_text[last_offset:]) else: markdown_text += escape_markdown(message_text[last_offset * 2:].decode('utf-16-le')) return markdown_text @property def text_markdown(self): """Creates an Markdown-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 Markdown in the same way the original message was formatted. Returns: :obj:`str`: Message text with entities formatted as Markdown. """ return self._parse_markdown(self.text, self.parse_entities(), urled=False) @property def text_markdown_urled(self): """Creates an Markdown-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 Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. Returns: :obj:`str`: Message text with entities formatted as Markdown. """ return self._parse_markdown(self.text, self.parse_entities(), urled=True) @property def caption_markdown(self): """Creates an Markdown-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 Markdown in the same way the original message was formatted. Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. """ return self._parse_markdown(self.caption, self.parse_caption_entities(), urled=False) @property def caption_markdown_urled(self): """Creates an Markdown-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 Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. """ return self._parse_markdown(self.caption, self.parse_caption_entities(), urled=True) python-telegram-bot-12.4.2/telegram/messageentity.py000066400000000000000000000074731362023133600225460ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import User, TelegramObject class MessageEntity(TelegramObject): """ This object represents one special entity in a text message. For example, hashtags, usernames, URLs, etc. 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. Args: type (:obj:`str`): Type of the entity. Can be mention (@username), hashtag, bot_command, url, email, bold (bold text), italic (italic text), 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 "text_link" only, url that will be opened after usertaps on the text. user (:class:`telegram.User`, optional): For "text_mention" only, the mentioned user. """ def __init__(self, type, offset, length, url=None, user=None, **kwargs): # Required self.type = type self.offset = offset self.length = length # Optionals self.url = url self.user = user self._id_attrs = (self.type, self.offset, self.length) @classmethod def de_json(cls, data, bot): data = super(MessageEntity, cls).de_json(data, bot) if not data: return None data['user'] = User.de_json(data.get('user'), bot) return cls(**data) @classmethod def de_list(cls, data, bot): if not data: return list() entities = list() for entity in data: entities.append(cls.de_json(entity, bot)) return entities MENTION = 'mention' """:obj:`str`: 'mention'""" HASHTAG = 'hashtag' """:obj:`str`: 'hashtag'""" CASHTAG = 'cashtag' """:obj:`str`: 'cashtag'""" PHONE_NUMBER = 'phone_number' """:obj:`str`: 'phone_number'""" BOT_COMMAND = 'bot_command' """:obj:`str`: 'bot_command'""" URL = 'url' """:obj:`str`: 'url'""" EMAIL = 'email' """:obj:`str`: 'email'""" BOLD = 'bold' """:obj:`str`: 'bold'""" ITALIC = 'italic' """:obj:`str`: 'italic'""" CODE = 'code' """:obj:`str`: 'code'""" PRE = 'pre' """:obj:`str`: 'pre'""" TEXT_LINK = 'text_link' """:obj:`str`: 'text_link'""" TEXT_MENTION = 'text_mention' """:obj:`str`: 'text_mention'""" ALL_TYPES = [ MENTION, HASHTAG, CASHTAG, PHONE_NUMBER, BOT_COMMAND, URL, EMAIL, BOLD, ITALIC, CODE, PRE, TEXT_LINK, TEXT_MENTION ] """List[:obj:`str`]: List of all the types.""" python-telegram-bot-12.4.2/telegram/parsemode.py000066400000000000000000000021351362023133600216320ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" class ParseMode(object): """This object represents a Telegram Message Parse Modes.""" MARKDOWN = 'Markdown' """:obj:`str`: 'Markdown'""" HTML = 'HTML' """:obj:`str`: 'HTML'""" python-telegram-bot-12.4.2/telegram/passport/000077500000000000000000000000001362023133600211535ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/passport/__init__.py000066400000000000000000000000001362023133600232520ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/passport/credentials.py000066400000000000000000000415561362023133600240350ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. try: import ujson as json except ImportError: import json from base64 import b64decode from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1 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 SHA512, SHA256, Hash, SHA1 from future.utils import bord from telegram import TelegramObject, TelegramError class TelegramDecryptionError(TelegramError): """ Something went wrong with decryption. """ def __init__(self, message): super(TelegramDecryptionError, self).__init__("TelegramDecryptionError: " "{}".format(message)) 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. """ # 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, iv = secret_hash_hash[:32], secret_hash_hash[32:32 + 16] # Init a AES-CBC cipher and decrypt the data cipher = Cipher(AES(key), CBC(iv), 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("Hashes are not equal! {} != {}".format(data_hash, hash)) # Return data without padding return data[bord(data[0]):] 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. 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. 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. Note: This object is decrypted only when originating from :obj:`telegram.PassportData.decrypted_credentials`. """ def __init__(self, data, hash, secret, bot=None, **kwargs): # 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 = None @classmethod def de_json(cls, data, bot): if not data: return None data = super(EncryptedCredentials, cls).de_json(data, bot) return cls(bot=bot, **data) @property def decrypted_secret(self): """ :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: # 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 )) except ValueError as e: # If decryption fails raise exception raise TelegramDecryptionError(e) return self._decrypted_secret @property def decrypted_data(self): """ :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 """ def __init__(self, secure_data, nonce, bot=None, **kwargs): # Required self.secure_data = secure_data self.nonce = nonce self.bot = bot @classmethod def de_json(cls, data, bot): 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. """ def __init__(self, personal_details=None, passport=None, internal_passport=None, driver_license=None, identity_card=None, address=None, utility_bill=None, bank_statement=None, rental_agreement=None, passport_registration=None, temporary_registration=None, bot=None, **kwargs): # 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, bot): 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. """ def __init__(self, data=None, front_side=None, reverse_side=None, selfie=None, files=None, translation=None, bot=None, **kwargs): 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, bot): 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): data = super(SecureValue, self).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.""" def __init__(self, hash, secret, bot=None, **kwargs): self.hash = hash self.secret = secret # Aliases just be be sure self.file_hash = self.hash self.data_hash = self.hash self.bot = bot @classmethod def de_json(cls, data, bot): if not data: return None return cls(bot=bot, **data) @classmethod def de_list(cls, data, bot): if not data: return [] credentials = list() for c in data: credentials.append(cls.de_json(c, bot=bot)) return credentials 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 """ def __init__(self, data_hash, secret, **kwargs): super(DataCredentials, self).__init__(data_hash, secret, **kwargs) def to_dict(self): data = super(DataCredentials, self).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 """ def __init__(self, file_hash, secret, **kwargs): super(FileCredentials, self).__init__(file_hash, secret, **kwargs) def to_dict(self): data = super(FileCredentials, self).to_dict() del data['data_hash'] del data['hash'] return data python-telegram-bot-12.4.2/telegram/passport/data.py000066400000000000000000000100761362023133600224420ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 TelegramObject 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 (:obj:`str`): First Name in the language of the user's country of residence. middle_name (:obj:`str`): Optional. Middle Name in the language of the user's country of residence. last_name (:obj:`str`): Last Name in the language of the user's country of residence. """ def __init__(self, first_name, last_name, birth_date, gender, country_code, residence_country_code, first_name_native=None, last_name_native=None, middle_name=None, middle_name_native=None, bot=None, **kwargs): # 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 @classmethod def de_json(cls, data, bot): if not data: return None return cls(bot=bot, **data) 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. """ def __init__(self, street_line1, street_line2, city, state, country_code, post_code, bot=None, **kwargs): # 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 @classmethod def de_json(cls, data, bot): if not data: return None return cls(bot=bot, **data) 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. """ def __init__(self, document_no, expiry_date, bot=None, **kwargs): self.document_no = document_no self.expiry_date = expiry_date self.bot = bot @classmethod def de_json(cls, data, bot): if not data: return None return cls(bot=bot, **data) python-telegram-bot-12.4.2/telegram/passport/encryptedpassportelement.py000066400000000000000000000253371362023133600267020ustar00rootroot00000000000000#!/usr/bin/env python # flake8: noqa: E501 # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import (TelegramObject, PassportFile, PersonalDetails, IdDocumentData, ResidentialAddress) from telegram.passport.credentials import decrypt_json 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. 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` or :class:`telegram.IdDocument` or :class:`telegram.ResidentialAddress` or :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. 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` or :class:`telegram.IdDocument` or :class:`telegram.ResidentialAddress` or :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. Note: This object is decrypted only when originating from :obj:`telegram.PassportData.decrypted_data`. """ def __init__(self, type, data=None, phone_number=None, email=None, files=None, front_side=None, reverse_side=None, selfie=None, translation=None, hash=None, bot=None, credentials=None, **kwargs): # 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, bot): if not data: return None data = super(EncryptedPassportElement, cls).de_json(data, bot) 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, bot, credentials): if not data: return None data = super(EncryptedPassportElement, cls).de_json(data, bot) 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) @classmethod def de_list(cls, data, bot): if not data: return [] encrypted_passport_elements = list() for element in data: encrypted_passport_elements.append(cls.de_json(element, bot)) return encrypted_passport_elements def to_dict(self): data = super(EncryptedPassportElement, self).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-12.4.2/telegram/passport/passportdata.py000066400000000000000000000104511362023133600242330ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 telegram import EncryptedCredentials, EncryptedPassportElement, TelegramObject class PassportData(TelegramObject): """Contains information about Telegram Passport data shared with the bot by the user. 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. Args: data (List[:class:`telegram.EncryptedPassportElement`]): Array with encrypted information about documents and other Telegram Passport elements that was shared with the bot. credentials (:obj:`str`): Encrypted credentials. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. 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`. """ def __init__(self, data, credentials, bot=None, **kwargs): self.data = data self.credentials = credentials self.bot = bot self._decrypted_data = None self._id_attrs = tuple([x.type for x in data] + [credentials.hash]) @classmethod def de_json(cls, data, bot): if not data: return None data = super(PassportData, cls).de_json(data, bot) 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): data = super(PassportData, self).to_dict() data['data'] = [e.to_dict() for e in self.data] return data @property def decrypted_data(self): """ 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): """ :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-12.4.2/telegram/passport/passportelementerrors.py000066400000000000000000000335271362023133600262210ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 PassportElementError.""" from telegram import TelegramObject class PassportElementError(TelegramObject): """Baseclass for the PassportElementError* classes. 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 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. """ def __init__(self, source, type, message, **kwargs): # 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. 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. 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. """ def __init__(self, type, field_name, data_hash, message, **kwargs): # Required super(PassportElementErrorDataField, self).__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. 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. 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. """ def __init__(self, type, file_hash, message, **kwargs): # Required super(PassportElementErrorFile, self).__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 file with the document scan changes. 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. 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. """ def __init__(self, type, file_hashes, message, **kwargs): # Required super(PassportElementErrorFiles, self).__init__('files', type, message) self.file_hashes = file_hashes self._id_attrs = ((self.source, self.type, self.message) + tuple([file_hash for file_hash in 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. 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. 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. """ def __init__(self, type, file_hash, message, **kwargs): # Required super(PassportElementErrorFrontSide, self).__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 front side of a document. The error is considered resolved when the file with the reverse side of the document changes. 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 reverse side of the document. message (:obj:`str`): Error message. 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. """ def __init__(self, type, file_hash, message, **kwargs): # Required super(PassportElementErrorReverseSide, self).__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. 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. 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. """ def __init__(self, type, file_hash, message, **kwargs): # Required super(PassportElementErrorSelfie, self).__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. 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. 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. """ def __init__(self, type, file_hash, message, **kwargs): # Required super(PassportElementErrorTranslationFile, self).__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 change. 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 file hash. message (:obj:`str`): Error message. 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. """ def __init__(self, type, file_hashes, message, **kwargs): # Required super(PassportElementErrorTranslationFiles, self).__init__('translation_files', type, message) self.file_hashes = file_hashes self._id_attrs = ((self.source, self.type, self.message) + tuple([file_hash for file_hash in file_hashes])) class PassportElementErrorUnspecified(PassportElementError): """ Represents an issue in an unspecified place. The error is considered resolved when new data is added. 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. 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. """ def __init__(self, type, element_hash, message, **kwargs): # Required super(PassportElementErrorUnspecified, self).__init__('unspecified', type, message) self.element_hash = element_hash self._id_attrs = (self.source, self.type, self.element_hash, self.message) python-telegram-bot-12.4.2/telegram/passport/passportfile.py000066400000000000000000000073711362023133600242500ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject 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. Attributes: file_id (:obj:`str`): Unique identifier for this 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. Args: file_id (:obj:`str`): Unique identifier for this 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. """ def __init__(self, file_id, file_date, file_size=None, bot=None, credentials=None, **kwargs): # Required self.file_id = file_id self.file_size = file_size self.file_date = file_date # Optionals self.bot = bot self._credentials = credentials self._id_attrs = (self.file_id,) @classmethod def de_json(cls, data, bot): if not data: return None data = super(PassportFile, cls).de_json(data, bot) return cls(bot=bot, **data) @classmethod def de_json_decrypted(cls, data, bot, credentials): if not data: return None data = super(PassportFile, cls).de_json(data, bot) data['credentials'] = credentials return cls(bot=bot, **data) @classmethod def de_list(cls, data, bot): if not data: return [] return [cls.de_json(passport_file, bot) for passport_file in data] @classmethod def de_list_decrypted(cls, data, bot, credentials): 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=None, **kwargs): """ 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`. 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). **kwargs (:obj:`dict`): Arbitrary keyword arguments. Returns: :class:`telegram.File` Raises: :class:`telegram.TelegramError` """ file = self.bot.get_file(self.file_id, timeout=timeout, **kwargs) file.set_credentials(self._credentials) return file python-telegram-bot-12.4.2/telegram/payment/000077500000000000000000000000001362023133600207555ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/payment/__init__.py000066400000000000000000000000001362023133600230540ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/payment/invoice.py000066400000000000000000000044171362023133600227710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class Invoice(TelegramObject): """This object contains basic information about an invoice. 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. 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, title, description, start_parameter, currency, total_amount, **kwargs): self.title = title self.description = description self.start_parameter = start_parameter self.currency = currency self.total_amount = total_amount @classmethod def de_json(cls, data, bot): if not data: return None return cls(**data) python-telegram-bot-12.4.2/telegram/payment/labeledprice.py000066400000000000000000000033561362023133600237510ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class LabeledPrice(TelegramObject): """This object represents a portion of the price for goods or services. Attributes: label (:obj:`str`): Portion label. amount (:obj:`int`): Price of the product in the smallest units of the currency. Args: label (:obj:`str`): Portion label amount (:obj:`int`): Price of the product in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, label, amount, **kwargs): self.label = label self.amount = amount python-telegram-bot-12.4.2/telegram/payment/orderinfo.py000066400000000000000000000041661362023133600233250ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject, ShippingAddress class OrderInfo(TelegramObject): """This object represents information about an order. 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. 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. """ def __init__(self, name=None, phone_number=None, email=None, shipping_address=None, **kwargs): self.name = name self.phone_number = phone_number self.email = email self.shipping_address = shipping_address @classmethod def de_json(cls, data, bot): if not data: return cls() data = super(OrderInfo, cls).de_json(data, bot) data['shipping_address'] = ShippingAddress.de_json(data.get('shipping_address'), bot) return cls(**data) python-telegram-bot-12.4.2/telegram/payment/precheckoutquery.py000066400000000000000000000111771362023133600247400ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject, User, OrderInfo class PreCheckoutQuery(TelegramObject): """This object contains information about an incoming pre-checkout query. Note: * In Python `from` is a reserved word, use `from_user` instead. 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. Args: id (:obj:`str`): Unique query identifier. from_user (:class:`telegram.User`): User who sent the query. currency (:obj:`str`): Three-letter ISO 4217 currency code total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). invoice_payload (:obj:`str`): Bot specified invoice payload. shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, id, from_user, currency, total_amount, invoice_payload, shipping_option_id=None, order_info=None, bot=None, **kwargs): self.id = id 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, bot): if not data: return None data = super(PreCheckoutQuery, cls).de_json(data, bot) 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(self, *args, **kwargs): """Shortcut for:: bot.answer_pre_checkout_query(update.pre_checkout_query.id, *args, **kwargs) Args: ok (:obj:`bool`): Specify True if everything is alright (goods are available, etc.) and the bot is ready to proceed with the order. Use False if there are any problems. error_message (:obj:`str`, optional): Required if ok is 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. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ return self.bot.answer_pre_checkout_query(self.id, *args, **kwargs) python-telegram-bot-12.4.2/telegram/payment/shippingaddress.py000066400000000000000000000044761362023133600245310ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class ShippingAddress(TelegramObject): """This object represents a Telegram ShippingAddress. 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. 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. """ def __init__(self, country_code, state, city, street_line1, street_line2, post_code, **kwargs): 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) @classmethod def de_json(cls, data, bot): if not data: return None return cls(**data) python-telegram-bot-12.4.2/telegram/payment/shippingoption.py000066400000000000000000000033761362023133600244120ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class ShippingOption(TelegramObject): """This object represents one shipping option. Attributes: id (:obj:`str`): Shipping option identifier. title (:obj:`str`): Option title. prices (List[:class:`telegram.LabeledPrice`]): List of price portions. 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. """ def __init__(self, id, title, prices, **kwargs): self.id = id self.title = title self.prices = prices self._id_attrs = (self.id,) def to_dict(self): data = super(ShippingOption, self).to_dict() data['prices'] = [p.to_dict() for p in self.prices] return data python-telegram-bot-12.4.2/telegram/payment/shippingquery.py000066400000000000000000000070441362023133600242430ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject, User, ShippingAddress class ShippingQuery(TelegramObject): """This object contains information about an incoming shipping query. Note: * In Python `from` is a reserved word, use `from_user` instead. 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. 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. """ def __init__(self, id, from_user, invoice_payload, shipping_address, bot=None, **kwargs): self.id = id 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, bot): if not data: return None data = super(ShippingQuery, cls).de_json(data, bot) 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(self, *args, **kwargs): """Shortcut for:: bot.answer_shipping_query(update.shipping_query.id, *args, **kwargs) Args: ok (:obj:`bool`): Specify True if delivery to the specified address is possible and 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 True. A JSON-serialized array of available shipping options. error_message (:obj:`str`, optional): Required if ok is 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. """ return self.bot.answer_shipping_query(self.id, *args, **kwargs) python-telegram-bot-12.4.2/telegram/payment/successfulpayment.py000066400000000000000000000070421362023133600251070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject, OrderInfo class SuccessfulPayment(TelegramObject): """This object contains basic information about a successful payment. 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. Args: currency (:obj:`str`): Three-letter ISO 4217 currency code. total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). invoice_payload (:obj:`str`): Bot specified invoice payload. shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the user. order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user telegram_payment_charge_id (:obj:`str`): Telegram payment identifier. provider_payment_charge_id (:obj:`str`): Provider payment identifier. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, currency, total_amount, invoice_payload, telegram_payment_charge_id, provider_payment_charge_id, shipping_option_id=None, order_info=None, **kwargs): 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, bot): if not data: return None data = super(SuccessfulPayment, cls).de_json(data, bot) data['order_info'] = OrderInfo.de_json(data.get('order_info'), bot) return cls(**data) python-telegram-bot-12.4.2/telegram/poll.py000066400000000000000000000054571362023133600206330ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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.""" from telegram import (TelegramObject) class PollOption(TelegramObject): """ This object contains information about one answer option in a poll. Attributes: text (:obj:`str`): Option text, 1-100 characters. voter_count (:obj:`int`): Number of users that voted for this option. Args: text (:obj:`str`): Option text, 1-100 characters. voter_count (:obj:`int`): Number of users that voted for this option. """ def __init__(self, text, voter_count, **kwargs): self.text = text self.voter_count = voter_count @classmethod def de_json(cls, data, bot): if not data: return None return cls(**data) class Poll(TelegramObject): """ This object contains information about a poll. Attributes: id (:obj:`str`): Unique poll identifier. question (:obj:`str`): Poll question, 1-255 characters. options (List[:class:`PollOption`]): List of poll options. is_closed (:obj:`bool`): True, if the poll is closed. Args: id (:obj:`str`): Unique poll identifier. question (:obj:`str`): Poll question, 1-255 characters. options (List[:class:`PollOption`]): List of poll options. is_closed (:obj:`bool`): True, if the poll is closed. """ def __init__(self, id, question, options, is_closed, **kwargs): self.id = id self.question = question self.options = options self.is_closed = is_closed self._id_attrs = (self.id,) @classmethod def de_json(cls, data, bot): if not data: return None data = super(Poll, cls).de_json(data, bot) data['options'] = [PollOption.de_json(option, bot) for option in data['options']] return cls(**data) def to_dict(self): data = super(Poll, self).to_dict() data['options'] = [x.to_dict() for x in self.options] return data python-telegram-bot-12.4.2/telegram/replykeyboardmarkup.py000066400000000000000000000234251362023133600237540ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import ReplyMarkup class ReplyKeyboardMarkup(ReplyMarkup): """This object represents a custom keyboard with reply options. 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. 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 false, in which case the custom keyboard is always of the same height as the app's standard keyboard. Defaults to ``False`` 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 ``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 ``False``. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, keyboard, resize_keyboard=False, one_time_keyboard=False, selective=False, **kwargs): # Required self.keyboard = keyboard # Optionals self.resize_keyboard = bool(resize_keyboard) self.one_time_keyboard = bool(one_time_keyboard) self.selective = bool(selective) def to_dict(self): data = super(ReplyKeyboardMarkup, self).to_dict() data['keyboard'] = [] for row in self.keyboard: r = [] for button in row: if hasattr(button, 'to_dict'): r.append(button.to_dict()) # telegram.KeyboardButton else: r.append(button) # str data['keyboard'].append(r) return data @classmethod def from_button(cls, button, resize_keyboard=False, one_time_keyboard=False, selective=False, **kwargs): """Shortcut for:: ReplyKeyboardMarkup([[button]], **kwargs) Return an 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 false, in which case the custom keyboard is always of the same height as the app's standard keyboard. Defaults to ``False`` 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 ``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 ``False``. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ return cls([[button]], resize_keyboard=resize_keyboard, one_time_keyboard=one_time_keyboard, selective=selective, **kwargs) @classmethod def from_row(cls, button_row, resize_keyboard=False, one_time_keyboard=False, selective=False, **kwargs): """Shortcut for:: ReplyKeyboardMarkup([button_row], **kwargs) Return an 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 false, in which case the custom keyboard is always of the same height as the app's standard keyboard. Defaults to ``False`` 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 ``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 ``False``. **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ return cls([button_row], resize_keyboard=resize_keyboard, one_time_keyboard=one_time_keyboard, selective=selective, **kwargs) @classmethod def from_column(cls, button_column, resize_keyboard=False, one_time_keyboard=False, selective=False, **kwargs): """Shortcut for:: ReplyKeyboardMarkup([[button] for button in button_column], **kwargs) Return an 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 false, in which case the custom keyboard is always of the same height as the app's standard keyboard. Defaults to ``False`` 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 ``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 ``False``. **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, **kwargs) python-telegram-bot-12.4.2/telegram/replykeyboardremove.py000066400000000000000000000046001362023133600237440ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 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`). 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. 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. 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 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. """ def __init__(self, selective=False, **kwargs): # Required self.remove_keyboard = True # Optionals self.selective = bool(selective) python-telegram-bot-12.4.2/telegram/replymarkup.py000066400000000000000000000021341362023133600222250ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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.ReplyKeyboardMarkup` and :class:`telegram.InlineKeyboardMarkup` for detailed use. """ pass python-telegram-bot-12.4.2/telegram/update.py000066400000000000000000000233261362023133600211420ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import (Message, TelegramObject, InlineQuery, ChosenInlineResult, CallbackQuery, ShippingQuery, PreCheckoutQuery, Poll) class Update(TelegramObject): """This object represents an incoming update. Note: At most one of the optional parameters can be present in any given update. 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 polls, which are sent or stopped by the bot Args: update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a certain positive number and increase sequentially. This ID becomes especially handy if you're using Webhooks, since it allows you to ignore repeated updates or to restore the correct update sequence, should they get out of order. 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 polls, which are sent or stopped by the bot **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ def __init__(self, update_id, message=None, edited_message=None, channel_post=None, edited_channel_post=None, inline_query=None, chosen_inline_result=None, callback_query=None, shipping_query=None, pre_checkout_query=None, poll=None, **kwargs): # 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._effective_user = None self._effective_chat = None self._effective_message = None self._id_attrs = (self.update_id,) @property def effective_user(self): """ :class:`telegram.User`: The user that sent this update, no matter what kind of update this is. Will be ``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 self._effective_user = user return user @property def effective_chat(self): """ :class:`telegram.Chat`: The chat that this update was sent in, no matter what kind of update this is. Will be ``None`` for :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, :attr:`shipping_query`, :attr:`pre_checkout_query` and :attr:`poll`. """ 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 self._effective_chat = chat return chat @property def effective_message(self): """ :class:`telegram.Message`: The message included in this update, no matter what kind of update this is. Will be ``None`` for :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, :attr:`shipping_query`, :attr:`pre_checkout_query` and :attr:`poll`. """ 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, bot): if not data: return None data = super(Update, cls).de_json(data, bot) message = data.get('message') if message: message['default_quote'] = data.get('default_quote') data['message'] = Message.de_json(message, bot) edited_message = data.get('edited_message') if edited_message: edited_message['default_quote'] = data.get('default_quote') data['edited_message'] = Message.de_json(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) callback_query = data.get('callback_query') if callback_query: callback_query['default_quote'] = data.get('default_quote') data['callback_query'] = CallbackQuery.de_json(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) channel_post = data.get('channel_post') if channel_post: channel_post['default_quote'] = data.get('default_quote') data['channel_post'] = Message.de_json(channel_post, bot) edited_channel_post = data.get('edited_channel_post') if edited_channel_post: edited_channel_post['default_quote'] = data.get('default_quote') data['edited_channel_post'] = Message.de_json(edited_channel_post, bot) data['poll'] = Poll.de_json(data.get('poll'), bot) return cls(**data) python-telegram-bot-12.4.2/telegram/user.py000066400000000000000000000202261362023133600206320ustar00rootroot00000000000000#!/usr/bin/env python # pylint: disable=C0103,W0622 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject from telegram.utils.helpers import mention_html as util_mention_html from telegram.utils.helpers import mention_markdown as util_mention_markdown class User(TelegramObject): """This object represents a Telegram user or bot. Attributes: id (:obj:`int`): Unique identifier for this user or bot. is_bot (:obj:`bool`): 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. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. Args: id (:obj:`int`): Unique identifier for this user or bot. is_bot (:obj:`bool`): 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. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. """ def __init__(self, id, first_name, is_bot, last_name=None, username=None, language_code=None, bot=None, **kwargs): # Required self.id = int(id) 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.bot = bot self._id_attrs = (self.id,) @property def name(self): """: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 '@{}'.format(self.username) return self.full_name @property def full_name(self): """:obj:`str`: Convenience property. The user's :attr:`first_name`, followed by (if available) :attr:`last_name`.""" if self.last_name: return u'{} {}'.format(self.first_name, self.last_name) return self.first_name @property def link(self): """:obj:`str`: Convenience property. If :attr:`username` is available, returns a t.me link of the user.""" if self.username: return "https://t.me/{}".format(self.username) return None @classmethod def de_json(cls, data, bot): if not data: return None data = super(User, cls).de_json(data, bot) return cls(bot=bot, **data) def get_profile_photos(self, *args, **kwargs): """ Shortcut for:: bot.get_user_profile_photos(update.message.from_user.id, *args, **kwargs) """ return self.bot.get_user_profile_photos(self.id, *args, **kwargs) @classmethod def de_list(cls, data, bot): if not data: return [] users = list() for user in data: users.append(cls.de_json(user, bot)) return users def mention_markdown(self, name=None): """ 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. """ if name: return util_mention_markdown(self.id, name) return util_mention_markdown(self.id, self.full_name) def mention_html(self, name=None): """ 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 send_message(self, *args, **kwargs): """Shortcut for:: bot.send_message(User.id, *args, **kwargs) Where User is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_message(self.id, *args, **kwargs) def send_photo(self, *args, **kwargs): """Shortcut for:: bot.send_photo(User.id, *args, **kwargs) Where User is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_photo(self.id, *args, **kwargs) def send_audio(self, *args, **kwargs): """Shortcut for:: bot.send_audio(User.id, *args, **kwargs) Where User is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_audio(self.id, *args, **kwargs) def send_document(self, *args, **kwargs): """Shortcut for:: bot.send_document(User.id, *args, **kwargs) Where User is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_document(self.id, *args, **kwargs) def send_animation(self, *args, **kwargs): """Shortcut for:: bot.send_animation(User.id, *args, **kwargs) Where User is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_animation(self.id, *args, **kwargs) def send_sticker(self, *args, **kwargs): """Shortcut for:: bot.send_sticker(User.id, *args, **kwargs) Where User is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_sticker(self.id, *args, **kwargs) def send_video(self, *args, **kwargs): """Shortcut for:: bot.send_video(User.id, *args, **kwargs) Where User is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_video(self.id, *args, **kwargs) def send_video_note(self, *args, **kwargs): """Shortcut for:: bot.send_video_note(User.id, *args, **kwargs) Where User is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_video_note(self.id, *args, **kwargs) def send_voice(self, *args, **kwargs): """Shortcut for:: bot.send_voice(User.id, *args, **kwargs) Where User is the current instance. Returns: :class:`telegram.Message`: On success, instance representing the message posted. """ return self.bot.send_voice(self.id, *args, **kwargs) python-telegram-bot-12.4.2/telegram/userprofilephotos.py000066400000000000000000000040531362023133600234500ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import PhotoSize, TelegramObject class UserProfilePhotos(TelegramObject): """This object represent a user's profile pictures. Attributes: total_count (:obj:`int`): Total number of profile pictures. photos (List[List[:class:`telegram.PhotoSize`]]): Requested profile pictures. 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). """ def __init__(self, total_count, photos, **kwargs): # Required self.total_count = int(total_count) self.photos = photos @classmethod def de_json(cls, data, bot): if not data: return None data = super(UserProfilePhotos, cls).de_json(data, bot) data['photos'] = [PhotoSize.de_list(photo, bot) for photo in data['photos']] return cls(**data) def to_dict(self): data = super(UserProfilePhotos, self).to_dict() data['photos'] = [] for photo in self.photos: data['photos'].append([x.to_dict() for x in photo]) return data python-telegram-bot-12.4.2/telegram/utils/000077500000000000000000000000001362023133600204405ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/utils/__init__.py000066400000000000000000000000001362023133600225370ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/utils/deprecate.py000066400000000000000000000031211362023133600227430ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should 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): pass def warn_deprecate_obj(old, new, stacklevel=3): warnings.warn( '{0} is being deprecated, please use {1} from now on.'.format(old, new), category=TelegramDeprecationWarning, stacklevel=stacklevel) def deprecate(func, old, new): """Warn users invoking old to switch to the new function.""" def f(*args, **kwargs): warn_deprecate_obj(old, new) return func(*args, **kwargs) return f python-telegram-bot-12.4.2/telegram/utils/helpers.py000066400000000000000000000332501362023133600224570ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 time from collections import defaultdict from numbers import Number try: import ujson as json except ImportError: import json from html import escape import re import signal # 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): """Returns the signal name of the given signal number.""" return _signames[signum] def escape_markdown(text): """Helper function to escape telegram markup symbols.""" escape_chars = '\*_`\[' return re.sub(r'([%s])' % escape_chars, r'\\\1', text) # -------- date/time related helpers -------- # TODO: add generic specification of UTC for naive datetimes to docs if hasattr(dtm, 'timezone'): # Python 3.3+ def _datetime_to_float_timestamp(dt_obj): if dt_obj.tzinfo is None: dt_obj = dt_obj.replace(tzinfo=_UTC) return dt_obj.timestamp() _UtcOffsetTimezone = dtm.timezone _UTC = dtm.timezone.utc else: # Python < 3.3 (incl 2.7) # hardcoded timezone class (`datetime.timezone` isn't available in py2) class _UtcOffsetTimezone(dtm.tzinfo): def __init__(self, offset): self.offset = offset def tzname(self, dt): return 'UTC +{}'.format(self.offset) def utcoffset(self, dt): return self.offset def dst(self, dt): return dtm.timedelta(0) _UTC = _UtcOffsetTimezone(dtm.timedelta(0)) __EPOCH_DT = dtm.datetime.fromtimestamp(0, tz=_UTC) __NAIVE_EPOCH_DT = __EPOCH_DT.replace(tzinfo=None) # _datetime_to_float_timestamp # Not using future.backports.datetime here as datetime value might be an input from the user, # making every isinstace() call more delicate. So we just use our own compat layer. def _datetime_to_float_timestamp(dt_obj): epoch_dt = __EPOCH_DT if dt_obj.tzinfo is not None else __NAIVE_EPOCH_DT return (dt_obj - epoch_dt).total_seconds() _datetime_to_float_timestamp.__doc__ = \ """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.""" def to_float_timestamp(t, reference_timestamp=None): """ 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. Any objects from the :class:`datetime` module that are timezone-naive will be assumed to be in UTC. ``None`` s are left alone (i.e. ``to_float_timestamp(None)`` is ``None``). Args: t (int | float | datetime.timedelta | datetime.datetime | 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 (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 ``datetime.datetime`` object), ``reference_timestamp`` is not relevant and so its value should be ``None``. If this is not the case, a ``ValueError`` will be raised. Returns: (float | 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 """ if reference_timestamp is None: reference_timestamp = time.time() elif isinstance(t, dtm.datetime): raise ValueError('t is an (absolute) datetime while reference_timestamp is not None') if isinstance(t, dtm.timedelta): return reference_timestamp + t.total_seconds() elif isinstance(t, Number): return reference_timestamp + t elif isinstance(t, dtm.time): if t.tzinfo is not None: reference_dt = dtm.datetime.fromtimestamp(reference_timestamp, tz=t.tzinfo) else: reference_dt = dtm.datetime.utcfromtimestamp(reference_timestamp) # assume UTC reference_date = reference_dt.date() reference_time = reference_dt.timetz() if reference_time > t: # if the time of day has passed today, use tomorrow reference_date += dtm.timedelta(days=1) return _datetime_to_float_timestamp(dtm.datetime.combine(reference_date, t)) elif isinstance(t, dtm.datetime): return _datetime_to_float_timestamp(t) raise TypeError('Unable to convert {} object to timestamp'.format(type(t).__name__)) def to_timestamp(dt_obj, reference_timestamp=None): """ 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)) if dt_obj is not None else None def from_timestamp(unixtime): """ Converts an (integer) unix timestamp to a naive datetime object in UTC. ``None`` s are left alone (i.e. ``from_timestamp(None)`` is ``None``). Args: unixtime (int): integer POSIX timestamp Returns: equivalent :obj:`datetime.datetime` value in naive UTC if ``timestamp`` is not ``None``; else ``None`` """ if unixtime is None: return None return dtm.datetime.utcfromtimestamp(unixtime) # -------- end -------- def mention_html(user_id, name): """ 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. """ if isinstance(user_id, int): return u'{}'.format(user_id, escape(name)) def mention_markdown(user_id, name): """ 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 markdown. """ if isinstance(user_id, int): return u'[{}](tg://user?id={})'.format(escape_markdown(name), user_id) def effective_message_type(entity): """ Extracts the type of message as a string identifier from a :class:`telegram.Message` or a :class:`telegram.Update`. Args: entity (:obj:`Update` | :obj:`Message`) The ``update`` or ``message`` to extract from Returns: str: One of ``Message.MESSAGE_TYPES`` """ # Importing on file-level yields cyclic Import Errors from telegram import Message from telegram import Update if isinstance(entity, Message): message = entity elif isinstance(entity, Update): message = entity.effective_message else: raise TypeError("entity is not Message or Update (got: {})".format(type(entity))) for i in Message.MESSAGE_TYPES: if getattr(message, i, None): return i return None def create_deep_linked_url(bot_username, payload=None, group=False): """ 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 `True` the user is prompted to select a group to add the bot to. If `False`, opens a one-on-one conversation with the bot. Defaults to `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 = 'https://t.me/{}'.format(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 '{0}?{1}={2}'.format( base_url, key, payload ) def encode_conversations_to_json(conversations): """Helper method to encode a conversations dict (that uses tuples as keys) to a JSON-serializable way. Use :attr:`_decode_conversations_from_json` to decode. Args: conversations (:obj:`dict`): The conversations dict to transofrm to JSON. Returns: :obj:`str`: The JSON-serialized conversations dict """ tmp = {} 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): """Helper method to decode a conversations dict (that uses tuples as keys) from a JSON-string created with :attr:`_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 = {} 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): """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(dict) decoded_data = json.loads(data) for user, data in decoded_data.items(): user = int(user) tmp[user] = {} for key, value in data.items(): try: key = int(key) except ValueError: pass tmp[user][key] = value return tmp class DefaultValue: """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('`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: ... Attributes: value (:obj:`obj`): The value of the default argument Args: value (:obj:`obj`): The value of the default argument """ def __init__(self, value=None): self.value = value def __bool__(self): return bool(self.value) DEFAULT_NONE = DefaultValue(None) """:class:`DefaultValue`: Default `None`""" python-telegram-bot-12.4.2/telegram/utils/promise.py000066400000000000000000000062701362023133600224750ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) class Promise(object): """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`. 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. """ def __init__(self, pooled_function, args, kwargs): self.pooled_function = pooled_function self.args = args self.kwargs = kwargs self.done = Event() self._result = None self._exception = None def run(self): """Calls the :attr:`pooled_function` callable.""" try: self._result = self.pooled_function(*self.args, **self.kwargs) except Exception as exc: logger.exception('An uncaught error was raised while running the promise') self._exception = exc finally: self.done.set() def __call__(self): self.run() def result(self, timeout=None): """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: Any 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 @property def exception(self): """The exception raised by :attr:`pooled_function` or ``None`` if no exception has been raised (yet).""" return self._exception python-telegram-bot-12.4.2/telegram/utils/request.py000066400000000000000000000336171362023133600225140ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 from builtins import str # For PY2 try: import ujson as json except ImportError: import json 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.util.timeout import Timeout from telegram.vendor.ptb_urllib3.urllib3.fields import RequestField except ImportError: # pragma: no cover try: import urllib3 import urllib3.contrib.appengine as appengine from urllib3.connection import HTTPConnection from urllib3.util.timeout import Timeout from urllib3.fields import RequestField 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 from telegram import (InputFile, TelegramError, InputMedia) from telegram.error import (Unauthorized, NetworkError, TimedOut, BadRequest, ChatMigrated, RetryAfter, InvalidToken, Conflict) def _render_part(self, name, value): """ 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(u'\\', u'\\\\').replace(u'"', u'\\"') value = value.replace(u'\r', u' ').replace(u'\n', u' ') return u'%s="%s"' % (name, value) RequestField._render_part = _render_part logging.getLogger('urllib3').setLevel(logging.WARNING) USER_AGENT = 'Python Telegram Bot (https://github.com/python-telegram-bot/python-telegram-bot)' class Request(object): """ Helper class for python-telegram-bot which provides methods to perform POST & GET towards telegram servers. Args: con_pool_size (int): Number of connections to keep in the connection pool. proxy_url (str): The URL to the proxy server. For example: `http://127.0.0.1:3128`. urllib3_proxy_kwargs (dict): Arbitrary arguments passed as-is to `urllib3.ProxyManager`. This value will be ignored if proxy_url is not set. connect_timeout (int|float): The maximum amount of time (in seconds) to wait for a connection attempt to a server to succeed. None will set an infinite timeout for connection attempts. (default: 5.) read_timeout (int|float): The maximum amount of time (in seconds) to wait between consecutive read operations for a response from the server. None will set an infinite timeout. This value is usually overridden by the various ``telegram.Bot`` methods. (default: 5.) """ def __init__(self, con_pool_size=1, proxy_url=None, urllib3_proxy_kwargs=None, connect_timeout=5., read_timeout=5.): if urllib3_proxy_kwargs is None: urllib3_proxy_kwargs = dict() 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') if not proxy_url: if appengine.is_appengine_sandbox(): # Use URLFetch service if running in App Engine mgr = appengine.AppEngineManager() else: mgr = urllib3.PoolManager(**kwargs) else: kwargs.update(urllib3_proxy_kwargs) if proxy_url.startswith('socks'): try: from telegram.vendor.ptb_urllib3.urllib3.contrib.socks import SOCKSProxyManager except ImportError: raise RuntimeError('PySocks is missing') mgr = 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 @property def con_pool_size(self): """The size of the connection pool used.""" return self._con_pool_size def stop(self): self._con_pool.clear() @staticmethod def _parse(json_data): """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: raise TelegramError('Invalid server response') 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, **kwargs): """Wraps urllib3 request for handling known exceptions. Args: args: unnamed arguments, passed to urllib3 request. kwargs: keyword arguments, passed tp urllib3 request. Returns: str: 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: raise TimedOut() 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('urllib3 HTTPError {0}'.format(error)) if 200 <= resp.status <= 299: # 200-299 range are HTTP success statuses return resp.data try: message = self._parse(resp.data) except ValueError: message = 'Unknown HTTPError' if resp.status in (401, 403): raise Unauthorized(message) elif resp.status == 400: raise BadRequest(message) elif resp.status == 404: raise InvalidToken() elif resp.status == 409: raise Conflict(message) elif resp.status == 413: raise NetworkError('File too large. Check telegram api limits ' 'https://core.telegram.org/bots/api#senddocument') elif resp.status == 502: raise NetworkError('Bad Gateway') else: raise NetworkError('{0} ({1})'.format(message, resp.status)) def get(self, url, timeout=None): """Request an 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). Returns: A JSON object. """ urlopen_kwargs = {} if timeout is not None: urlopen_kwargs['timeout'] = Timeout(read=timeout, connect=self._connect_timeout) result = self._request_wrapper('GET', url, **urlopen_kwargs) return self._parse(result) def post(self, url, data, timeout=None): """Request an URL. Args: url (:obj:`str`): The web location we want to retrieve. data (dict[str, str|int]): A dict of key/value pairs. Note: On py2.7 value is unicode. 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). Returns: A JSON object. """ urlopen_kwargs = {} if timeout is not None: urlopen_kwargs['timeout'] = Timeout(read=timeout, connect=self._connect_timeout) # Are we uploading files? files = False 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': # One media or multiple if isinstance(val, InputMedia): # Attach and set val to attached name data[key] = val.to_json() if isinstance(val.media, InputFile): data[val.media.attach] = val.media.field_tuple else: # Attach and set val to attached name for all media = [] for m in val: media.append(m.to_dict()) if isinstance(m.media, InputFile): data[m.media.attach] = m.media.field_tuple data[key] = json.dumps(media) files = True # 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, timeout=None): """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, filename, timeout=None): """Download a file by its URL. Args: url (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). filename: 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-12.4.2/telegram/utils/webhookhandler.py000066400000000000000000000117071362023133600240140ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 telegram import Update from future.utils import bytes_to_native_str from threading import Lock try: import ujson as json except ImportError: import json from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop import tornado.web import tornado.iostream logging.getLogger(__name__).addHandler(logging.NullHandler()) class WebhookServer(object): def __init__(self, listen, port, webhook_app, ssl_ctx): self.http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx) self.listen = listen self.port = port self.loop = None self.logger = logging.getLogger(__name__) self.is_running = False self.server_lock = Lock() self.shutdown_lock = Lock() def serve_forever(self): with self.server_lock: IOLoop().make_current() self.is_running = True self.logger.debug('Webhook Server started.') self.http_server.listen(self.port, address=self.listen) self.loop = IOLoop.current() self.loop.start() self.logger.debug('Webhook Server stopped.') self.is_running = False def shutdown(self): with self.shutdown_lock: if not self.is_running: self.logger.warning('Webhook Server already stopped.') return else: self.loop.add_callback(self.loop.stop) def handle_error(self, request, client_address): """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, bot, update_queue, default_quote=None): self.shared_objects = {"bot": bot, "update_queue": update_queue, "default_quote": default_quote} handlers = [ (r"{0}/?".format(webhook_path), WebhookHandler, self.shared_objects) ] # noqa tornado.web.Application.__init__(self, handlers) def log_request(self, handler): pass # WebhookHandler, process webhook calls class WebhookHandler(tornado.web.RequestHandler): SUPPORTED_METHODS = ["POST"] def __init__(self, application, request, **kwargs): super(WebhookHandler, self).__init__(application, request, **kwargs) self.logger = logging.getLogger(__name__) def initialize(self, bot, update_queue, default_quote=None): self.bot = bot self.update_queue = update_queue self._default_quote = default_quote def set_default_headers(self): self.set_header("Content-Type", 'application/json; charset="utf-8"') def post(self): self.logger.debug('Webhook triggered') self._validate_post() json_string = bytes_to_native_str(self.request.body) data = json.loads(json_string) self.set_status(200) self.logger.debug('Webhook received data: ' + json_string) data['default_quote'] = self._default_quote update = Update.de_json(data, self.bot) self.logger.debug('Received Update with ID %d on Webhook' % update.update_id) self.update_queue.put(update) def _validate_post(self): 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, **kwargs): """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(WebhookHandler, self).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-12.4.2/telegram/vendor/000077500000000000000000000000001362023133600205755ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/vendor/__init__.py000066400000000000000000000000001362023133600226740ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/vendor/ptb_urllib3/000077500000000000000000000000001362023133600230165ustar00rootroot00000000000000python-telegram-bot-12.4.2/telegram/version.py000066400000000000000000000015011362023133600213340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. __version__ = '12.4.2' python-telegram-bot-12.4.2/telegram/webhookinfo.py000066400000000000000000000066251362023133600221750ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have 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 telegram import TelegramObject class WebhookInfo(TelegramObject): """This object represents a Telegram WebhookInfo. Contains information about the current status of a webhook. 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. 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. Args: url (:obj:`str`): Webhook URL, may be empty if webhook is not set up. has_custom_certificate (:obj:`bool`): True, if a custom certificate was provided for webhook certificate checks. pending_update_count (:obj:`int`): Number of updates awaiting delivery. last_error_date (:obj:`int`, optional): Unix time for the most recent error that happened when trying todeliver 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. """ def __init__(self, url, has_custom_certificate, pending_update_count, last_error_date=None, last_error_message=None, max_connections=None, allowed_updates=None, **kwargs): # Required self.url = url self.has_custom_certificate = has_custom_certificate self.pending_update_count = pending_update_count self.last_error_date = last_error_date self.last_error_message = last_error_message self.max_connections = max_connections self.allowed_updates = allowed_updates @classmethod def de_json(cls, data, bot): if not data: return None return cls(**data) python-telegram-bot-12.4.2/tests/000077500000000000000000000000001362023133600166425ustar00rootroot00000000000000python-telegram-bot-12.4.2/tests/__init__.py000066400000000000000000000000001362023133600207410ustar00rootroot00000000000000python-telegram-bot-12.4.2/tests/bots.py000066400000000000000000000053321362023133600201660ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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 # 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 = [ { 'token': '579694714:AAHRLL5zBVy4Blx2jRFKe1HlfnXCg08WuLY', 'payment_provider_token': '284685063:TEST:NjQ0NjZlNzI5YjJi', 'chat_id': '675666224', 'super_group_id': '-1001493296829', 'channel_id': '@pythontelegrambottests', 'bot_name': 'PTB tests fallback 1', 'bot_username': '@ptb_fallback_1_bot' }, { 'token': '558194066:AAEEylntuKSLXj9odiv3TnX7Z5KY2J3zY3M', 'payment_provider_token': '284685063:TEST:YjEwODQwMTFmNDcy', 'chat_id': '675666224', 'super_group_id': '-1001493296829', 'channel_id': '@pythontelegrambottests', 'bot_name': 'PTB tests fallback 2', 'bot_username': '@ptb_fallback_2_bot' } ] 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) 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()} python-telegram-bot-12.4.2/tests/conftest.py000066400000000000000000000252131362023133600210440ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 os import sys import re from collections import defaultdict from queue import Queue from threading import Thread, Event from time import sleep import pytest from telegram import (Bot, Message, User, Chat, MessageEntity, Update, InlineQuery, CallbackQuery, ShippingQuery, PreCheckoutQuery, ChosenInlineResult) from telegram.ext import Dispatcher, JobQueue, Updater, BaseFilter, Defaults from telegram.utils.helpers import _UtcOffsetTimezone from tests.bots import get_bot TRAVIS = os.getenv('TRAVIS', False) if TRAVIS: pytest_plugins = ['tests.travis_fold'] 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 @pytest.fixture(scope='session') def bot_info(): return get_bot() @pytest.fixture(scope='session') def bot(bot_info): return make_bot(bot_info) 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 else: 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): for dp in create_dp(bot): yield dp @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 = [] _dp.__stop_event = Event() _dp.__exception_event = Event() _dp.__async_queue = Queue() _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) yield up if up.running: up.stop() @pytest.fixture(scope='function') def thumb_file(): f = open(u'tests/data/thumb.jpg', 'rb') yield f f.close() @pytest.fixture(scope='class') def class_thumb_file(): f = open(u'tests/data/thumb.jpg', 'rb') yield f f.close() def pytest_configure(config): if sys.version_info >= (3,): 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): return Bot(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='function') def mock_filter(): class MockFilter(BaseFilter): def __init__(self): self.tested = False def filter(self, message): self.tested = True return MockFilter() def get_false_update_fixture_decorator_params(): message = Message(1, User(1, '', False), DATE, Chat(1, ''), 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=[1, 2], ids=lambda h: 'UTC +{hour:0>2}:00'.format(hour=h)) def utc_offset(request): return datetime.timedelta(hours=request.param) @pytest.fixture() def timezone(utc_offset): return _UtcOffsetTimezone(utc_offset) python-telegram-bot-12.4.2/tests/data/000077500000000000000000000000001362023133600175535ustar00rootroot00000000000000python-telegram-bot-12.4.2/tests/data/game.gif000066400000000000000000001072671362023133600211700ustar00rootroot00000000000000GIF89ahH4n: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-12.4.2/tests/data/game.png000066400000000000000000001200411362023133600211700ustar00rootroot00000000000000PNG  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-12.4.2/tests/data/telegram000066400000000000000000000000001362023133600212640ustar00rootroot00000000000000python-telegram-bot-12.4.2/tests/data/telegram.gif000066400000000000000000000074461362023133600220550ustar00rootroot00000000000000GIF87aX___???߿,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-12.4.2/tests/data/telegram.jpg000066400000000000000000001004361362023133600220610ustar00rootroot00000000000000JFIFHHC       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-12.4.2/tests/data/telegram.mp3000066400000000000000000003600501362023133600220000ustar00rootroot00000000000000ID3TXXXSoftwareLavf56.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-12.4.2/tests/data/telegram.png000066400000000000000000000312241362023133600220630ustar00rootroot00000000000000PNG  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-12.4.2/tests/data/telegram2.mp4000066400000000000000000004017641362023133600220730ustar00rootroot00000000000000 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*Ǵq5K14VKf>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-12.4.2/tests/data/telegram_sticker.png000066400000000000000000001220211362023133600236030ustar00rootroot00000000000000PNG  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-12.4.2/tests/data/thumb.jpg000066400000000000000000000052701362023133600214000ustar00rootroot00000000000000JFIFC       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-12.4.2/tests/plugin_github_group.py000066400000000000000000000046501362023133600232750ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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('##[group] {}\n'.format(text)) 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.keys(): 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('\n##[group] {}'.format(name)) yield if nextitem is None or _get_name(nextitem.location) != name: terminal.write('\n##[endgroup]') python-telegram-bot-12.4.2/tests/test_animation.py000066400000000000000000000222221362023133600222320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 PhotoSize, Animation, Voice, TelegramError from telegram.utils.helpers import escape_markdown @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(object): animation_file_id = 'CgADAQADngIAAuyVeEez0xRovKi9VAI' 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_creation(self, animation): assert isinstance(animation, Animation) assert isinstance(animation.file_id, str) assert animation.file_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) @pytest.mark.timeout(10) 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, filename=self.file_name, thumb=thumb_file) assert isinstance(message.animation, Animation) assert isinstance(message.animation.file_id, str) assert message.animation.file_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 @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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 message.animation.file_id is not None assert message.animation.duration == animation.duration assert message.animation.mime_type == animation.mime_type assert message.animation.file_size == animation.file_size @flaky(3, 1) @pytest.mark.timeout(10) @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.timeout(10) @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.timeout(10) @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) @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.skip(reason='Doesnt work without API 4.5') 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('telegram.utils.request.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, '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.thumb == animation.thumb 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['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) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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 test(*args, **kwargs): return args[1] == animation.file_id monkeypatch.setattr('telegram.Bot.get_file', test) assert animation.get_file() def test_equality(self): a = Animation(self.animation_file_id, self.height, self.width, self.duration) b = Animation(self.animation_file_id, self.height, self.width, self.duration) d = Animation('', 0, 0, 0) e = Voice(self.animation_file_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-12.4.2/tests/test_audio.py000066400000000000000000000221611362023133600213560ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 Audio, TelegramError, Voice from telegram.utils.helpers import escape_markdown @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(object): caption = 'Test *audio*' performer = 'Leandro Toledo' title = 'Teste' 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 def test_creation(self, audio): # Make sure file has been uploaded. assert isinstance(audio, Audio) assert isinstance(audio.file_id, str) assert audio.file_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) @pytest.mark.timeout(10) 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, 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 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.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 @flaky(3, 1) @pytest.mark.timeout(10) 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_path.startswith('https://') new_file.download('telegram.mp3') assert os.path.isfile('telegram.mp3') @flaky(3, 1) @pytest.mark.timeout(10) 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 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) @pytest.mark.timeout(10) @pytest.mark.skip(reason='Doesnt work without API 4.5') 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('telegram.utils.request.Request.post', test) message = bot.send_audio(audio=audio, chat_id=chat_id) assert message @flaky(3, 1) @pytest.mark.timeout(10) @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.timeout(10) @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.timeout(10) @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_de_json(self, bot, audio): json_dict = {'file_id': 'not a file id', 'duration': self.duration, 'performer': self.performer, 'title': self.title, '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 == 'not a file id' assert json_audio.duration == self.duration assert json_audio.performer == self.performer assert json_audio.title == self.title 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['duration'] == audio.duration assert audio_dict['mime_type'] == audio.mime_type assert audio_dict['file_size'] == audio.file_size @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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 test(*args, **kwargs): return args[1] == audio.file_id monkeypatch.setattr('telegram.Bot.get_file', test) assert audio.get_file() def test_equality(self, audio): a = Audio(audio.file_id, audio.duration) b = Audio(audio.file_id, audio.duration) c = Audio(audio.file_id, 0) d = Audio('', audio.duration) e = Voice(audio.file_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-12.4.2/tests/test_bot.py000066400000000000000000001145071362023133600210470ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 sys import time import datetime as dtm from platform import python_implementation import pytest from flaky import flaky from future.utils import string_types from telegram import (Bot, Update, ChatAction, TelegramError, User, InlineKeyboardMarkup, InlineKeyboardButton, InlineQueryResultArticle, InputTextMessageContent, ShippingOption, LabeledPrice, ChatPermissions, Poll, InlineQueryResultDocument) from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter from telegram.utils.helpers import from_timestamp, escape_markdown BASE_TIME = time.time() HIGHSCORE_DELTA = 1450000000 @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) class TestBot(object): @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) @flaky(3, 1) @pytest.mark.timeout(10) 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() @flaky(3, 1) @pytest.mark.timeout(10) def test_get_me_and_properties(self, bot): get_me_bot = bot.get_me() assert isinstance(get_me_bot, User) assert get_me_bot.id == bot.id assert get_me_bot.username == bot.username assert get_me_bot.first_name == bot.first_name assert get_me_bot.last_name == bot.last_name assert get_me_bot.name == bot.name @flaky(3, 1) @pytest.mark.timeout(10) 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 @flaky(3, 1) @pytest.mark.timeout(10) def test_forward_message(self, bot, chat_id, message): message = bot.forward_message(chat_id, from_chat_id=chat_id, message_id=message.message_id) assert message.text == message.text assert message.forward_from.username == message.from_user.username assert isinstance(message.forward_date, dtm.datetime) @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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' message = bot.send_venue(chat_id=chat_id, title=title, address=address, latitude=latitude, longitude=longitude, foursquare_id=foursquare_id, foursquare_type=foursquare_type) 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 @flaky(3, 1) @pytest.mark.timeout(10) @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) assert message.contact assert message.contact.phone_number == phone_number assert message.contact.first_name == first_name assert message.contact.last_name == last_name # TODO: Add bot to group to test polls too @flaky(3, 1) @pytest.mark.timeout(10) def test_send_and_stop_poll(self, bot, super_group_id): question = 'Is this a test?' answers = ['Yes', 'No', 'Maybe'] message = bot.send_poll(chat_id=super_group_id, question=question, options=answers, timeout=60) 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_closed poll = bot.stop_poll(chat_id=super_group_id, message_id=message.message_id, 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 @flaky(3, 1) @pytest.mark.timeout(10) def test_send_game(self, bot, chat_id): game_short_name = 'test_game' message = bot.send_game(chat_id, game_short_name) 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 != '' assert message.game.photo[0].file_size == 851 @flaky(3, 1) @pytest.mark.timeout(10) def test_send_chat_action(self, bot, chat_id): assert bot.send_chat_action(chat_id, ChatAction.TYPING) # 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('telegram.utils.request.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') 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('telegram.utils.request.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('telegram.utils.request.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') @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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. # TODO: Needs improvement. No feasable way to test until bots can add members. def test_kick_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 return chat_id and user_id and until_date monkeypatch.setattr('telegram.utils.request.Request.post', test) until = from_timestamp(1577887200) assert bot.kick_chat_member(2, 32) assert bot.kick_chat_member(2, 32, until_date=until) assert bot.kick_chat_member(2, 32, until_date=1577887200) # TODO: Needs improvement. def test_unban_chat_member(self, monkeypatch, bot): 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('telegram.utils.request.Request.post', test) assert bot.unban_chat_member(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('telegram.utils.request.Request.post', test) assert bot.set_chat_permissions(2, chat_permissions) # 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('telegram.utils.request.Request.post', test) assert bot.answer_callback_query(23, text='answer', show_alert=True, url='no_url', cache_time=1) @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) @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) @pytest.mark.timeout(10) 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' # edit_message_media is tested in test_inputmedia @flaky(3, 1) @pytest.mark.timeout(10) @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) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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) @flaky(3, 1) @pytest.mark.timeout(15) @pytest.mark.xfail @pytest.mark.skipif(os.getenv('APPVEYOR') and (sys.version_info < (3, 6)), reason='only run on 3.6 on appveyor') 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) 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 @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) def test_get_chat(self, bot, super_group_id): chat = bot.get_chat(super_group_id) assert chat.type == 'supergroup' assert chat.title == '>>> telegram.Bot(test)' assert chat.id == int(super_group_id) # TODO: Add bot to group to test there too @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.parametrize('default_bot', [{'quote': True}], indirect=True) def test_get_chat_default_quote(self, default_bot, super_group_id): message = default_bot.send_message(super_group_id, text="test_get_chat_default_quote") assert default_bot.pin_chat_message(chat_id=super_group_id, message_id=message.message_id, disable_notification=True) chat = default_bot.get_chat(super_group_id) assert chat.pinned_message.default_quote is True assert default_bot.unpinChatMessage(super_group_id) @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) def test_get_chat_members_count(self, bot, channel_id): count = bot.get_chat_members_count(channel_id) assert isinstance(count, int) assert count > 3 @flaky(3, 1) @pytest.mark.timeout(10) 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 yet.") def test_set_chat_sticker_set(self): pass @pytest.mark.skip(reason="Not implemented yet.") def test_delete_chat_sticker_set(self): pass @flaky(3, 1) @pytest.mark.timeout(10) def test_set_game_score_1(self, bot, chat_id): # NOTE: numbering of methods assures proper order between test_set_game_scoreX methods game_short_name = 'test_game' game = bot.send_game(chat_id, game_short_name) message = bot.set_game_score( user_id=chat_id, score=int(BASE_TIME) - HIGHSCORE_DELTA, chat_id=game.chat_id, message_id=game.message_id) assert message.game.description == game.game.description assert message.game.animation.file_id == game.game.animation.file_id assert message.game.photo[0].file_size == game.game.photo[0].file_size assert message.game.text != game.game.text @flaky(3, 1) @pytest.mark.timeout(10) def test_set_game_score_2(self, bot, chat_id): # NOTE: numbering of methods assures proper order between test_set_game_scoreX methods game_short_name = 'test_game' game = bot.send_game(chat_id, game_short_name) score = int(BASE_TIME) - HIGHSCORE_DELTA + 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.animation.file_id == game.game.animation.file_id assert message.game.photo[0].file_size == game.game.photo[0].file_size assert message.game.text == game.game.text @flaky(3, 1) @pytest.mark.timeout(10) def test_set_game_score_3(self, bot, chat_id): # NOTE: numbering of methods assures proper order between test_set_game_scoreX methods game_short_name = 'test_game' game = bot.send_game(chat_id, game_short_name) score = int(BASE_TIME) - HIGHSCORE_DELTA - 1 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) @flaky(3, 1) @pytest.mark.timeout(10) def test_set_game_score_4(self, bot, chat_id): # NOTE: numbering of methods assures proper order between test_set_game_scoreX methods game_short_name = 'test_game' game = bot.send_game(chat_id, game_short_name) score = int(BASE_TIME) - HIGHSCORE_DELTA - 2 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.animation.file_id == game.game.animation.file_id assert message.game.photo[0].file_size == game.game.photo[0].file_size # For some reason the returned message does not contain the updated score. need to fetch # the game again... game2 = bot.send_game(chat_id, game_short_name) assert str(score) in game2.game.text @flaky(3, 1) @pytest.mark.timeout(10) def test_set_game_score_too_low_score(self, bot, chat_id): # We need a game to set the score for game_short_name = 'test_game' game = bot.send_game(chat_id, game_short_name) with pytest.raises(BadRequest): bot.set_game_score(user_id=chat_id, score=100, chat_id=game.chat_id, message_id=game.message_id) @flaky(3, 1) @pytest.mark.timeout(10) 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 pytest.approx(high_scores[0].score, abs=20) == int(BASE_TIME) - HIGHSCORE_DELTA # send_invoice is tested in test_invoice # TODO: Needs improvement. Need incoming shippping 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('telegram.utils.request.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('telegram.utils.request.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) # 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('telegram.utils.request.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('telegram.utils.request.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) @pytest.mark.timeout(10) 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()) @flaky(3, 1) @pytest.mark.timeout(10) def test_promote_chat_member(self, bot, channel_id): # 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, 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) @flaky(3, 1) @pytest.mark.timeout(10) 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, string_types) assert invite_link != '' @flaky(3, 1) @pytest.mark.timeout(10) def test_delete_chat_photo(self, bot, channel_id): assert bot.delete_chat_photo(channel_id) @flaky(3, 1) @pytest.mark.timeout(10) def test_set_chat_photo(self, bot, channel_id): with open('tests/data/telegram_test_channel.jpg', 'rb') as f: assert bot.set_chat_photo(channel_id, f) @flaky(3, 1) @pytest.mark.timeout(10) def test_set_chat_title(self, bot, channel_id): assert bot.set_chat_title(channel_id, '>>> telegram.Bot() - Tests') @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) def test_pin_and_unpin_message(self, bot, super_group_id): message = bot.send_message(super_group_id, text="test_pin_message") assert bot.pin_chat_message(chat_id=super_group_id, message_id=message.message_id, disable_notification=True) chat = bot.get_chat(super_group_id) assert chat.pinned_message == message assert bot.unpinChatMessage(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 submition 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) @pytest.mark.timeout(10) @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.timeout(10) @pytest.mark.parametrize('default_bot', [{'quote': True}], indirect=True) def test_send_message_default_quote(self, default_bot, chat_id): message = default_bot.send_message(chat_id, 'test') assert message.default_quote is True python-telegram-bot-12.4.2/tests/test_callbackcontext.py000066400000000000000000000125521362023133600234210ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 from telegram.ext import CallbackContext class TestCallbackContext(object): 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, User(1, 'user', False), None, Chat(1, 'chat'))) 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, User(2, 'user', False), None, Chat(2, 'chat'))) 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, User(1, 'user', False), None, Chat(1, 'chat'))) 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 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, User(1, 'user', False), None, Chat(1, 'chat'))) callback_context = CallbackContext.from_update(update, cdp) with pytest.raises(AttributeError): callback_context.chat_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 python-telegram-bot-12.4.2/tests/test_callbackquery.py000066400000000000000000000147171362023133600231070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @pytest.fixture(scope='class', 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 else: cbq.inline_message_id = TestCallbackQuery.inline_message_id return cbq class TestCallbackQuery(object): id = 'id' from_user = User(1, 'test_user', False) chat_instance = 'chat_instance' message = Message(3, User(5, 'bot', False), None, Chat(4, 'private')) data = 'data' inline_message_id = 'inline_message_id' game_short_name = 'the_game' 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, 'default_quote': True} 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.message.default_quote is True 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 test(*args, **kwargs): return args[0] == callback_query.id monkeypatch.setattr(callback_query.bot, 'answerCallbackQuery', test) # TODO: PEP8 assert callback_query.answer() def test_edit_message_text(self, monkeypatch, callback_query): def test(*args, **kwargs): text = args[0] == 'test' try: id = kwargs['inline_message_id'] == callback_query.inline_message_id return id and text except KeyError: chat_id = kwargs['chat_id'] == callback_query.message.chat_id message_id = kwargs['message_id'] == callback_query.message.message_id return chat_id and message_id and text monkeypatch.setattr(callback_query.bot, 'edit_message_text', test) 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 test(*args, **kwargs): caption = kwargs['caption'] == 'new caption' try: id = kwargs['inline_message_id'] == callback_query.inline_message_id return id and caption except KeyError: id = kwargs['chat_id'] == callback_query.message.chat_id message = kwargs['message_id'] == callback_query.message.message_id return id and message and caption monkeypatch.setattr(callback_query.bot, 'edit_message_caption', test) 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 test(*args, **kwargs): reply_markup = kwargs['reply_markup'] == [['1', '2']] try: id = kwargs['inline_message_id'] == callback_query.inline_message_id return id and reply_markup except KeyError: id = kwargs['chat_id'] == callback_query.message.chat_id message = kwargs['message_id'] == callback_query.message.message_id return id and message and reply_markup monkeypatch.setattr(callback_query.bot, 'edit_message_reply_markup', test) assert callback_query.edit_message_reply_markup(reply_markup=[['1', '2']]) assert callback_query.edit_message_reply_markup([['1', '2']]) 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, 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-12.4.2/tests/test_callbackqueryhandler.py000066400000000000000000000203111362023133600244300ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, User(1, '', False), None, Chat(1, ''), 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(object): test_flag = False @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) 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 python-telegram-bot-12.4.2/tests/test_chat.py000066400000000000000000000222101362023133600211670ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 from telegram import User, Message @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) class TestChat(object): 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, ) 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() } 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 def test_de_json_default_quote(self, bot): json_dict = { 'id': self.id, 'type': self.type, 'pinned_message': Message( message_id=123, from_user=None, date=None, chat=None ).to_dict(), 'default_quote': True } chat = Chat.de_json(json_dict, bot) assert chat.pinned_message.default_quote is True 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() def test_link(self, chat): assert chat.link == 'https://t.me/{}'.format(chat.username) chat.username = None assert chat.link is None def test_send_action(self, monkeypatch, chat): def test(*args, **kwargs): id = args[0] == chat.id action = kwargs['action'] == ChatAction.TYPING return id and action monkeypatch.setattr(chat.bot, 'send_chat_action', test) assert chat.send_action(action=ChatAction.TYPING) def test_leave(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id monkeypatch.setattr(chat.bot, 'leave_chat', test) assert chat.leave() def test_get_administrators(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id monkeypatch.setattr(chat.bot, 'get_chat_administrators', test) assert chat.get_administrators() def test_get_members_count(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id monkeypatch.setattr(chat.bot, 'get_chat_members_count', test) assert chat.get_members_count() def test_get_member(self, monkeypatch, chat): def test(*args, **kwargs): chat_id = args[0] == chat.id user_id = args[1] == 42 return chat_id and user_id monkeypatch.setattr(chat.bot, 'get_chat_member', test) assert chat.get_member(42) def test_kick_member(self, monkeypatch, chat): def test(*args, **kwargs): chat_id = args[0] == chat.id user_id = args[1] == 42 until = kwargs['until_date'] == 43 return chat_id and user_id and until monkeypatch.setattr(chat.bot, 'kick_chat_member', test) assert chat.kick_member(42, until_date=43) def test_unban_member(self, monkeypatch, chat): def test(*args, **kwargs): chat_id = args[0] == chat.id user_id = args[1] == 42 return chat_id and user_id monkeypatch.setattr(chat.bot, 'unban_chat_member', test) assert chat.unban_member(42) def test_set_permissions(self, monkeypatch, chat): def test(*args, **kwargs): chat_id = args[0] == chat.id permissions = args[1] == self.permissions return chat_id and permissions monkeypatch.setattr(chat.bot, 'set_chat_permissions', test) assert chat.set_permissions(self.permissions) def test_instance_method_send_message(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id and args[1] == 'test' monkeypatch.setattr(chat.bot, 'send_message', test) assert chat.send_message('test') def test_instance_method_send_photo(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id and args[1] == 'test_photo' monkeypatch.setattr(chat.bot, 'send_photo', test) assert chat.send_photo('test_photo') def test_instance_method_send_audio(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id and args[1] == 'test_audio' monkeypatch.setattr(chat.bot, 'send_audio', test) assert chat.send_audio('test_audio') def test_instance_method_send_document(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id and args[1] == 'test_document' monkeypatch.setattr(chat.bot, 'send_document', test) assert chat.send_document('test_document') def test_instance_method_send_sticker(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id and args[1] == 'test_sticker' monkeypatch.setattr(chat.bot, 'send_sticker', test) assert chat.send_sticker('test_sticker') def test_instance_method_send_video(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id and args[1] == 'test_video' monkeypatch.setattr(chat.bot, 'send_video', test) assert chat.send_video('test_video') def test_instance_method_send_video_note(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id and args[1] == 'test_video_note' monkeypatch.setattr(chat.bot, 'send_video_note', test) assert chat.send_video_note('test_video_note') def test_instance_method_send_voice(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id and args[1] == 'test_voice' monkeypatch.setattr(chat.bot, 'send_voice', test) assert chat.send_voice('test_voice') def test_instance_method_send_animation(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id and args[1] == 'test_animation' monkeypatch.setattr(chat.bot, 'send_animation', test) assert chat.send_animation('test_animation') def test_instance_method_send_poll(self, monkeypatch, chat): def test(*args, **kwargs): return args[0] == chat.id and args[1] == 'test_poll' monkeypatch.setattr(chat.bot, 'send_poll', test) assert chat.send_poll('test_poll') 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-12.4.2/tests/test_chatmember.py000066400000000000000000000077421362023133600223740ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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, ChatMember 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_member(user): return ChatMember(user, TestChatMember.status) class TestChatMember(object): status = ChatMember.CREATOR def test_de_json_required_args(self, bot, user): json_dict = {'user': user.to_dict(), 'status': self.status} chat_member = ChatMember.de_json(json_dict, bot) assert chat_member.user == user assert chat_member.status == self.status def test_de_json_all_args(self, bot, user): time = datetime.datetime.utcnow() json_dict = {'user': user.to_dict(), 'status': self.status, '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} chat_member = ChatMember.de_json(json_dict, bot) assert chat_member.user == user assert chat_member.status == self.status assert chat_member.can_be_edited is False assert chat_member.can_change_info is True assert chat_member.can_post_messages is False assert chat_member.can_edit_messages is True assert chat_member.can_delete_messages is True assert chat_member.can_invite_users is False assert chat_member.can_restrict_members is True assert chat_member.can_pin_messages is False assert chat_member.can_promote_members is True assert chat_member.can_send_messages is False assert chat_member.can_send_media_messages is True assert chat_member.can_send_polls is False assert chat_member.can_send_other_messages is True assert chat_member.can_add_web_page_previews is False def test_to_dict(self, chat_member): chat_member_dict = chat_member.to_dict() assert isinstance(chat_member_dict, dict) assert chat_member_dict['user'] == chat_member.user.to_dict() assert chat_member['status'] == chat_member.status def test_equality(self): a = ChatMember(User(1, '', False), ChatMember.ADMINISTRATOR) b = ChatMember(User(1, '', False), ChatMember.ADMINISTRATOR) d = ChatMember(User(2, '', False), ChatMember.ADMINISTRATOR) d2 = ChatMember(User(1, '', False), ChatMember.CREATOR) 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) python-telegram-bot-12.4.2/tests/test_chatpermissions.py000066400000000000000000000071411362023133600234710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @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(object): 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_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 python-telegram-bot-12.4.2/tests/test_choseninlineresult.py000066400000000000000000000062451362023133600241770ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): result_id = 'result id' query = 'query text' 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, 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-12.4.2/tests/test_choseninlineresulthandler.py000066400000000000000000000146031362023133600255320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, User(1, '', False), None, Chat(1, ''), 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(object): test_flag = False @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.chosen_inline_result, ChosenInlineResult)) 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 python-telegram-bot-12.4.2/tests/test_commandhandler.py000066400000000000000000000434771362023133600232460ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): """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, None) 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) == 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) == 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' @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 == '{}@{}'.format(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('/not{}'.format(command[1:]))) assert not is_match(handler, make_command_update('not {} at start'.format(command))) @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 = '{}@{}'.format(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)) @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('not {} at start'.format(text))) 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_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-12.4.2/tests/test_constants.py000066400000000000000000000037771362023133600223050ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): @flaky(3, 1) @pytest.mark.timeout(10) 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', message='MAX_MESSAGE_LENGTH is no longer valid'): bot.send_message(chat_id=chat_id, text='a' * (constants.MAX_MESSAGE_LENGTH + 1)) @flaky(3, 1) @pytest.mark.timeout(10) 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", message='MAX_CAPTION_LENGTH is no longer valid'): with open('tests/data/telegram.png', 'rb') as f: bot.send_photo(photo=f, caption=bad_caption, chat_id=chat_id) python-telegram-bot-12.4.2/tests/test_contact.py000066400000000000000000000067031362023133600217140ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 Contact, Voice @pytest.fixture(scope='class') def contact(): return Contact(TestContact.phone_number, TestContact.first_name, TestContact.last_name, TestContact.user_id) class TestContact(object): phone_number = '+11234567890' first_name = 'Leandro' last_name = 'Toledo' user_id = 23 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('telegram.utils.request.Request.post', test) message = bot.send_contact(contact=contact, chat_id=chat_id) assert message 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('', 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-12.4.2/tests/test_conversationhandler.py000066400000000000000000001155011362023133600243260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 telegram import (CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message, PreCheckoutQuery, ShippingQuery, Update, User, MessageEntity) from telegram.ext import (ConversationHandler, CommandHandler, CallbackQueryHandler, MessageHandler, Filters, InlineQueryHandler, CallbackContext) @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 TestConversationHandler(object): # 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) # Test related @pytest.fixture(autouse=True) def reset(self): self.current_state = dict() 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 def start(self, bot, update): if isinstance(update, Update): return self._set_state(update, self.THIRSTY) else: return self._set_state(bot, self.THIRSTY) def end(self, bot, update): return self._set_state(update, self.END) def start_end(self, bot, update): return self._set_state(update, self.END) def start_none(self, bot, update): return self._set_state(update, None) def brew(self, bot, update): if isinstance(update, Update): return self._set_state(update, self.BREWING) else: return self._set_state(bot, self.BREWING) def drink(self, bot, update): return self._set_state(update, self.DRINKING) def code(self, bot, update): return self._set_state(update, self.CODING) def passout(self, bot, update): assert update.message.text == '/brew' assert isinstance(update, Update) self.is_timeout = True def passout2(self, bot, update): assert isinstance(update, Update) self.is_timeout = True def passout_context(self, update, context): assert update.message.text == '/brew' assert isinstance(context, CallbackContext) self.is_timeout = True def passout2_context(self, update, context): assert isinstance(context, CallbackContext) self.is_timeout = True # Drinking actions (nested) def hold(self, bot, update): return self._set_state(update, self.HOLDING) def sip(self, bot, update): return self._set_state(update, self.SIPPING) def swallow(self, bot, update): return self._set_state(update, self.SWALLOWING) def replenish(self, bot, update): return self._set_state(update, self.REPLENISHING) def stop(self, bot, update): return self._set_state(update, self.STOPPING) # Tests 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, user1, None, self.group, 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, user1, None, self.group, 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, user1, None, self.group, 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_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, user1, None, self.group, 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, user1, None, self.group, 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, user1, None, self.group, 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, user1, None, self.group, 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): start_end_async = (lambda bot, update: dp.run_async(self.start_end, bot, update)) handler = ConversationHandler( entry_points=[CommandHandler('start', start_end_async)], 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, user1, None, self.group, 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(.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(.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, user1, None, self.group, 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): start_none_async = (lambda bot, update: dp.run_async(self.start_none, bot, update)) handler = ConversationHandler( entry_points=[CommandHandler('start', start_none_async)], 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, user1, None, self.group, 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(.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(.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=[CommandHandler('start', self.start_end)], states={}, fallbacks=[]) message = Message(0, None, None, Chat(0, Chat.CHANNEL, 'Misses Test'), bot=bot) update = Update(0, message=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, user1, None, self.group, 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_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, user1, None, self.group, 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.5) dp.job_queue.tick() 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.job_queue.tick() dp.process_update(Update(update_id=2, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING sleep(0.5) dp.job_queue.tick() assert handler.conversations.get((self.group.id, user1.id)) is None 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=.25 /brew (timeout=.75) # t=.5 original timeout # t=.6 /pourCoffee (timeout=1.1) # t=.75 second timeout # t=1.1 actual timeout message = Message(0, user1, None, self.group, 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.25) # t=.25 dp.job_queue.tick() 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.35) # t=.6 dp.job_queue.tick() 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(.4) # t=1 dp.job_queue.tick() assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING sleep(.1) # t=1.1 dp.job_queue.tick() 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, user1, None, self.group, 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.job_queue.tick() 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.job_queue.tick() dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user2.id)) == self.THIRSTY sleep(0.5) dp.job_queue.tick() 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, user1, None, self.group, 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.5) dp.job_queue.tick() 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.5) dp.job_queue.tick() 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.5) dp.job_queue.tick() 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, user1, None, self.group, 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.5) cdp.job_queue.tick() 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.5) cdp.job_queue.tick() 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.5) cdp.job_queue.tick() 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 dp.job_queue.tick() 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, user1, None, self.group, 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) dp.job_queue.tick() message.text = '/slowbrew' message.entities[0].length = len('/slowbrew') dp.process_update(Update(update_id=0, message=message)) dp.job_queue.tick() assert handler.conversations.get((self.group.id, user1.id)) is not None assert not self.is_timeout sleep(0.5) dp.job_queue.tick() assert handler.conversations.get((self.group.id, user1.id)) is None assert self.is_timeout 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, user1, None, self.group, 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 python-telegram-bot-12.4.2/tests/test_defaults.py000066400000000000000000000036331362023133600220670ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): def test_data_assignment(self, cdp): defaults = Defaults() with pytest.raises(AttributeError): defaults.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.timeout = True with pytest.raises(AttributeError): defaults.quote = 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-12.4.2/tests/test_dispatcher.py000066400000000000000000000402441362023133600224050ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import sys 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, CommandHandler, CallbackContext, JobQueue, BasePersistence) from telegram.ext.dispatcher import run_async, Dispatcher, DispatcherHandlerStop from telegram.utils.deprecate import TelegramDeprecationWarning from tests.conftest import create_dp from collections import defaultdict @pytest.fixture(scope='function') def dp2(bot): for dp in create_dp(bot): yield dp class TestDispatcher(object): message_update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''), text='Text')) received = None count = 0 @pytest.fixture(autouse=True, name='reset') def reset_fixture(self): self.reset() def reset(self): self.received = None self.count = 0 def error_handler(self, bot, update, error): self.received = 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): raise TelegramError(update.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_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(.1) assert self.received == 'Unauthorized.' # Remove handler dp.remove_error_handler(self.error_handler) self.reset() dp.update_queue.put(error) sleep(.1) assert self.received is None 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 with pytest.raises(TypeError, match='persistence should 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(.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(.1) assert self.count == 1 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(.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(.1) assert self.received == self.message_update.message 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(.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(.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(.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(.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, dp, bot): increment = [] class OwnPersistence(BasePersistence): def __init__(self): super(BasePersistence, self).__init__() self.store_user_data = True self.store_chat_data = True self.store_bot_data = True def get_bot_data(self): return dict() 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 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, User(1, "Test", False), None, Chat(1, "lala"), 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) dp.add_handler(CommandHandler('start', start1)) dp.add_error_handler(error) dp.process_update(update) assert increment == ["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(.1) assert self.received == 'Unauthorized.' def test_sensible_worker_thread_names(self, dp2): thread_names = [thread.name for thread in getattr(dp2, '_Dispatcher__async_threads')] print(thread_names) for thread_name in thread_names: assert thread_name.startswith("Bot:{}:worker:".format(dp2.bot.id)) @pytest.mark.skipif(sys.version_info < (3, 0), reason='pytest fails this for no reason') 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) python-telegram-bot-12.4.2/tests/test_document.py000066400000000000000000000207551362023133600221020ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 Document, PhotoSize, TelegramError, Voice from telegram.utils.helpers import escape_markdown @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(object): 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 def test_creation(self, document): assert isinstance(document, Document) assert isinstance(document.file_id, str) assert document.file_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) @pytest.mark.timeout(10) 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, 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.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 @flaky(3, 1) @pytest.mark.timeout(10) 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_path.startswith('https://') new_file.download('telegram.png') assert os.path.isfile('telegram.png') @flaky(3, 1) @pytest.mark.timeout(10) 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(document.thumb, PhotoSize) assert document.file_name == 'telegram.gif' assert document.mime_type == 'image/gif' assert document.file_size == 3878 @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.skip(reason='Doesnt work without API 4.5') 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 def test_send_with_document(self, monkeypatch, bot, chat_id, document): def test(_, url, data, **kwargs): return data['document'] == document.file_id monkeypatch.setattr('telegram.utils.request.Request.post', test) message = bot.send_document(document=document, chat_id=chat_id) assert message @flaky(3, 1) @pytest.mark.timeout(10) @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.timeout(10) @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.timeout(10) @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) def test_de_json(self, bot, document): json_dict = {'file_id': 'not a file 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 == 'not a file 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_name'] == document.file_name assert document_dict['mime_type'] == document.mime_type assert document_dict['file_size'] == document.file_size @flaky(3, 1) @pytest.mark.timeout(10) def test_error_send_empty_file(self, bot, chat_id): with open(os.devnull, 'rb') as f: with pytest.raises(TelegramError): bot.send_document(chat_id=chat_id, document=f) @flaky(3, 1) @pytest.mark.timeout(10) 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 test(*args, **kwargs): return args[1] == document.file_id monkeypatch.setattr('telegram.Bot.get_file', test) assert document.get_file() def test_equality(self, document): a = Document(document.file_id) b = Document(document.file_id) d = Document('') e = Voice(document.file_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-12.4.2/tests/test_encryptedcredentials.py000066400000000000000000000051301362023133600244650ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): data = 'data' hash = 'hash' secret = 'secret' 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-12.4.2/tests/test_encryptedpassportelement.py000066400000000000000000000100421362023133600254130ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): 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_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-12.4.2/tests/test_error.py000066400000000000000000000075161362023133600214150ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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.error import Unauthorized, InvalidToken, NetworkError, BadRequest, TimedOut, \ ChatMigrated, RetryAfter, Conflict class TestErrors(object): 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 seconds"): raise RetryAfter(12) def test_conflict(self): with pytest.raises(Conflict, match='Something something.'): raise Conflict('Something something.') python-telegram-bot-12.4.2/tests/test_file.py000066400000000000000000000122571362023133600212010ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 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, file_path=TestFile.file_path, file_size=TestFile.file_size, bot=bot) class TestFile(object): file_id = 'NOTVALIDDOESNOTMATTER' file_path = ( u'https://api.org/file/bot133505823:AAHZFMHno3mzVLErU5b5jJvaeG--qUyLyG0/document/file_3') file_size = 28232 file_content = u'Saint-Saëns'.encode('utf-8') # Intentionally contains unicode chars. def test_de_json(self, bot): json_dict = { 'file_id': self.file_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_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_path'] == file.file_path assert file_dict['file_size'] == file.file_size @flaky(3, 1) @pytest.mark.timeout(10) def test_error_get_empty_file_id(self, bot): with pytest.raises(TelegramError): bot.get_file(file_id='') 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_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_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_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_equality(self, bot): a = File(self.file_id, bot) b = File(self.file_id, bot) c = File(self.file_id, None) d = File('', bot) e = Voice(self.file_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-12.4.2/tests/test_filters.py000066400000000000000000001032521362023133600217260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 from telegram.ext import Filters, BaseFilter import re @pytest.fixture(scope='function') def update(): return Update(0, Message(0, User(0, 'Testuser', False), datetime.datetime.utcnow(), Chat(0, 'private'))) @pytest.fixture(scope='function', params=MessageEntity.ALL_TYPES) def message_entity(request): return MessageEntity(request.param, 0, 0, url='', user='') class TestFilters(object): 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_iterable(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_iterable(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) == 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) == 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) == 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) == 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) == 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) == 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' filter = (Filters.regex('test') & ((Filters.status_update | Filters.forwarded) | Filters.regex('out'))) result = filter(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 2 assert all([type(res) == SRE_TYPE for res in matches]) update.message.forward_date = datetime.datetime.utcnow() result = filter(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert all([type(res) == SRE_TYPE for res in matches]) update.message.text = 'test it' result = filter(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert all([type(res) == SRE_TYPE for res in matches]) update.message.forward_date = False result = filter(update) assert not result update.message.text = 'test it out' result = filter(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert all([type(res) == SRE_TYPE for res in matches]) update.message.pinned_message = True result = filter(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert all([type(res) == SRE_TYPE for res in matches]) update.message.text = 'it out' result = filter(update) assert not result update.message.text = 'test it out' update.message.forward_date = None update.message.pinned_message = None filter = ((Filters.regex('test') | Filters.command) & (Filters.regex('it') | Filters.status_update)) result = filter(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 2 assert all([type(res) == SRE_TYPE for res in matches]) update.message.text = 'test' result = filter(update) assert not result update.message.pinned_message = True result = filter(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 1 assert all([type(res) == SRE_TYPE for res in matches]) update.message.text = 'nothing' result = filter(update) assert not result update.message.text = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] result = filter(update) assert result assert isinstance(result, bool) update.message.text = '/start it' result = filter(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 1 assert all([type(res) == 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_reply(self, update): another_message = Message(1, User(1, 'TestOther', False), datetime.datetime.utcnow(), Chat(0, 'private')) 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", 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_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.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 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_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_filters_user(self): with pytest.raises(ValueError, match='user_id or username'): Filters.user(user_id=1, username='user') with pytest.raises(ValueError, match='user_id or username'): Filters.user() 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) 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) def test_filters_chat(self): with pytest.raises(ValueError, match='chat_id or username'): Filters.chat(chat_id=-1, username='chat') with pytest.raises(ValueError, match='chat_id or username'): Filters.chat() 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) def test_filters_chat_username(self, update): assert not Filters.chat(username='chat')(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=['@chat1', 'chat_2'])(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) 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 = False 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_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_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 custom = _CustomFilter() with pytest.raises(NotImplementedError): (custom & Filters.text)(update) def test_custom_unnamed_filter(self, update): class Unnamed(BaseFilter): 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): update.message.text = '/test' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] class TestException(Exception): pass class RaisingFilter(BaseFilter): 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_short_circuit_or(self, update): update.message.text = 'test' class TestException(Exception): pass class RaisingFilter(BaseFilter): 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): update.message.text = '/test' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] class DataFilter(BaseFilter): 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): update.message.text = '/test' class DataFilter(BaseFilter): 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'] python-telegram-bot-12.4.2/tests/test_forcereply.py000066400000000000000000000033511362023133600224270ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @pytest.fixture(scope='class') def force_reply(): return ForceReply(TestForceReply.force_reply, TestForceReply.selective) class TestForceReply(object): force_reply = True selective = True @flaky(3, 1) @pytest.mark.timeout(10) 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 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 python-telegram-bot-12.4.2/tests/test_game.py000066400000000000000000000072601362023133600211710ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): title = 'Python-telegram-bot Test Game' description = 'description' photo = [PhotoSize('Blah', 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', 320, 180, 1) 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'} python-telegram-bot-12.4.2/tests/test_gamehighscore.py000066400000000000000000000036041362023133600230630ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): position = 12 user = User(2, 'test user', False) score = 42 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 python-telegram-bot-12.4.2/tests/test_helpers.py000066400000000000000000000212051362023133600217150ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 datetime as dtm import pytest from telegram import Sticker from telegram import Update from telegram import User from telegram.message import Message from telegram.utils import helpers from telegram.utils.helpers import _UtcOffsetTimezone, _datetime_to_float_timestamp # sample time specification values categorised into absolute / delta / time-of-day ABSOLUTE_TIME_SPECS = [dtm.datetime.now(tz=_UtcOffsetTimezone(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=_UtcOffsetTimezone(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 class TestHelpers(object): def test_escape_markdown(self): test_str = '*bold*, _italic_, `code`, [text_link](http://github.com/)' expected_str = '\*bold\*, \_italic\_, \`code\`, \[text\_link](http://github.com/)' assert expected_str == helpers.escape_markdown(test_str) 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_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 datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5, tzinfo=timezone) assert (helpers.to_float_timestamp(datetime) == 1573431976.1 - timezone.utcoffset(None).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 utc_offset = timezone.utcoffset(None) ref_datetime = dtm.datetime(1970, 1, 1, 12) ref_t, time_of_day = _datetime_to_float_timestamp(ref_datetime), ref_datetime.time() # 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(time_of_day.replace(tzinfo=timezone), 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))) @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(self): assert helpers.from_timestamp(1573431976) == dtm.datetime(2019, 11, 11, 0, 26, 16) def test_create_deep_linked_url(self): username = 'JamesTheMock' payload = "hello" expected = "https://t.me/{}?start={}".format(username, payload) actual = helpers.create_deep_linked_url(username, payload) assert expected == actual expected = "https://t.me/{}?startgroup={}".format(username, payload) actual = helpers.create_deep_linked_url(username, payload, group=True) assert expected == actual payload = "" expected = "https://t.me/{}".format(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', 50, 50, 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') python-telegram-bot-12.4.2/tests/test_inlinekeyboardbutton.py000066400000000000000000000111121362023133600245020ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): 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_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 python-telegram-bot-12.4.2/tests/test_inlinekeyboardmarkup.py000066400000000000000000000103041362023133600244700ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @pytest.fixture(scope='class') def inline_keyboard_markup(): return InlineKeyboardMarkup(TestInlineKeyboardMarkup.inline_keyboard) class TestInlineKeyboardMarkup(object): inline_keyboard = [[ InlineKeyboardButton(text='button1', callback_data='data1'), InlineKeyboardButton(text='button2', callback_data='data2') ]] @flaky(3, 1) @pytest.mark.timeout(10) 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_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' python-telegram-bot-12.4.2/tests/test_inlinequery.py000066400000000000000000000061551362023133600226260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @pytest.fixture(scope='class') def inline_query(bot): return InlineQuery(TestInlineQuery.id, TestInlineQuery.from_user, TestInlineQuery.query, TestInlineQuery.offset, location=TestInlineQuery.location, bot=bot) class TestInlineQuery(object): id = 1234 from_user = User(1, 'First name', False) query = 'query text' offset = 'offset' location = Location(8.8, 53.1) def test_de_json(self, bot): json_dict = { 'id': self.id, 'from': self.from_user.to_dict(), 'query': self.query, 'offset': self.offset, 'location': self.location.to_dict() } inline_query_json = InlineQuery.de_json(json_dict, bot) assert inline_query_json.id == self.id assert inline_query_json.from_user == self.from_user assert inline_query_json.location == self.location assert inline_query_json.query == self.query assert inline_query_json.offset == self.offset def test_to_dict(self, inline_query): inline_query_dict = inline_query.to_dict() assert isinstance(inline_query_dict, dict) assert inline_query_dict['id'] == inline_query.id assert inline_query_dict['from'] == inline_query.from_user.to_dict() assert inline_query_dict['location'] == inline_query.location.to_dict() assert inline_query_dict['query'] == inline_query.query assert inline_query_dict['offset'] == inline_query.offset def test_answer(self, monkeypatch, inline_query): def test(*args, **kwargs): return args[0] == inline_query.id monkeypatch.setattr(inline_query.bot, 'answer_inline_query', test) assert inline_query.answer() 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-12.4.2/tests/test_inlinequeryhandler.py000066400000000000000000000205141362023133600241570ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, User(1, '', False), None, Chat(1, ''), 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 TestCallbackQueryHandler(object): test_flag = False @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 python-telegram-bot-12.4.2/tests/test_inlinequeryresultarticle.py000066400000000000000000000117471362023133600254340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): 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_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-12.4.2/tests/test_inlinequeryresultaudio.py000066400000000000000000000110751362023133600251040ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, input_message_content=TestInlineQueryResultAudio.input_message_content, reply_markup=TestInlineQueryResultAudio.reply_markup) class TestInlineQueryResultAudio(object): id = 'id' type = 'audio' audio_url = 'audio url' title = 'title' performer = 'performer' audio_duration = 'audio_duration' caption = 'caption' parse_mode = 'Markdown' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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['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-12.4.2/tests/test_inlinequeryresultcachedaudio.py000066400000000000000000000102411362023133600262260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, input_message_content=TestInlineQueryResultCachedAudio.input_message_content, reply_markup=TestInlineQueryResultCachedAudio.reply_markup) class TestInlineQueryResultCachedAudio(object): id = 'id' type = 'audio' audio_file_id = 'audio file id' caption = 'caption' parse_mode = 'HTML' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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['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-12.4.2/tests/test_inlinequeryresultcacheddocument.py000066400000000000000000000117221362023133600267500ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, description=TestInlineQueryResultCachedDocument.description, input_message_content=TestInlineQueryResultCachedDocument.input_message_content, reply_markup=TestInlineQueryResultCachedDocument.reply_markup) class TestInlineQueryResultCachedDocument(object): id = 'id' type = 'document' document_file_id = 'document file id' title = 'title' caption = 'caption' parse_mode = 'Markdown' description = 'description' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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['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-12.4.2/tests/test_inlinequeryresultcachedgif.py000066400000000000000000000104051362023133600256740ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, input_message_content=TestInlineQueryResultCachedGif.input_message_content, reply_markup=TestInlineQueryResultCachedGif.reply_markup) class TestInlineQueryResultCachedGif(object): id = 'id' type = 'gif' gif_file_id = 'gif file id' title = 'title' caption = 'caption' parse_mode = 'HTML' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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['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-12.4.2/tests/test_inlinequeryresultcachedmpeg4gif.py000066400000000000000000000111451362023133600266330ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, input_message_content=TestInlineQueryResultCachedMpeg4Gif.input_message_content, reply_markup=TestInlineQueryResultCachedMpeg4Gif.reply_markup) class TestInlineQueryResultCachedMpeg4Gif(object): id = 'id' type = 'mpeg4_gif' mpeg4_file_id = 'mpeg4 file id' title = 'title' caption = 'caption' parse_mode = 'Markdown' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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['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-12.4.2/tests/test_inlinequeryresultcachedphoto.py000066400000000000000000000113411362023133600262600ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, input_message_content=TestInlineQueryResultCachedPhoto.input_message_content, reply_markup=TestInlineQueryResultCachedPhoto.reply_markup) class TestInlineQueryResultCachedPhoto(object): id = 'id' type = 'photo' photo_file_id = 'photo file id' title = 'title' description = 'description' caption = 'caption' parse_mode = 'HTML' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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['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-12.4.2/tests/test_inlinequeryresultcachedsticker.py000066400000000000000000000073351362023133600266030ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): 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_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-12.4.2/tests/test_inlinequeryresultcachedvideo.py000066400000000000000000000114171362023133600262410ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, description=TestInlineQueryResultCachedVideo.description, input_message_content=TestInlineQueryResultCachedVideo.input_message_content, reply_markup=TestInlineQueryResultCachedVideo.reply_markup) class TestInlineQueryResultCachedVideo(object): id = 'id' type = 'video' video_file_id = 'video file id' title = 'title' caption = 'caption' parse_mode = 'Markdown' description = 'description' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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['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-12.4.2/tests/test_inlinequeryresultcachedvoice.py000066400000000000000000000107231362023133600262370ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, input_message_content=TestInlineQueryResultCachedVoice.input_message_content, reply_markup=TestInlineQueryResultCachedVoice.reply_markup) class TestInlineQueryResultCachedVoice(object): id = 'id' type = 'voice' voice_file_id = 'voice file id' title = 'title' caption = 'caption' parse_mode = 'HTML' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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['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-12.4.2/tests/test_inlinequeryresultcontact.py000066400000000000000000000115151362023133600254350ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): 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_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-12.4.2/tests/test_inlinequeryresultdocument.py000066400000000000000000000133721362023133600256230ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, 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(object): id = 'id' type = 'document' document_url = 'document url' title = 'title' caption = 'caption' parse_mode = 'Markdown' 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_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.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['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-12.4.2/tests/test_inlinequeryresultgame.py000066400000000000000000000056751362023133600247250ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): id = 'id' type = 'game' game_short_name = 'game short name' reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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-12.4.2/tests/test_inlinequeryresultgif.py000066400000000000000000000115671362023133600245560ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, input_message_content=TestInlineQueryResultGif.input_message_content, reply_markup=TestInlineQueryResultGif.reply_markup) class TestInlineQueryResultGif(object): id = 'id' type = 'gif' gif_url = 'gif url' gif_width = 10 gif_height = 15 gif_duration = 1 thumb_url = 'thumb url' title = 'title' caption = 'caption' parse_mode = 'HTML' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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.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['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['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-12.4.2/tests/test_inlinequeryresultlocation.py000066400000000000000000000121241362023133600256070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) class TestInlineQueryResultLocation(object): id = 'id' type = 'location' latitude = 0.0 longitude = 1.0 title = 'title' live_period = 70 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_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() 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()) 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-12.4.2/tests/test_inlinequeryresultmpeg4gif.py000066400000000000000000000126231362023133600255050ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, input_message_content=TestInlineQueryResultMpeg4Gif.input_message_content, reply_markup=TestInlineQueryResultMpeg4Gif.reply_markup) class TestInlineQueryResultMpeg4Gif(object): id = 'id' type = 'mpeg4_gif' mpeg4_url = 'mpeg4 url' mpeg4_width = 10 mpeg4_height = 15 mpeg4_duration = 1 thumb_url = 'thumb url' title = 'title' caption = 'caption' parse_mode = 'Markdown' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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.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['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['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-12.4.2/tests/test_inlinequeryresultphoto.py000066400000000000000000000121341362023133600251310ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, input_message_content=TestInlineQueryResultPhoto.input_message_content, reply_markup=TestInlineQueryResultPhoto.reply_markup) class TestInlineQueryResultPhoto(object): 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' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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['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-12.4.2/tests/test_inlinequeryresultvenue.py000066400000000000000000000131371362023133600251260ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) class TestInlineQueryResultVenue(object): id = 'id' type = 'venue' latitude = 'latitude' longitude = 'longitude' title = 'title' address = 'address' foursquare_id = 'foursquare id' foursquare_type = 'foursquare 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_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.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['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-12.4.2/tests/test_inlinequeryresultvideo.py000066400000000000000000000135561362023133600251170ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, description=TestInlineQueryResultVideo.description, input_message_content=TestInlineQueryResultVideo.input_message_content, reply_markup=TestInlineQueryResultVideo.reply_markup) class TestInlineQueryResultVideo(object): 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' description = 'description' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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['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-12.4.2/tests/test_inlinequeryresultvoice.py000066400000000000000000000106031362023133600251040ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) @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, input_message_content=TestInlineQueryResultVoice.input_message_content, reply_markup=TestInlineQueryResultVoice.reply_markup) class TestInlineQueryResultVoice(object): id = 'id' type = 'voice' voice_url = 'voice url' title = 'title' voice_duration = 'voice_duration' caption = 'caption' parse_mode = 'HTML' input_message_content = InputTextMessageContent('input_message_content') reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton('reply_markup')]]) 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.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['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-12.4.2/tests/test_inputcontactmessagecontent.py000066400000000000000000000042621362023133600257320ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @pytest.fixture(scope='class') def input_contact_message_content(): return InputContactMessageContent(TestInputContactMessageContent.phone_number, TestInputContactMessageContent.first_name, last_name=TestInputContactMessageContent.last_name) class TestInputContactMessageContent(object): phone_number = 'phone number' first_name = 'first name' last_name = 'last name' 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) python-telegram-bot-12.4.2/tests/test_inputfile.py000066400000000000000000000101641362023133600222540ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 subprocess import sys from io import BytesIO from telegram import InputFile class TestInputFile(object): png = os.path.join('tests', 'data', 'game.png') 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): # Only test a few to make sure logic works okay assert InputFile(open('tests/data/telegram.jpg', 'rb')).mimetype == 'image/jpeg' if sys.version_info >= (3, 5): 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' 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(object): # 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' python-telegram-bot-12.4.2/tests/test_inputlocationmessagecontent.py000066400000000000000000000042521362023133600261060ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @pytest.fixture(scope='class') def input_location_message_content(): return InputLocationMessageContent(TestInputLocationMessageContent.latitude, TestInputLocationMessageContent.longitude, live_period=TestInputLocationMessageContent.live_period) class TestInputLocationMessageContent(object): latitude = -23.691288 longitude = -46.788279 live_period = 80 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 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) python-telegram-bot-12.4.2/tests/test_inputmedia.py000066400000000000000000000404321362023133600224150ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import pytest from flaky import flaky from telegram import (InputMediaVideo, InputMediaPhoto, InputMediaAnimation, Message, InputFile, InputMediaAudio, InputMediaDocument) # noinspection PyUnresolvedReferences 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 @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, 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) @pytest.fixture(scope='class') def input_media_animation(class_thumb_file): return InputMediaAnimation(media=TestInputMediaAnimation.media, caption=TestInputMediaAnimation.caption, parse_mode=TestInputMediaAnimation.parse_mode, 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) @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) class TestInputMediaVideo(object): type = "video" media = "NOTAREALFILEID" caption = "My Caption" width = 3 height = 4 duration = 5 parse_mode = 'HTML' supports_streaming = True 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.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['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" class TestInputMediaPhoto(object): type = "photo" media = "NOTAREALFILEID" caption = "My Caption" parse_mode = 'Markdown' 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 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 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" class TestInputMediaAnimation(object): type = "animation" media = "NOTAREALFILEID" caption = "My Caption" parse_mode = 'Markdown' width = 30 height = 30 duration = 1 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 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['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" class TestInputMediaAudio(object): type = "audio" media = "NOTAREALFILEID" caption = "My Caption" duration = 3 performer = 'performer' title = 'title' parse_mode = 'HTML' 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 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 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" class TestInputMediaDocument(object): type = "document" media = "NOTAREALFILEID" caption = "My Caption" parse_mode = 'HTML' 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 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 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" @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')] class TestSendMediaGroup(object): @flaky(3, 1) @pytest.mark.timeout(10) 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) == 2 assert all([isinstance(mes, Message) for mes in messages]) assert all([mes.media_group_id == messages[0].media_group_id for mes in messages]) @flaky(3, 1) @pytest.mark.timeout(10) 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) assert isinstance(messages, list) assert len(messages) == 2 assert all([isinstance(mes, Message) for mes in messages]) assert all([mes.media_group_id == messages[0].media_group_id for mes in messages]) @flaky(3, 1) # noqa: F811 @pytest.mark.timeout(10) # noqa: F811 def test_send_media_group_new_files(self, bot, chat_id, video_file, photo_file, # noqa: F811 animation_file): # noqa: F811 messages = bot.send_media_group(chat_id, [ InputMediaVideo(video_file), InputMediaPhoto(photo_file) ]) assert isinstance(messages, list) assert len(messages) == 2 assert all([isinstance(mes, Message) for mes in messages]) assert all([mes.media_group_id == messages[0].media_group_id for mes in messages]) @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.parametrize('default_bot', [{'quote': True}], indirect=True) def test_send_media_group_default_quote(self, default_bot, chat_id, media_group): messages = default_bot.send_media_group(chat_id, media_group) assert all([mes.default_quote is True for mes in messages]) @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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) python-telegram-bot-12.4.2/tests/test_inputtextmessagecontent.py000066400000000000000000000043001362023133600252540ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @pytest.fixture(scope='class') def input_text_message_content(): return InputTextMessageContent( TestInputTextMessageContent.message_text, parse_mode=TestInputTextMessageContent.parse_mode, disable_web_page_preview=TestInputTextMessageContent.disable_web_page_preview) class TestInputTextMessageContent(object): message_text = '*message text*' parse_mode = ParseMode.MARKDOWN disable_web_page_preview = True 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 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['disable_web_page_preview'] == input_text_message_content.disable_web_page_preview) python-telegram-bot-12.4.2/tests/test_inputvenuemessagecontent.py000066400000000000000000000057211362023133600254220ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @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) class TestInputVenueMessageContent(object): latitude = 1. longitude = 2. title = 'title' address = 'address' foursquare_id = 'foursquare id' foursquare_type = 'foursquare type' 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 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) python-telegram-bot-12.4.2/tests/test_invoice.py000066400000000000000000000116561362023133600217200ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @pytest.fixture(scope='class') def invoice(): return Invoice(TestInvoice.title, TestInvoice.description, TestInvoice.start_parameter, TestInvoice.currency, TestInvoice.total_amount) class TestInvoice(object): 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]) 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) @pytest.mark.timeout(10) def test_send_required_args_only(self, bot, chat_id, provider_token): message = bot.send_invoice(chat_id, self.title, self.description, self.payload, provider_token, self.start_parameter, self.currency, self.prices) 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 @flaky(3, 1) @pytest.mark.timeout(10) def test_send_all_args(self, bot, chat_id, provider_token): message = bot.send_invoice( chat_id, self.title, self.description, self.payload, provider_token, self.start_parameter, self.currency, self.prices, 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) 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 def test_send_object_as_provider_data(self, monkeypatch, bot, chat_id, provider_token): def test(_, url, data, **kwargs): return (data['provider_data'] == '{"test_data": 123456789}' # Depends if using or data['provider_data'] == '{"test_data":123456789}') # ujson or not monkeypatch.setattr('telegram.utils.request.Request.post', test) assert bot.send_invoice(chat_id, self.title, self.description, self.payload, provider_token, self.start_parameter, self.currency, self.prices, provider_data={'test_data': 123456789}) python-telegram-bot-12.4.2/tests/test_jobqueue.py000066400000000000000000000303101362023133600220670ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 os import sys import time from queue import Queue from time import sleep import pytest from flaky import flaky from telegram.ext import JobQueue, Updater, Job, CallbackContext from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import _UtcOffsetTimezone @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('APPVEYOR'), reason="On Appveyor precise timings are not accurate.") @pytest.mark.skipif(os.getenv('GITHUB_ACTIONS', False) and os.name == 'nt', reason="On windows precise timings are not accurate.") @flaky(10, 1) # Timings aren't quite perfect class TestJobQueue(object): result = 0 job_time = 0 @pytest.fixture(autouse=True) def reset(self): self.result = 0 self.job_time = 0 def job_run_once(self, bot, job): self.result += 1 def job_with_exception(self, bot, job): 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 context.job.job_queue): self.result += 1 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. Set the target datetime to utcnow + x hours (naive) with the timezone set to utc + x hours, which is equivalent to now. """ # 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.utcnow() + timezone.utcoffset(None)).replace(tzinfo=timezone) job_queue.run_once(self.job_run_once, when) sleep(0.001) assert self.result == 1 def test_run_once_no_time_spec(self, job_queue): # test that an appropiate exception is raised if a job is attempted to be scheduled # without specifying a time with pytest.raises(ValueError): job_queue.run_once(self.job_run_once, when=None) 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_immediate(self, job_queue): job_queue.run_repeating(self.job_run_once, 0.1, first=0) sleep(0.05) 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``""" first = (dtm.datetime.utcnow() + timezone.utcoffset(None)).replace(tzinfo=timezone) job_queue.run_repeating(self.job_run_once, 0.05, first=first) sleep(0.001) assert self.result == 1 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) 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: u.stop() 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), time.time() when = dtm.datetime.utcfromtimestamp(now) + delta expected_time = now + delta.total_seconds() 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, time.time() when = (dtm.datetime.utcfromtimestamp(now) + dtm.timedelta(seconds=delta)).time() expected_time = now + delta 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, time.time() when = (dtm.datetime.utcfromtimestamp(now) + dtm.timedelta(seconds=delta)).time() expected_time = now + delta + 60 * 60 * 24 job_queue.run_once(self.job_datetime_tests, when) assert job_queue._queue.get(False)[0] == pytest.approx(expected_time) def test_run_daily(self, job_queue): delta, now = 0.1, time.time() time_of_day = (dtm.datetime.utcfromtimestamp(now) + dtm.timedelta(seconds=delta)).time() expected_reschedule_time = now + delta + 24 * 60 * 60 job_queue.run_daily(self.job_run_once, time_of_day) sleep(0.2) assert self.result == 1 assert job_queue._queue.get(False)[0] == pytest.approx(expected_reschedule_time) def test_run_daily_with_timezone(self, job_queue): """test that the weekday is retrieved based on the job's timezone We set a job to run at the current UTC time of day (plus a small delay buffer) with a timezone that is---approximately (see below)---UTC +24, and set it to run on the weekday after the current UTC weekday. The job should therefore be executed now (because in UTC+24, the time of day is the same as the current weekday is the one after the current UTC weekday). """ now = time.time() utcnow = dtm.datetime.utcfromtimestamp(now) delta = 0.1 # must subtract one minute because the UTC offset has to be strictly less than 24h # thus this test will xpass if run in the interval [00:00, 00:01) UTC time # (because target time will be 23:59 UTC, so local and target weekday will be the same) target_tzinfo = _UtcOffsetTimezone(dtm.timedelta(days=1, minutes=-1)) target_datetime = (utcnow + dtm.timedelta(days=1, minutes=-1, seconds=delta)).replace( tzinfo=target_tzinfo) target_time = target_datetime.timetz() target_weekday = target_datetime.date().weekday() expected_reschedule_time = now + delta + 24 * 60 * 60 job_queue.run_daily(self.job_run_once, time=target_time, days=(target_weekday,)) sleep(delta + 0.1) assert self.result == 1 assert job_queue._queue.get(False)[0] == pytest.approx(expected_reschedule_time) def test_warnings(self, job_queue): j = Job(self.job_run_once, repeat=False) with pytest.raises(ValueError, match='can not be set to'): j.repeat = True j.interval = 15 assert j.interval_seconds == 15 j.repeat = True with pytest.raises(ValueError, match='can not be'): j.interval = None j.repeat = False with pytest.raises(ValueError, match='must be of type'): j.interval = 'every 3 minutes' j.interval = 15 assert j.interval_seconds == 15 with pytest.raises(ValueError, match='argument should be of type'): j.days = 'every day' with pytest.raises(ValueError, match='The elements of the'): j.days = ('mon', 'wed') with pytest.raises(ValueError, match='from 0 up to and'): j.days = (0, 6, 12, 14) def test_get_jobs(self, job_queue): job1 = job_queue.run_once(self.job_run_once, 10, name='name1') job2 = job_queue.run_once(self.job_run_once, 10, name='name1') job3 = job_queue.run_once(self.job_run_once, 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,) @pytest.mark.skipif(sys.version_info < (3, 0), reason='pytest fails this for no reason') def test_bot_in_init_deprecation(self, bot): with pytest.warns(TelegramDeprecationWarning): JobQueue(bot) def test_context_based_callback(self, job_queue): job_queue.run_once(self.job_context_based_callback, 0.01, context=2) sleep(0.03) assert self.result == 0 python-telegram-bot-12.4.2/tests/test_keyboardbutton.py000066400000000000000000000035421362023133600233130ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @pytest.fixture(scope='class') def keyboard_button(): return KeyboardButton(TestKeyboardButton.text, request_location=TestKeyboardButton.request_location, request_contact=TestKeyboardButton.request_contact) class TestKeyboardButton(object): text = 'text' request_location = True request_contact = True 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 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 python-telegram-bot-12.4.2/tests/test_labeledprice.py000066400000000000000000000027251362023133600226740ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @pytest.fixture(scope='class') def labeled_price(): return LabeledPrice(TestLabeledPrice.label, TestLabeledPrice.amount) class TestLabeledPrice(object): label = 'label' amount = 100 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 python-telegram-bot-12.4.2/tests/test_location.py000066400000000000000000000130141362023133600220620ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) class TestLocation(object): latitude = -23.691288 longitude = -46.788279 def test_de_json(self, bot): json_dict = {'latitude': TestLocation.latitude, 'longitude': TestLocation.longitude} location = Location.de_json(json_dict, bot) assert location.latitude == self.latitude assert location.longitude == self.longitude @flaky(3, 1) @pytest.mark.xfail @pytest.mark.timeout(10) 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) assert message.location assert message.location.latitude == 52.223880 assert message.location.longitude == 5.166146 message2 = bot.edit_message_live_location(message.chat_id, message.message_id, latitude=52.223098, longitude=5.164306) assert message2.location.latitude == 52.223098 assert message2.location.longitude == 5.164306 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 test(_, url, data, **kwargs): lat = data['latitude'] == location.latitude lon = data['longitude'] == location.longitude id = data['inline_message_id'] == 1234 return lat and lon and id monkeypatch.setattr('telegram.utils.request.Request.post', test) assert bot.edit_message_live_location(inline_message_id=1234, location=location) # 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('telegram.utils.request.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('telegram.utils.request.Request.post', test) assert bot.send_location(location=location, chat_id=chat_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('telegram.utils.request.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 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-12.4.2/tests/test_loginurl.py000066400000000000000000000042721362023133600221130ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): url = "http://www.google.com" forward_text = "Send me forward!" bot_username = "botname" request_write_access = True 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-12.4.2/tests/test_message.py000066400000000000000000001016651362023133600217100ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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) from tests.test_passport import RAW_PASSPORT_DATA @pytest.fixture(scope='class') def message(bot): return Message(TestMessage.id, TestMessage.from_user, TestMessage.date, TestMessage.chat, 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', 12), 'caption': 'audio_file'}, {'document': Document('document_id'), 'caption': 'document_file'}, {'animation': Animation('animation_id', 30, 30, 1), 'caption': 'animation_file'}, {'game': Game('my_game', 'just my game', [PhotoSize('game_photo_id', 30, 30), ])}, {'photo': [PhotoSize('photo_id', 50, 50)], 'caption': 'photo_file'}, {'sticker': Sticker('sticker_id', 50, 50, True)}, {'video': Video('video_id', 12, 12, 12), 'caption': 'video_file'}, {'voice': Voice('voice_id', 5)}, {'video_note': VideoNote('video_note_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', 50, 50)]}, {'delete_chat_photo': True}, {'group_chat_created': True}, {'supergroup_chat_created': True}, {'channel_chat_created': True}, {'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', 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)}, {'text': 'a text message', 'reply_markup': {'inline_keyboard': [[{ 'text': 'start', 'url': 'http://google.com'}, { 'text': 'next', 'callback_data': 'abcd'}], [{'text': 'Cancel', 'callback_data': 'Cancel'}]]}}, {'quote': 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', 'migrated_to', 'migrated_from', 'pinned', 'invoice', 'successful_payment', 'connected_website', 'forward_signature', 'author_signature', 'photo_from_media_group', 'passport_data', 'poll', 'reply_markup', 'default_quote']) 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(object): id = 1 from_user = User(2, 'testuser', False) date = datetime.utcnow() chat = Chat(3, 'private') test_entities = [{'length': 4, 'offset': 10, 'type': 'bold'}, {'length': 7, 'offset': 16, 'type': 'italic'}, {'length': 4, 'offset': 25, 'type': 'code'}, {'length': 5, 'offset': 31, 'type': 'text_link', 'url': 'http://github.com/'}, {'length': 12, 'offset': 38, 'type': 'text_mention', 'user': User(123456789, 'mentioned user', False)}, {'length': 3, 'offset': 55, 'type': 'pre'}, {'length': 17, 'offset': 60, 'type': 'url'}] test_text = 'Test for bold, ita_lic, code, ' 'links, ' 'text-mention and ' '

pre
. http://google.com') text_html = self.test_message.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, code, ' 'links, ' 'text-mention and ' '
pre
. http://google.com') text_html = self.test_message.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/), ' '[text-mention](tg://user?id=123456789) and ```pre```. ' 'http://google.com') text_markdown = self.test_message.text_markdown assert text_markdown == test_md_string def test_text_markdown_empty(self, message): message.text = None message.caption = "test" assert message.text_markdown is None def test_text_markdown_urled(self): test_md_string = (r'Test for <*bold*, _ita\_lic_, `code`, [links](http://github.com/), ' '[text-mention](tg://user?id=123456789) and ```pre```. ' '[http://google.com](http://google.com)') text_markdown = self.test_message.text_markdown_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, code, ' 'links, ' 'text-mention and ' '
pre
. http://google.com') caption_html = self.test_message.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, code, ' 'links, ' 'text-mention and ' '
pre
. http://google.com') caption_html = self.test_message.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/), ' '[text-mention](tg://user?id=123456789) and ```pre```. ' 'http://google.com') caption_markdown = self.test_message.caption_markdown 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 def test_caption_markdown_urled(self): test_md_string = (r'Test for <*bold*, _ita\_lic_, `code`, [links](http://github.com/), ' '[text-mention](tg://user?id=123456789) and ```pre```. ' '[http://google.com](http://google.com)') caption_markdown = self.test_message.caption_markdown_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 == 'https://t.me/{}/{}'.format(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 == 'https://t.me/c/{}/{}'.format(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', '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 test(*args, **kwargs): id = args[0] == message.chat_id text = args[1] == 'test' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and text and reply monkeypatch.setattr(message.bot, 'send_message', test) 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/), ' '[text-mention](tg://user?id=123456789) and ```pre```. ' 'http://google.com') def test(*args, **kwargs): cid = args[0] == message.chat_id markdown_text = args[1] == test_md_string markdown_enabled = kwargs['parse_mode'] == ParseMode.MARKDOWN if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return all([cid, markdown_text, reply, markdown_enabled]) text_markdown = self.test_message.text_markdown assert text_markdown == test_md_string monkeypatch.setattr(message.bot, 'send_message', test) 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_html(self, monkeypatch, message): test_html_string = ('Test for <bold, ita_lic, code, ' 'links, ' 'text-mention and ' '
pre
. http://google.com') def test(*args, **kwargs): cid = args[0] == message.chat_id html_text = args[1] == test_html_string html_enabled = kwargs['parse_mode'] == ParseMode.HTML if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return all([cid, html_text, reply, html_enabled]) text_html = self.test_message.text_html assert text_html == test_html_string monkeypatch.setattr(message.bot, 'send_message', test) assert message.reply_html(self.test_message.text_html) assert message.reply_html(self.test_message.text_html, quote=True) assert message.reply_html(self.test_message.text_html, reply_to_message_id=message.message_id, quote=True) def test_reply_media_group(self, monkeypatch, message): def test(*args, **kwargs): id = args[0] == message.chat_id media = kwargs['media'] == 'reply_media_group' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and media and reply monkeypatch.setattr(message.bot, 'send_media_group', test) 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 test(*args, **kwargs): id = args[0] == message.chat_id photo = kwargs['photo'] == 'test_photo' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and photo and reply monkeypatch.setattr(message.bot, 'send_photo', test) assert message.reply_photo(photo='test_photo') assert message.reply_photo(photo='test_photo', quote=True) def test_reply_audio(self, monkeypatch, message): def test(*args, **kwargs): id = args[0] == message.chat_id audio = kwargs['audio'] == 'test_audio' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and audio and reply monkeypatch.setattr(message.bot, 'send_audio', test) assert message.reply_audio(audio='test_audio') assert message.reply_audio(audio='test_audio', quote=True) def test_reply_document(self, monkeypatch, message): def test(*args, **kwargs): id = args[0] == message.chat_id document = kwargs['document'] == 'test_document' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and document and reply monkeypatch.setattr(message.bot, 'send_document', test) assert message.reply_document(document='test_document') assert message.reply_document(document='test_document', quote=True) def test_reply_animation(self, monkeypatch, message): def test(*args, **kwargs): id = args[0] == message.chat_id animation = kwargs['animation'] == 'test_animation' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and animation and reply monkeypatch.setattr(message.bot, 'send_animation', test) assert message.reply_animation(animation='test_animation') assert message.reply_animation(animation='test_animation', quote=True) def test_reply_sticker(self, monkeypatch, message): def test(*args, **kwargs): id = args[0] == message.chat_id sticker = kwargs['sticker'] == 'test_sticker' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and sticker and reply monkeypatch.setattr(message.bot, 'send_sticker', test) assert message.reply_sticker(sticker='test_sticker') assert message.reply_sticker(sticker='test_sticker', quote=True) def test_reply_video(self, monkeypatch, message): def test(*args, **kwargs): id = args[0] == message.chat_id video = kwargs['video'] == 'test_video' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and video and reply monkeypatch.setattr(message.bot, 'send_video', test) 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 test(*args, **kwargs): id = args[0] == message.chat_id video_note = kwargs['video_note'] == 'test_video_note' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and video_note and reply monkeypatch.setattr(message.bot, 'send_video_note', test) 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 test(*args, **kwargs): id = args[0] == message.chat_id voice = kwargs['voice'] == 'test_voice' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and voice and reply monkeypatch.setattr(message.bot, 'send_voice', test) assert message.reply_voice(voice='test_voice') assert message.reply_voice(voice='test_voice', quote=True) def test_reply_location(self, monkeypatch, message): def test(*args, **kwargs): id = args[0] == message.chat_id location = kwargs['location'] == 'test_location' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and location and reply monkeypatch.setattr(message.bot, 'send_location', test) assert message.reply_location(location='test_location') assert message.reply_location(location='test_location', quote=True) def test_reply_venue(self, monkeypatch, message): def test(*args, **kwargs): id = args[0] == message.chat_id venue = kwargs['venue'] == 'test_venue' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and venue and reply monkeypatch.setattr(message.bot, 'send_venue', test) assert message.reply_venue(venue='test_venue') assert message.reply_venue(venue='test_venue', quote=True) def test_reply_contact(self, monkeypatch, message): def test(*args, **kwargs): id = args[0] == message.chat_id contact = kwargs['contact'] == 'test_contact' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and contact and reply monkeypatch.setattr(message.bot, 'send_contact', test) assert message.reply_contact(contact='test_contact') assert message.reply_contact(contact='test_contact', quote=True) def test_reply_poll(self, monkeypatch, message): def test(*args, **kwargs): id = args[0] == message.chat_id contact = kwargs['contact'] == 'test_poll' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: reply = True return id and contact and reply monkeypatch.setattr(message.bot, 'send_poll', test) assert message.reply_poll(contact='test_poll') assert message.reply_poll(contact='test_poll', quote=True) def test_forward(self, monkeypatch, message): def test(*args, **kwargs): chat_id = kwargs['chat_id'] == 123456 from_chat = kwargs['from_chat_id'] == message.chat_id message_id = kwargs['message_id'] == message.message_id if kwargs.get('disable_notification'): notification = kwargs['disable_notification'] is True else: notification = True return chat_id and from_chat and message_id and notification monkeypatch.setattr(message.bot, 'forward_message', test) assert message.forward(123456) assert message.forward(123456, disable_notification=True) assert not message.forward(635241) def test_edit_text(self, monkeypatch, message): def test(*args, **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 monkeypatch.setattr(message.bot, 'edit_message_text', test) assert message.edit_text(text='test') def test_edit_caption(self, monkeypatch, message): def test(*args, **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 monkeypatch.setattr(message.bot, 'edit_message_caption', test) assert message.edit_caption(caption='new caption') def test_edit_media(self, monkeypatch, message): def test(*args, **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 monkeypatch.setattr(message.bot, 'edit_message_media', test) assert message.edit_media('my_media') def test_edit_reply_markup(self, monkeypatch, message): def test(*args, **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 monkeypatch.setattr(message.bot, 'edit_message_reply_markup', test) assert message.edit_reply_markup(reply_markup=[['1', '2']]) def test_delete(self, monkeypatch, message): def test(*args, **kwargs): chat_id = kwargs['chat_id'] == message.chat_id message_id = kwargs['message_id'] == message.message_id return chat_id and message_id monkeypatch.setattr(message.bot, 'delete_message', test) assert message.delete() def test_default_quote(self, message): kwargs = {} message.default_quote = False message._quote(kwargs) assert 'reply_to_message_id' not in kwargs message.default_quote = True message._quote(kwargs) assert 'reply_to_message_id' in kwargs kwargs = {} message.default_quote = None message.chat.type = Chat.PRIVATE message._quote(kwargs) assert 'reply_to_message_id' not in kwargs message.chat.type = Chat.GROUP message._quote(kwargs) assert 'reply_to_message_id' in kwargs def test_equality(self): id = 1 a = Message(id, self.from_user, self.date, self.chat) b = Message(id, self.from_user, self.date, self.chat) c = Message(id, User(0, '', False), self.date, self.chat) d = Message(0, self.from_user, self.date, self.chat) 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-12.4.2/tests/test_messageentity.py000066400000000000000000000051121362023133600231330ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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) return MessageEntity(type, 1, 3, url=url, user=user) class TestMessageEntity(object): type = 'url' offset = 1 length = 2 url = 'url' 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() 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-12.4.2/tests/test_messagehandler.py000066400000000000000000000265451362023133600232510ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 message = Message(1, User(1, '', False), None, Chat(1, ''), 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, User(1, '', False), None, Chat(1, ''), bot=bot) class TestMessageHandler(object): test_flag = False SRE_TYPE = type(re.match("", "")) @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) == 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) == 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_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-12.4.2/tests/test_messagequeue.py000066400000000000000000000046151362023133600227520ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 import pytest import telegram.ext.messagequeue as mq @pytest.mark.skipif(os.getenv('APPVEYOR'), reason="On Appveyor precise timings are not accurate.") @pytest.mark.skipif(os.getenv('GITHUB_ACTIONS', False) and os.name == 'nt', reason="On windows precise timings are not accurate.") class TestDelayQueue(object): N = 128 burst_limit = 30 time_limit_ms = 1000 margin_ms = 0 testtimes = [] def call(self): self.testtimes.append(mq.curtime()) 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 i in range(self.N): dsp(self.call) starttime = mq.curtime() # 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 mq.curtime() < 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 fails == [] python-telegram-bot-12.4.2/tests/test_meta.py000066400000000000000000000027451362023133600212110ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 def call_pre_commit_hook(hook_id): __tracebackhide__ = True return os.system(' '.join(['pre-commit', 'run', '--all-files', hook_id])) # pragma: no cover @pytest.mark.nocoverage @pytest.mark.parametrize('hook_id', argvalues=('yapf', 'flake8', 'pylint')) @pytest.mark.skipif(not os.getenv('TEST_PRE_COMMIT', False), reason='TEST_PRE_COMMIT not enabled') def test_pre_commit_hook(hook_id): assert call_pre_commit_hook(hook_id) == 0 # pragma: no cover @pytest.mark.nocoverage @pytest.mark.skipif(not 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 python-telegram-bot-12.4.2/tests/test_official.py000066400000000000000000000122551362023133600220340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 IGNORED_OBJECTS = ('ResponseParameters', 'CallbackGame') IGNORED_PARAMETERS = {'self', 'args', 'kwargs', 'read_latency', 'network_delay', 'timeout', 'bot'} 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, "Parameter {} not found in {}".format(parameter[0], 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 == 'sendDocument': ignored |= {'filename'} # Undocumented 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 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')) and field == 'type'): continue elif name.startswith('PassportElementError') and field == 'source': continue elif field == 'remove_keyboard': continue param = sig.parameters.get(field) assert param is not None, "Attribute {} not found in {}".format(field, obj.__name__) # TODO: Check type via docstring # TODO: Check if optional or required checked.append(field) ignored = IGNORED_PARAMETERS.copy() if name == 'InputFile': return elif name == 'InlineQueryResult': ignored |= {'id', 'type'} elif name == 'User': ignored |= {'type'} # TODO: Deprecation elif name in ('PassportFile', 'EncryptedPassportElement'): ignored |= {'credentials'} elif name == 'PassportElementError': ignored |= {'message', 'type', 'source'} 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(os.getenv('TEST_OFFICIAL') != 'true', reason='test_official is not enabled') def test_official(method, data): method(data) python-telegram-bot-12.4.2/tests/test_orderinfo.py000066400000000000000000000042731362023133600222500ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): name = 'name' phone_number = 'phone_number' email = 'email' shipping_address = ShippingAddress('GB', '', 'London', '12 Grimmauld Place', '', 'WC1') 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() python-telegram-bot-12.4.2/tests/test_parsemode.py000066400000000000000000000034651362023133600222420ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 ParseMode class TestParseMode(object): markdown_text = '*bold* _italic_ [link](http://google.com) [name](tg://user?id=123456789).' html_text = ('bold italic link ' 'name.') formatted_text_formatted = u'bold italic link name.' @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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-12.4.2/tests/test_passport.py000066400000000000000000000565501362023133600221410ustar00rootroot00000000000000#!/usr/bin/env python # flake8: noqa: E501 # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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) # Generated using the scope: # { # data: [ # { # type: 'personal_details', # native_names: true # }, # { # type: 'id_document', # selfie: true, # translation: true # }, # { # type: 'address_document', # translation: true # }, # 'address', # 'email' # ], # v: 1 # } 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'}, 'translation': [{'file_size': 28640, 'file_date': 1535630933, 'file_id': 'DgADBAADswMAAisqQVAmooP-kVgLgAI'}, {'file_size': 28672, 'file_date': 1535630933, 'file_id': 'DgADBAAD1QMAAnrpQFBMZsT3HysjwwI'}], 'front_side': {'file_size': 28624, 'file_date': 1534074942, 'file_id': 'DgADBAADxwMAApnQgVPK2-ckL2eXVAI'}, 'type': 'driver_license', 'selfie': {'file_size': 28592, 'file_date': 1534074942, 'file_id': 'DgADBAADEQQAAkopgFNr6oi-wISRtAI'}, 'data': 'eJUOFuY53QKmGqmBgVWlLBAQCUQJ79n405SX6M5aGFIIodOPQqnLYvMNqTwTrXGDlW+mVLZcbu+y8luLVO8WsJB/0SB7q5WaXn/IMt1G9lz5G/KMLIZG/x9zlnimsaQLg7u8srG6L4KZzv+xkbbHjZdETrxU8j0N/DoS4HvLMRSJAgeFUrY6v2YW9vSRg+fSxIqQy1jR2VKpzAT8OhOz7A==' }, { 'translation': [{'file_size': 28480, 'file_date': 1535630939, 'file_id': 'DgADBAADyQUAAqyqQVC_eoX_KwNjJwI'}, {'file_size': 28528, 'file_date': 1535630939, 'file_id': 'DgADBAADsQQAAubTQVDRO_FN3lOwWwI'}], 'files': [{'file_size': 28640, 'file_date': 1534074988, 'file_id': 'DgADBAADLAMAAhwfgVMyfGa5Nr0LvAI'}, {'file_size': 28480, 'file_date': 1534074988, 'file_id': 'DgADBAADaQQAAsFxgVNVfLZuT-_3ZQI'}], '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(object): driver_license_selfie_file_id = 'DgADBAADEQQAAkopgFNr6oi-wISRtAI' driver_license_front_side_file_id = 'DgADBAADxwMAApnQgVPK2-ckL2eXVAI' driver_license_reverse_side_file_id = 'DgADBAADNQQAAtoagFPf4wwmFZdmyQI' driver_license_translation_1_file_id = 'DgADBAADswMAAisqQVAmooP-kVgLgAI' driver_license_translation_2_file_id = 'DgADBAAD1QMAAnrpQFBMZsT3HysjwwI' utility_bill_1_file_id = 'DgADBAADLAMAAhwfgVMyfGa5Nr0LvAI' utility_bill_2_file_id = 'DgADBAADaQQAAsFxgVNVfLZuT-_3ZQI' utility_bill_translation_1_file_id = 'DgADBAADyQUAAqyqQVC_eoX_KwNjJwI' utility_bill_translation_2_file_id = 'DgADBAADsQQAAubTQVDRO_FN3lOwWwI' driver_license_selfie_credentials_file_hash = 'Cila/qLXSBH7DpZFbb5bRZIRxeFW2uv/ulL0u0JNsYI=' driver_license_selfie_credentials_secret = 'tivdId6RNYNsvXYPppdzrbxOBuBOr9wXRPDcCvnXU7E=' 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 isinstance(driver_license.front_side, PassportFile) assert driver_license.front_side.file_id == self.driver_license_front_side_file_id assert isinstance(driver_license.reverse_side, PassportFile) assert driver_license.reverse_side.file_id == self.driver_license_reverse_side_file_id assert isinstance(driver_license.translation[0], PassportFile) assert driver_license.translation[0].file_id == self.driver_license_translation_1_file_id assert isinstance(driver_license.translation[1], PassportFile) assert driver_license.translation[1].file_id == self.driver_license_translation_2_file_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 isinstance(utility_bill.files[1], PassportFile) assert utility_bill.files[1].file_id == self.utility_bill_2_file_id assert isinstance(utility_bill.translation[0], PassportFile) assert utility_bill.translation[0].file_id == self.utility_bill_translation_1_file_id assert isinstance(utility_bill.translation[1], PassportFile) assert utility_bill.translation[1].file_id == self.utility_bill_translation_2_file_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 isinstance(driver_license.front_side, PassportFile) assert driver_license.front_side.file_id == self.driver_license_front_side_file_id assert isinstance(driver_license.reverse_side, PassportFile) assert driver_license.reverse_side.file_id == self.driver_license_reverse_side_file_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 isinstance(utility_bill.files[1], PassportFile) assert utility_bill.files[1].file_id == self.utility_bill_2_file_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=u'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 def get_file(*args, **kwargs): return File(args[0]) monkeypatch.setattr(passport_data.bot, 'get_file', get_file) file = selfie.get_file() assert file.file_id == selfie.file_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('telegram.utils.request.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-12.4.2/tests/test_passportelementerrordatafield.py000066400000000000000000000072451362023133600264200ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): source = 'data' type = 'test_type' field_name = 'test_field' data_hash = 'data_hash' message = 'Error message' 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-12.4.2/tests/test_passportelementerrorfile.py000066400000000000000000000060141362023133600254130ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): source = 'file' type = 'test_type' file_hash = 'file_hash' message = 'Error message' 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-12.4.2/tests/test_passportelementerrorfiles.py000066400000000000000000000062031362023133600255760ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): source = 'files' type = 'test_type' file_hashes = ['hash1', 'hash2'] message = 'Error message' 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-12.4.2/tests/test_passportelementerrorfrontside.py000066400000000000000000000062771362023133600265040ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): source = 'front_side' type = 'test_type' file_hash = 'file_hash' message = 'Error message' 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-12.4.2/tests/test_passportelementerrorreverseside.py000066400000000000000000000063771362023133600270300ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): source = 'reverse_side' type = 'test_type' file_hash = 'file_hash' message = 'Error message' 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-12.4.2/tests/test_passportelementerrorselfie.py000066400000000000000000000061121362023133600257420ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): source = 'selfie' type = 'test_type' file_hash = 'file_hash' message = 'Error message' 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-12.4.2/tests/test_passportelementerrortranslationfile.py000066400000000000000000000066131362023133600276770ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): source = 'translation_file' type = 'test_type' file_hash = 'file_hash' message = 'Error message' 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-12.4.2/tests/test_passportelementerrortranslationfiles.py000066400000000000000000000067111362023133600300610ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): source = 'translation_files' type = 'test_type' file_hashes = ['hash1', 'hash2'] message = 'Error message' 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-12.4.2/tests/test_passportelementerrorunspecified.py000066400000000000000000000064101362023133600267720ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): source = 'unspecified' type = 'test_type' element_hash = 'element_hash' message = 'Error message' 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-12.4.2/tests/test_passportfile.py000066400000000000000000000046271362023133600227770ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @pytest.fixture(scope='class') def passport_file(): return PassportFile(file_id=TestPassportFile.file_id, file_size=TestPassportFile.file_size, file_date=TestPassportFile.file_date) class TestPassportFile(object): file_id = 'data' file_size = 50 file_date = 1532879128 def test_expected_values(self, passport_file): assert passport_file.file_id == self.file_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_size'] == passport_file.file_size) assert (passport_file_dict['file_date'] == passport_file.file_date) def test_equality(self): a = PassportFile(self.file_id, self.file_size, self.file_date) b = PassportFile(self.file_id, self.file_size, self.file_date) c = PassportFile(self.file_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-12.4.2/tests/test_persistence.py000066400000000000000000001464211362023133600226070ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import signal import sys 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 import pytest from telegram import Update, Message, User, Chat, MessageEntity from telegram.ext import BasePersistence, Updater, ConversationHandler, MessageHandler, Filters, \ PicklePersistence, CommandHandler, DictPersistence, TypeHandler @pytest.fixture(scope="function") def base_persistence(): return BasePersistence(store_chat_data=True, store_user_data=True, store_bot_data=True) @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'}, -67890: {3: 'test4'}}) @pytest.fixture(scope="function") def user_data(): return defaultdict(dict, {12345: {'test1': 'test2'}, 67890: {3: 'test4'}}) @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 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 return u class TestBasePersistence(object): def test_creation(self, base_persistence): assert base_persistence.store_chat_data assert base_persistence.store_user_data with pytest.raises(NotImplementedError): base_persistence.get_bot_data() with pytest.raises(NotImplementedError): base_persistence.get_chat_data() with pytest.raises(NotImplementedError): base_persistence.get_user_data() with pytest.raises(NotImplementedError): base_persistence.get_conversations("test") with pytest.raises(NotImplementedError): base_persistence.update_bot_data(None) with pytest.raises(NotImplementedError): base_persistence.update_chat_data(None, None) with pytest.raises(NotImplementedError): base_persistence.update_user_data(None, None) with pytest.raises(NotImplementedError): base_persistence.update_conversation(None, None, None) 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 with pytest.raises(NotImplementedError): dp.add_handler(ConversationHandler([], {}, [], persistent=True, name="My Handler")) def test_dispatcher_integration_init(self, bot, base_persistence, chat_data, user_data, bot_data): def get_user_data(): return "test" def get_chat_data(): return "test" def get_bot_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 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"): u = 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"): u = Updater(bot=bot, persistence=base_persistence) def get_bot_data(): return bot_data base_persistence.get_bot_data = get_bot_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 u.dispatcher.chat_data[442233]['test5'] = 'test6' assert u.dispatcher.chat_data[442233]['test5'] == 'test6' def test_dispatcher_integration_handlers(self, caplog, bot, base_persistence, chat_data, user_data, bot_data): def get_user_data(): return user_data def get_chat_data(): return chat_data def get_bot_data(): return bot_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.update_chat_data = lambda x: x # base_persistence.update_user_data = lambda x: 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' 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, user1, None, chat2) u = Update(0, m) with caplog.at_level(logging.ERROR): dp.process_update(u) rec = caplog.records[-1] assert rec.msg == 'No error handlers are registered, logging exception.' assert rec.levelname == 'ERROR' rec = caplog.records[-2] assert rec.msg == '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() base_persistence.update_chat_data = save_chat_data base_persistence.update_user_data = save_user_data base_persistence.update_bot_data = save_bot_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' 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(object): 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 @pytest.fixture(scope='function') def pickle_persistence(): return PicklePersistence(filename='pickletest', store_user_data=True, store_chat_data=True, store_bot_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, 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, 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, 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_conversations', 'pickletest']: with open(name, 'w') as f: f.write('(())') yield True for name in ['pickletest_user_data', 'pickletest_chat_data', 'pickletest_bot_data', 'pickletest_conversations', 'pickletest']: os.remove(name) @pytest.fixture(scope='function') def good_pickle_files(user_data, chat_data, bot_data, conversations): all = {'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(all, f) yield True for name in ['pickletest_user_data', 'pickletest_chat_data', 'pickletest_bot_data', 'pickletest_conversations', 'pickletest']: os.remove(name) @pytest.fixture(scope='function') def pickle_files_wo_bot_data(user_data, chat_data, conversations): all = {'user_data': user_data, 'chat_data': chat_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_conversations', 'wb') as f: pickle.dump(conversations, f) with open('pickletest', 'wb') as f: pickle.dump(all, f) yield True for name in ['pickletest_user_data', 'pickletest_chat_data', 'pickletest_conversations', 'pickletest']: os.remove(name) @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, user, None, chat, text="Hi there", bot=bot) return Update(0, message=message) class TestPickelPersistence(object): 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_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_chat_data() == {} 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_conversations'): 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_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 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 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() 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() def test_updating_multi_file(self, pickle_persistence, good_pickle_files): 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 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 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 bot_data_test == bot_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 conversations_test['name1'] == conversation1 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[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 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 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 bot_data_test == bot_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 conversations_test['name1'] == conversation1 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 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 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 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() context.user_data['test1'] = 'test2' context.chat_data['test3'] = 'test4' context.bot_data['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() 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) del (dp) del (u) del (pickle_persistence) pickle_persistence_2 = PicklePersistence(filename='pickletest', store_user_data=True, store_chat_data=True, store_bot_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!' u.signal_handler(signal.SIGINT, None) del (dp) del (u) del (pickle_persistence) pickle_persistence_2 = PicklePersistence(filename='pickletest', store_user_data=True, store_chat_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!' def test_flush_on_stop_only_bot(self, bot, update, pickle_persistence_only_bot): os.remove('pickletest_user_data') os.remove('pickletest_chat_data') os.remove('pickletest_bot_data') 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!' u.signal_handler(signal.SIGINT, None) del (dp) del (u) del (pickle_persistence_only_bot) pickle_persistence_2 = PicklePersistence(filename='pickletest', store_user_data=False, store_chat_data=False, store_bot_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()['my_test3'] == 'Working3!' def test_flush_on_stop_only_chat(self, bot, update, pickle_persistence_only_chat): os.remove('pickletest_bot_data') 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!' u.signal_handler(signal.SIGINT, None) del (dp) del (u) del (pickle_persistence_only_chat) pickle_persistence_2 = PicklePersistence(filename='pickletest', store_user_data=False, store_chat_data=True, store_bot_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() == {} def test_flush_on_stop_only_user(self, bot, update, pickle_persistence_only_user): os.remove('pickletest_chat_data') 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!' u.signal_handler(signal.SIGINT, None) del (dp) del (u) del (pickle_persistence_only_user) pickle_persistence_2 = PicklePersistence(filename='pickletest', store_user_data=True, store_chat_data=False, store_bot_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()[-4242424242] == {} assert pickle_persistence_2.get_bot_data() == {} def test_with_conversationHandler(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(update, context): return NEXT2 next = MessageHandler(None, next) def next2(update, context): return ConversationHandler.END next2 = MessageHandler(None, next2) ch = ConversationHandler([start], {NEXT: [next], 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(update, context): return NEXT2 next = MessageHandler(None, next) def next2(update, context): return ConversationHandler.END next2 = MessageHandler(None, next2) nested_ch = ConversationHandler( [next], {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'] @classmethod def teardown_class(cls): try: for name in ['pickletest_user_data', 'pickletest_chat_data', 'pickletest_bot_data', 'pickletest_conversations', 'pickletest']: os.remove(name) except Exception: pass @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 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(object): 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_conversations('noname') == {} def test_bad_json_string_given(self): bad_user_data = 'thisisnojson99900()))(' bad_chat_data = 'thisisnojson99900()))(' bad_bot_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='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"]' 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='conversations'): DictPersistence(conversations_json=bad_conversations) def test_good_json_input(self, user_data_json, chat_data_json, bot_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, conversations_json=conversations_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 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_dict_outputs(self, user_data, user_data_json, chat_data, chat_data_json, bot_data, bot_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, 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.conversations == conversations @pytest.mark.skipif(sys.version_info < (3, 6), reason="dicts are not ordered in py<=3.5") def test_json_outputs(self, user_data_json, chat_data_json, bot_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, 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.bot_data_json == bot_data_json assert dict_persistence.conversations_json == conversations_json @pytest.mark.skipif(sys.version_info < (3, 6), reason="dicts are not ordered in py<=3.5") def test_json_changes(self, user_data, user_data_json, chat_data, chat_data_json, bot_data, bot_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, conversations_json=conversations_json) user_data_two = user_data.copy() user_data_two.update({4: {5: 6}}) dict_persistence.update_user_data(4, {5: 6}) assert dict_persistence.user_data == user_data_two assert dict_persistence.user_data_json != user_data_json assert dict_persistence.user_data_json == json.dumps(user_data_two) chat_data_two = chat_data.copy() chat_data_two.update({7: {8: 9}}) dict_persistence.update_chat_data(7, {8: 9}) assert dict_persistence.chat_data == chat_data_two assert dict_persistence.chat_data_json != chat_data_json assert dict_persistence.chat_data_json == json.dumps(chat_data_two) bot_data_two = bot_data.copy() bot_data_two.update({'7': {'8': '9'}}) bot_data['7'] = {'8': '9'} dict_persistence.update_bot_data(bot_data) assert dict_persistence.bot_data == bot_data_two assert dict_persistence.bot_data_json != bot_data_json assert dict_persistence.bot_data_json == json.dumps(bot_data_two) conversations_two = conversations.copy() conversations_two.update({'name4': {(1, 2): 3}}) dict_persistence.update_conversation('name4', (1, 2), 3) assert dict_persistence.conversations == conversations_two assert dict_persistence.conversations_json != conversations_json assert dict_persistence.conversations_json == encode_conversations_to_json( conversations_two) def test_with_handler(self, bot, update): dict_persistence = DictPersistence() 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() context.user_data['test1'] = 'test2' context.chat_data[3] = 'test4' context.bot_data['test1'] = 'test2' 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'] == 'test2': 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) del (dp) del (u) user_data = dict_persistence.user_data_json chat_data = dict_persistence.chat_data_json bot_data = dict_persistence.bot_data_json del (dict_persistence) dict_persistence_2 = DictPersistence(user_data_json=user_data, chat_data_json=chat_data, bot_data_json=bot_data) 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(update, context): return NEXT2 next = MessageHandler(None, next) def next2(update, context): return ConversationHandler.END next2 = MessageHandler(None, next2) ch = ConversationHandler([start], {NEXT: [next], 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(update, context): return NEXT2 next = MessageHandler(None, next) def next2(update, context): return ConversationHandler.END next2 = MessageHandler(None, next2) nested_ch = ConversationHandler( [next], {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'] python-telegram-bot-12.4.2/tests/test_photo.py000066400000000000000000000334751362023133600214200ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 import pytest from flaky import flaky from telegram import Sticker, TelegramError, PhotoSize, InputFile from telegram.utils.helpers import escape_markdown @pytest.fixture(scope='function') def photo_file(): f = open(u'tests/data/telegram.jpg', 'rb') yield f f.close() @pytest.fixture(scope='class') def _photo(bot, chat_id): with open('tests/data/telegram.jpg', 'rb') as f: return bot.send_photo(chat_id, photo=f, timeout=50).photo @pytest.fixture(scope='class') def thumb(_photo): return _photo[0] @pytest.fixture(scope='class') def photo(_photo): return _photo[1] class TestPhoto(object): width = 800 height = 800 caption = u'PhotoTest - *Caption*' photo_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram_new.jpg' file_size = 29176 def test_creation(self, thumb, photo): # Make sure file has been uploaded. assert isinstance(photo, PhotoSize) assert isinstance(photo.file_id, str) assert photo.file_id != '' assert isinstance(thumb, PhotoSize) assert isinstance(thumb.file_id, str) assert thumb.file_id != '' def test_expected_values(self, photo, thumb): assert photo.width == self.width assert photo.height == self.height assert photo.file_size == self.file_size assert thumb.width == 320 assert thumb.height == 320 assert thumb.file_size == 9331 @flaky(3, 1) @pytest.mark.timeout(10) 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, parse_mode='Markdown') assert isinstance(message.photo[0], PhotoSize) assert isinstance(message.photo[0].file_id, str) assert message.photo[0].file_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 message.photo[1].file_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('*', '') @flaky(3, 1) @pytest.mark.timeout(10) 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 message.photo[0].file_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 message.photo[1].file_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) @pytest.mark.timeout(10) 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 message.photo[0].file_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 message.photo[1].file_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) @pytest.mark.timeout(10) @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.timeout(10) @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.timeout(10) @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) @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.skip(reason='Doesnt work without API 4.5') 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_id == photo.file_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) @pytest.mark.timeout(10) 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 message.photo[0].file_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 message.photo[1].file_id != '' assert message.photo[1].width == photo.width assert message.photo[1].height == photo.height assert message.photo[1].file_size == photo.file_size @flaky(3, 1) @pytest.mark.timeout(10) 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 photo.file_id != '' @flaky(3, 1) @pytest.mark.timeout(10) 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 photo.file_id != '' @flaky(3, 1) @pytest.mark.timeout(10) 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(u'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 photo.file_id != '' @flaky(3, 1) @pytest.mark.timeout(10) 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 photo.file_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('telegram.utils.request.Request.post', test) message = bot.send_photo(photo=photo, chat_id=chat_id) assert message @flaky(3, 1) @pytest.mark.timeout(10) 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 message.photo[0].file_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 message.photo[1].file_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, '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.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['width'] == photo.width assert photo_dict['height'] == photo.height assert photo_dict['file_size'] == photo.file_size @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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 test(*args, **kwargs): return args[1] == photo.file_id monkeypatch.setattr('telegram.Bot.get_file', test) assert photo.get_file() def test_equality(self, photo): a = PhotoSize(photo.file_id, self.width, self.height) b = PhotoSize(photo.file_id, self.width, self.height) c = PhotoSize(photo.file_id, 0, 0) d = PhotoSize('', self.width, self.height) e = Sticker(photo.file_id, self.width, self.height, 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-12.4.2/tests/test_poll.py000066400000000000000000000060101362023133600212160ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 Poll, PollOption @pytest.fixture(scope="class") def poll_option(): return PollOption(text=TestPollOption.text, voter_count=TestPollOption.voter_count) class TestPollOption(object): text = "test option" voter_count = 3 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 @pytest.fixture(scope='class') def poll(): return Poll(TestPoll.id, TestPoll.question, TestPoll.options, TestPoll.is_closed) class TestPoll(object): id = 'id' question = 'Test?' options = [PollOption('test', 10), PollOption('test2', 11)] is_closed = True def test_de_json(self): json_dict = { 'id': self.id, 'question': self.question, 'options': [o.to_dict() for o in self.options], 'is_closed': self.is_closed } poll = Poll.de_json(json_dict, None) 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.is_closed == self.is_closed 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['is_closed'] == poll.is_closed python-telegram-bot-12.4.2/tests/test_precheckoutquery.py000066400000000000000000000104141362023133600236550ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @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(object): 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_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 test(*args, **kwargs): return args[0] == pre_checkout_query.id monkeypatch.setattr(pre_checkout_query.bot, 'answer_pre_checkout_query', test) assert pre_checkout_query.answer() 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-12.4.2/tests/test_precheckoutqueryhandler.py000066400000000000000000000145551362023133600252250ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, User(1, '', False), None, Chat(1, ''), 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(object): test_flag = False @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-12.4.2/tests/test_regexhandler.py000066400000000000000000000233371362023133600227330ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, User(1, '', False), None, Chat(1, ''), 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, User(1, '', False), None, Chat(1, ''), text='test message', bot=bot) class TestRegexHandler(object): test_flag = False @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-12.4.2/tests/test_replykeyboardmarkup.py000066400000000000000000000112471362023133600243540ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @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) class TestReplyKeyboardMarkup(object): keyboard = [[KeyboardButton('button1'), KeyboardButton('button2')]] resize_keyboard = True one_time_keyboard = True selective = True @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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 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 python-telegram-bot-12.4.2/tests/test_replykeyboardremove.py000066400000000000000000000036021362023133600243460ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): remove_keyboard = True selective = True @flaky(3, 1) @pytest.mark.timeout(10) 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-12.4.2/tests/test_request.py000066400000000000000000000027751362023133600217560ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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_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-12.4.2/tests/test_shippingaddress.py000066400000000000000000000105601362023133600234440ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): country_code = 'GB' state = 'state' city = 'London' street_line1 = '12 Grimmauld Place' street_line2 = 'street_line2' post_code = 'WC1' 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-12.4.2/tests/test_shippingoption.py000066400000000000000000000046351362023133600233350ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): id = 'id' title = 'title' prices = [ LabeledPrice('Fish Container', 100), LabeledPrice('Premium Fish Container', 1000) ] 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, 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-12.4.2/tests/test_shippingquery.py000066400000000000000000000066101362023133600231650ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 @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(object): id = 5 invoice_payload = 'invoice_payload' from_user = User(0, '', False) shipping_address = ShippingAddress('GB', '', 'London', '12 Grimmauld Place', '', 'WC1') 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 == 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 test(*args, **kwargs): return args[0] == shipping_query.id monkeypatch.setattr(shipping_query.bot, 'answer_shipping_query', test) assert shipping_query.answer() 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-12.4.2/tests/test_shippingqueryhandler.py000066400000000000000000000145611362023133600245270ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, User(1, '', False), None, Chat(1, ''), 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(object): test_flag = False @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-12.4.2/tests/test_sticker.py000066400000000000000000000314331362023133600217230ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 import pytest from flaky import flaky from future.utils import PY2 from telegram import Sticker, PhotoSize, TelegramError, StickerSet, Audio, MaskPosition @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 class TestSticker(object): # 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 file_size = 39518 thumb_width = 319 thumb_height = 320 thumb_file_size = 21472 def test_creation(self, sticker): # Make sure file has been uploaded. assert isinstance(sticker, Sticker) assert isinstance(sticker.file_id, str) assert sticker.file_id != '' assert isinstance(sticker.thumb, PhotoSize) assert isinstance(sticker.thumb.file_id, str) assert sticker.thumb.file_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.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) @pytest.mark.timeout(10) def test_send_all_args(self, bot, chat_id, sticker_file, sticker): message = bot.send_sticker(chat_id, sticker=sticker_file, disable_notification=False) assert isinstance(message.sticker, Sticker) assert isinstance(message.sticker.file_id, str) assert message.sticker.file_id != '' assert message.sticker.width == sticker.width assert message.sticker.height == sticker.height assert message.sticker.is_animated == sticker.is_animated assert message.sticker.file_size == sticker.file_size assert isinstance(message.sticker.thumb, PhotoSize) assert isinstance(message.sticker.thumb.file_id, str) assert message.sticker.thumb.file_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 @flaky(3, 1) @pytest.mark.timeout(10) 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_path.startswith('https://') new_file.download('telegram.webp') assert os.path.isfile('telegram.webp') @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.skip(reason='Doesnt work without API 4.5') 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) @pytest.mark.timeout(10) 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 if PY2: assert sticker.emoji == self.emoji.decode('utf-8') else: assert sticker.emoji == self.emoji @flaky(3, 1) @pytest.mark.timeout(10) 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 message.sticker.file_id != '' assert message.sticker.width == sticker.width assert message.sticker.height == sticker.height assert message.sticker.is_animated == sticker.is_animated assert message.sticker.file_size == sticker.file_size assert isinstance(message.sticker.thumb, PhotoSize) assert isinstance(message.sticker.thumb.file_id, str) assert message.sticker.thumb.file_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': 'not a file id', 'width': self.width, 'height': self.height, 'is_animated': self.is_animated, '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 == 'not a file id' assert json_sticker.width == self.width assert json_sticker.height == self.height assert json_sticker.is_animated == self.is_animated 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('telegram.utils.request.Request.post', test) message = bot.send_sticker(sticker=sticker, chat_id=chat_id) assert message 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['width'] == sticker.width assert sticker_dict['height'] == sticker.height assert sticker_dict['is_animated'] == sticker.is_animated assert sticker_dict['file_size'] == sticker.file_size assert sticker_dict['thumb'] == sticker.thumb.to_dict() @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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, self.width, self.height, self.is_animated) b = Sticker(sticker.file_id, self.width, self.height, self.is_animated) c = Sticker(sticker.file_id, 0, 0, False) d = Sticker('', self.width, self.height, self.is_animated) e = PhotoSize(sticker.file_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('test_by_{0}'.format(bot.username)) if len(ss.stickers) > 100: raise Exception('stickerset is growing too large.') return ss class TestStickerSet(object): title = 'Test stickers' is_animated = True contains_masks = False stickers = [Sticker('file_id', 512, 512, True)] name = 'NOTAREALNAME' def test_de_json(self, bot): name = 'test_by_{0}'.format(bot.username) json_dict = { 'name': name, 'title': self.title, 'is_animated': self.is_animated, 'contains_masks': self.contains_masks, 'stickers': [x.to_dict() for x in self.stickers] } 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.contains_masks == self.contains_masks assert sticker_set.stickers == self.stickers @flaky(3, 1) @pytest.mark.timeout(10) def test_bot_methods_1(self, bot, chat_id): with open('tests/data/telegram_sticker.png', 'rb') as f: file = bot.upload_sticker_file(95205500, f) assert file assert bot.add_sticker_to_set(chat_id, 'test_by_{0}'.format(bot.username), file.file_id, '😄') 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['contains_masks'] == sticker_set.contains_masks assert sticker_set_dict['stickers'][0] == sticker_set.stickers[0].to_dict() @flaky(3, 1) @pytest.mark.timeout(10) def test_bot_methods_2(self, bot, sticker_set): file_id = sticker_set.stickers[0].file_id assert bot.set_sticker_position_in_set(file_id, 1) @flaky(10, 1) @pytest.mark.timeout(10) def test_bot_methods_3(self, bot, sticker_set): sleep(1) file_id = sticker_set.stickers[-1].file_id assert bot.delete_sticker_from_set(file_id) def test_get_file_instance_method(self, monkeypatch, sticker): def test(*args, **kwargs): return args[1] == sticker.file_id monkeypatch.setattr('telegram.Bot.get_file', test) assert sticker.get_file() def test_equality(self): a = StickerSet(self.name, self.title, self.is_animated, self.contains_masks, self.stickers) b = StickerSet(self.name, self.title, self.is_animated, self.contains_masks, self.stickers) c = StickerSet(self.name, None, None, None, None) d = StickerSet('blah', self.title, self.is_animated, self.contains_masks, self.stickers) 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(object): 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 python-telegram-bot-12.4.2/tests/test_stringcommandhandler.py000066400000000000000000000140731362023133600244630ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, User(1, '', False), None, Chat(1, ''), 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(object): test_flag = False @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-12.4.2/tests/test_stringregexhandler.py000066400000000000000000000143731362023133600241620ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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, User(1, '', False), None, Chat(1, ''), 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(object): test_flag = False @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-12.4.2/tests/test_successfulpayment.py000066400000000000000000000107151362023133600240340ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): 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_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-12.4.2/tests/test_telegramobject.py000066400000000000000000000060011362023133600232370ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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(object): 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() python-telegram-bot-12.4.2/tests/test_typehandler.py000066400000000000000000000072311362023133600225750ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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(object): test_flag = False @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-12.4.2/tests/test_update.py000066400000000000000000000132171362023133600215410ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 (Message, User, Update, Chat, CallbackQuery, InlineQuery, ChosenInlineResult, ShippingQuery, PreCheckoutQuery, Poll, PollOption) message = Message(1, User(1, '', False), None, Chat(1, ''), 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')}, {'poll': Poll('id', '?', [PollOption('.', 1)], False)} ] all_types = ('message', 'edited_message', 'callback_query', 'channel_post', 'edited_channel_post', 'inline_query', 'chosen_inline_result', 'shipping_query', 'pre_checkout_query', 'poll') 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(object): update_id = 868573637 @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_de_json_default_quote(self, bot): json_dict = {'update_id': TestUpdate.update_id} json_dict['message'] = message.to_dict() json_dict['default_quote'] = True update = Update.de_json(json_dict, bot) assert update.message.default_quote is True 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): 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): 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-12.4.2/tests/test_updater.py000066400000000000000000000372551362023133600217330ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 signal import sys 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 try: # python2 from urllib2 import urlopen, Request, HTTPError except ImportError: # python3 from urllib.request import Request, urlopen from urllib.error import HTTPError import pytest from future.builtins import bytes from telegram import TelegramError, Message, User, Chat, Update, Bot from telegram.error import Unauthorized, InvalidToken, TimedOut, RetryAfter from telegram.ext import Updater, Dispatcher, BasePersistence signalskip = pytest.mark.skipif(sys.platform == 'win32', reason='Can\'t send signals without stopping ' 'whole process on windows') class TestUpdater(object): message_count = 0 received = None attempts = 0 err_handler_called = Event() cb_handler_called = Event() @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() 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() # TODO: test clean= argument of Updater._bootstrap @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() 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('unhandled exception in Bot:{}:updater'.format(updater.bot.id) 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 def test_webhook(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 for travis updater.start_webhook( ip, port, url_path='TOKEN') sleep(.2) try: # Now, we send an update to the server via urlopen update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''), text='Webhook')) self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN') sleep(.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(.2) assert not updater.httpd.is_running updater.stop() 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 for travis 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, clean=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 for travis updater.start_webhook(ip, port, webhook_url=None) sleep(.2) # Now, we send an update to the server via urlopen update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''), text='Webhook 2')) self._send_webhook_msg(ip, port, update.to_json()) sleep(.2) assert q.get(False) == update updater.stop() def test_webhook_default_quote(self, monkeypatch, updater): updater._default_quote = True 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 for travis updater.start_webhook( ip, port, url_path='TOKEN') sleep(.2) # Now, we send an update to the server via urlopen update = Update(1, message=Message(1, User(1, '', False), None, Chat(1, ''), text='Webhook')) self._send_webhook_msg(ip, port, update.to_json(), 'TOKEN') sleep(.2) # assert q.get(False) == update assert q.get(False).message.default_quote is True updater.stop() @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 @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(.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 = 'http://{ip}:{port}/{path}'.format(ip=ip, port=port, path=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() rec = caplog.records[-1] assert rec.msg.startswith('Received signal {}'.format(signal.SIGTERM)) assert rec.levelname == 'INFO' # If we get this far, idle() ran through sleep(.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(.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): dispatcher = Dispatcher(None, None) bot = Bot('123:zyxw') with pytest.raises(ValueError): Updater(bot=bot, dispatcher=dispatcher) def test_mutual_exclude_persistence_dispatcher(self): dispatcher = Dispatcher(None, None) persistence = BasePersistence() with pytest.raises(ValueError): Updater(dispatcher=dispatcher, persistence=persistence) def test_mutual_exclude_workers_dispatcher(self): dispatcher = Dispatcher(None, None) with pytest.raises(ValueError): Updater(dispatcher=dispatcher, workers=8) def test_mutual_exclude_use_context_dispatcher(self): dispatcher = Dispatcher(None, None) use_context = not dispatcher.use_context with pytest.raises(ValueError): Updater(dispatcher=dispatcher, use_context=use_context) python-telegram-bot-12.4.2/tests/test_user.py000066400000000000000000000170661362023133600212430ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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, Update @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 } @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, bot=bot) class TestUser(object): id = 1 is_bot = True first_name = u'first\u2022name' last_name = u'last\u2022name' username = 'username' language_code = 'en_us' 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 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 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 def test_name(self, user): assert user.name == '@username' user.username = None assert user.name == u'first\u2022name last\u2022name' user.last_name = None assert user.name == u'first\u2022name' user.username = self.username assert user.name == '@username' def test_full_name(self, user): assert user.full_name == u'first\u2022name last\u2022name' user.last_name = None assert user.full_name == u'first\u2022name' def test_link(self, user): assert user.link == 'https://t.me/{}'.format(user.username) user.username = None assert user.link is None def test_get_profile_photos(self, monkeypatch, user): def test(*args, **kwargs): return args[0] == user.id monkeypatch.setattr(user.bot, 'get_user_profile_photos', test) assert user.get_profile_photos() def test_instance_method_send_message(self, monkeypatch, user): def test(*args, **kwargs): return args[0] == user.id and args[1] == 'test' monkeypatch.setattr(user.bot, 'send_message', test) assert user.send_message('test') def test_instance_method_send_photo(self, monkeypatch, user): def test(*args, **kwargs): return args[0] == user.id and args[1] == 'test_photo' monkeypatch.setattr(user.bot, 'send_photo', test) assert user.send_photo('test_photo') def test_instance_method_send_audio(self, monkeypatch, user): def test(*args, **kwargs): return args[0] == user.id and args[1] == 'test_audio' monkeypatch.setattr(user.bot, 'send_audio', test) assert user.send_audio('test_audio') def test_instance_method_send_document(self, monkeypatch, user): def test(*args, **kwargs): return args[0] == user.id and args[1] == 'test_document' monkeypatch.setattr(user.bot, 'send_document', test) assert user.send_document('test_document') def test_instance_method_send_sticker(self, monkeypatch, user): def test(*args, **kwargs): return args[0] == user.id and args[1] == 'test_sticker' monkeypatch.setattr(user.bot, 'send_sticker', test) assert user.send_sticker('test_sticker') def test_instance_method_send_video(self, monkeypatch, user): def test(*args, **kwargs): return args[0] == user.id and args[1] == 'test_video' monkeypatch.setattr(user.bot, 'send_video', test) assert user.send_video('test_video') def test_instance_method_send_video_note(self, monkeypatch, user): def test(*args, **kwargs): return args[0] == user.id and args[1] == 'test_video_note' monkeypatch.setattr(user.bot, 'send_video_note', test) assert user.send_video_note('test_video_note') def test_instance_method_send_voice(self, monkeypatch, user): def test(*args, **kwargs): return args[0] == user.id and args[1] == 'test_voice' monkeypatch.setattr(user.bot, 'send_voice', test) assert user.send_voice('test_voice') def test_instance_method_send_animation(self, monkeypatch, user): def test(*args, **kwargs): return args[0] == user.id and args[1] == 'test_animation' monkeypatch.setattr(user.bot, 'send_animation', test) assert user.send_animation('test_animation') def test_mention_html(self, user): expected = u'{}' 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_markdown(self, user): expected = u'[{}](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_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-12.4.2/tests/test_userprofilephotos.py000066400000000000000000000036531362023133600240560ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # 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(object): total_count = 2 photos = [ [ PhotoSize('file_id1', 512, 512), PhotoSize('file_id2', 512, 512) ], [ PhotoSize('file_id3', 512, 512), PhotoSize('file_id4', 512, 512) ] ] 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() python-telegram-bot-12.4.2/tests/test_venue.py000066400000000000000000000073261362023133600214050ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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, Venue @pytest.fixture(scope='class') def venue(): return Venue(TestVenue.location, TestVenue.title, TestVenue.address, foursquare_id=TestVenue.foursquare_id, foursquare_type=TestVenue.foursquare_type) class TestVenue(object): location = Location(longitude=-46.788279, latitude=-23.691288) title = 'title' address = 'address' foursquare_id = 'foursquare id' foursquare_type = 'foursquare type' 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 } 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 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) monkeypatch.setattr('telegram.utils.request.Request.post', test) message = bot.send_venue(chat_id, venue=venue) assert message 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 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-12.4.2/tests/test_video.py000066400000000000000000000222501362023133600213620ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 Video, TelegramError, Voice, PhotoSize from telegram.utils.helpers import escape_markdown @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(object): width = 360 height = 640 duration = 5 file_size = 326534 mime_type = 'video/mp4' supports_streaming = True thumb_width = 180 thumb_height = 320 thumb_file_size = 1767 caption = u'VideoTest - *Caption*' video_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.mp4' def test_creation(self, video): # Make sure file has been uploaded. assert isinstance(video, Video) assert isinstance(video.file_id, str) assert video.file_id != '' assert isinstance(video.thumb, PhotoSize) assert isinstance(video.thumb.file_id, str) assert video.thumb.file_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) @pytest.mark.timeout(10) 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, 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 message.video.file_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 @flaky(3, 1) @pytest.mark.timeout(10) 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_path.startswith('https://') new_file.download('telegram.mp4') assert os.path.isfile('telegram.mp4') @flaky(3, 1) @pytest.mark.timeout(10) 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 message.video.file_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 message.video.thumb.file_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) @pytest.mark.timeout(10) @pytest.mark.skip(reason='Doesnt work without API 4.5') 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('telegram.utils.request.Request.post', test) message = bot.send_video(chat_id, video=video) assert message @flaky(3, 1) @pytest.mark.timeout(10) @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.timeout(10) @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.timeout(10) @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_de_json(self, bot): json_dict = { 'file_id': 'not a file id', 'width': self.width, 'height': self.height, 'duration': self.duration, 'mime_type': self.mime_type, 'file_size': self.file_size } json_video = Video.de_json(json_dict, bot) assert json_video.file_id == 'not a file 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 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['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 @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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 test(*args, **kwargs): return args[1] == video.file_id monkeypatch.setattr('telegram.Bot.get_file', test) assert video.get_file() def test_equality(self, video): a = Video(video.file_id, self.width, self.height, self.duration) b = Video(video.file_id, self.width, self.height, self.duration) c = Video(video.file_id, 0, 0, 0) d = Video('', self.width, self.height, self.duration) e = Voice(video.file_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-12.4.2/tests/test_videonote.py000066400000000000000000000142611362023133600222530ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 VideoNote, TelegramError, Voice, PhotoSize @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(object): length = 240 duration = 3 file_size = 132084 thumb_width = 240 thumb_height = 240 thumb_file_size = 11547 caption = u'VideoNoteTest - Caption' 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 video_note.file_id != '' assert isinstance(video_note.thumb, PhotoSize) assert isinstance(video_note.thumb.file_id, str) assert video_note.thumb.file_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) @pytest.mark.timeout(10) 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, thumb=thumb_file) assert isinstance(message.video_note, VideoNote) assert isinstance(message.video_note.file_id, str) assert message.video_note.file_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 @flaky(3, 1) @pytest.mark.timeout(10) 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_path.startswith('https://') new_file.download('telegram2.mp4') assert os.path.isfile('telegram2.mp4') @flaky(3, 1) @pytest.mark.timeout(10) @pytest.mark.skip(reason='Doesnt work without API 4.5') 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('telegram.utils.request.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': 'not a file 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 == 'not a file 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['length'] == video_note.length assert video_note_dict['duration'] == video_note.duration assert video_note_dict['file_size'] == video_note.file_size @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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 test(*args, **kwargs): return args[1] == video_note.file_id monkeypatch.setattr('telegram.Bot.get_file', test) assert video_note.get_file() def test_equality(self, video_note): a = VideoNote(video_note.file_id, self.length, self.duration) b = VideoNote(video_note.file_id, self.length, self.duration) c = VideoNote(video_note.file_id, 0, 0) d = VideoNote('', self.length, self.duration) e = Voice(video_note.file_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-12.4.2/tests/test_voice.py000066400000000000000000000172061362023133600213660ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 Audio, Voice, TelegramError from telegram.utils.helpers import escape_markdown @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(object): duration = 3 mime_type = 'audio/ogg' file_size = 9199 caption = u'Test *voice*' voice_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.ogg' def test_creation(self, voice): # Make sure file has been uploaded. assert isinstance(voice, Voice) assert isinstance(voice.file_id, str) assert voice.file_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) @pytest.mark.timeout(10) 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, parse_mode='Markdown') assert isinstance(message.voice, Voice) assert isinstance(message.voice.file_id, str) assert message.voice.file_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('*', '') @flaky(3, 1) @pytest.mark.timeout(10) 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_path.startswith('https://') new_file.download('telegram.ogg') assert os.path.isfile('telegram.ogg') @flaky(3, 1) @pytest.mark.timeout(10) 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 message.voice.file_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) @pytest.mark.timeout(10) @pytest.mark.skip(reason='Doesnt work without API 4.5') 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('telegram.utils.request.Request.post', test) message = bot.send_voice(chat_id, voice=voice) assert message @flaky(3, 1) @pytest.mark.timeout(10) @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.timeout(10) @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.timeout(10) @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_de_json(self, bot): json_dict = { 'file_id': 'not a file 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 == 'not a file 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['duration'] == voice.duration assert voice_dict['mime_type'] == voice.mime_type assert voice_dict['file_size'] == voice.file_size @flaky(3, 1) @pytest.mark.timeout(10) 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) @pytest.mark.timeout(10) 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 test(*args, **kwargs): return args[1] == voice.file_id monkeypatch.setattr('telegram.Bot.get_file', test) assert voice.get_file() def test_equality(self, voice): a = Voice(voice.file_id, self.duration) b = Voice(voice.file_id, self.duration) c = Voice(voice.file_id, 0) d = Voice('', self.duration) e = Audio(voice.file_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-12.4.2/tests/travis_fold.py000066400000000000000000000064121362023133600215330ustar00rootroot00000000000000#!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2020 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser Public License for more details. # # You 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 collections import defaultdict 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('travis_fold:start:plugin.{}\n{}\n'.format(plugin_name, text)) original(terminalreporter) terminalreporter.write('travis_fold:end:plugin.{}\n'.format(plugin_name)) 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.keys(): hookimpl.function = terminal_summary_wrapper(hookimpl.function, hookimpl.plugin_name) terminal = None previous_name = None failed = set() durations = defaultdict(int) def _get_name(location): return '{}::{}'.format(location[0], location[2].split('.')[0].split('[')[0]) @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() name = _get_name(item.location) durations[name] += rep.duration if rep.failed: global failed failed.add(name) @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('\ntravis_fold:start:{}\r'.format(name.split('::')[1])) terminal.write('travis_time:start:{}time\r'.format(name.split('::')[1])) terminal.write(name) yield if nextitem is None or _get_name(nextitem.location) != name: global failed if name in failed: terminal.write('') else: terminal.write('\n\ntravis_fold:end:{}'.format(name.split('::')[1])) terminal.write('\rtravis_time:end:{}time:' 'duration={}'.format(name.split('::')[1], int(durations[name] * 1E9))) time.sleep(0.001) # Tiny sleep so travis hopefully doesn't mangle the log