mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2026-06-28 04:05:01 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e7f4fae6f | |||
| dae5ab47a0 | |||
| a9d9b1d750 | |||
| 90496f70a5 | |||
| 940b42e048 | |||
| a582515766 | |||
| 3d42df3366 | |||
| 2c67a9833b | |||
| 5e8a961669 | |||
| a5ba64becb | |||
| 2a3169a22f | |||
| 894d8281ab | |||
| 2fdf48023b | |||
| 4e717a172b | |||
| 10c9ec2313 | |||
| 096a7c3593 | |||
| e9d9f01bd4 | |||
| 8b4b22cc89 | |||
| b294c92bad | |||
| 264de2b7c1 | |||
| ac64027580 | |||
| 34bdbc632a | |||
| bbcff96804 | |||
| 93449443b2 | |||
| 8cdb20a85a | |||
| 6fddb49af5 | |||
| b0aef0c718 | |||
| 88eccc6608 | |||
| 3d8771bbdf | |||
| 7152b5aaf9 | |||
| 98147fce32 | |||
| e54e9f2347 | |||
| 3545139dd7 | |||
| d0c27e2d46 | |||
| 3318239cf6 | |||
| aadb6df271 | |||
| 2cc9aac7dc | |||
| 1d007b1b60 | |||
| 3257148d13 | |||
| 805a798b50 | |||
| e60a42010b | |||
| ae88129f0f | |||
| 3812251dac | |||
| e1193425ca | |||
| ccf5e6c692 | |||
| 32dd415fb8 | |||
| f13aeaa2a1 | |||
| 4cd07361d1 | |||
| b38a1840b2 | |||
| fba3cc90d9 | |||
| 965ad17af8 | |||
| d5399de99b | |||
| 280306d1e9 |
@@ -25,7 +25,7 @@ Setting things up
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo pip install -r requirements.txt -r requirements-dev.txt
|
||||
$ pip install -r requirements.txt -r requirements-dev.txt
|
||||
|
||||
|
||||
5. Install pre-commit hooks:
|
||||
@@ -66,6 +66,26 @@ Here's how to make a one-off code change.
|
||||
- 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.
|
||||
|
||||
@@ -217,6 +237,7 @@ break the API classes. For example:
|
||||
.. _`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`: https://google-styleguide.googlecode.com/svn/trunk/pyguide.html
|
||||
.. _`Google Python Style Docstrings`: http://sphinx-doc.org/latest/ext/example_google.html
|
||||
.. _AUTHORS.rst: ../AUTHORS.rst
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: 'bug :bug:'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Thanks for reporting issues of python-telegram-bot!
|
||||
|
||||
Use this template to notify us if you found a bug, or if you want to request a new feature.
|
||||
If you're looking for help with programming your bot using our library, feel free to ask your
|
||||
questions in out telegram group at: https://t.me/pythontelegrambotgroup
|
||||
Use this template to notify us if you found a bug.
|
||||
|
||||
To make it easier for us to help you please enter detailed information below.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
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!*
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Question
|
||||
about: Get help with errors or general questions
|
||||
title: "[QUESTION]"
|
||||
labels: 'question :question:'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Hey there, you have a question? We are happy to answer. Please make sure no similar question was opened already.
|
||||
|
||||
The following template is a suggestion how you can report an issue you run into whilst using our library. If you just want to ask a question, feel free to delete everything; just make sure you have a describing title :)
|
||||
|
||||
Please mind that there is also a users' Telegram group at https://t.me/pythontelegrambotgroup for questions about the library. Questions asked there might be answered quicker than here. In case you are unable to join our group due to Telegram restrictions, you can use our IRC channel at https://webchat.freenode.net/?channels=##python-telegram-bot to participate in the group.
|
||||
-->
|
||||
|
||||
### 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
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
name: 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 }}
|
||||
@@ -0,0 +1,116 @@
|
||||
name: Testing your PR
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: 7 3 * * *
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
name: pytest
|
||||
runs-on: ${{matrix.os}}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [2.7, 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 $?
|
||||
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
|
||||
@@ -69,6 +69,7 @@ target/
|
||||
*.sublime*
|
||||
|
||||
# unitests files
|
||||
game.gif
|
||||
telegram.mp3
|
||||
telegram.mp4
|
||||
telegram2.mp4
|
||||
|
||||
@@ -10,9 +10,6 @@ repos:
|
||||
sha: 0b70e285e369bcb24b57b74929490ea7be9c4b19
|
||||
hooks:
|
||||
- id: flake8
|
||||
exclude: ^(setup.py|docs/source/conf.py)$
|
||||
args:
|
||||
- --ignore=W605,W503
|
||||
- repo: git://github.com/pre-commit/mirrors-pylint
|
||||
sha: 9d8dcbc2b86c796275680f239c1e90dcd50bd398
|
||||
hooks:
|
||||
|
||||
+8
-6
@@ -14,10 +14,11 @@ matrix:
|
||||
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
|
||||
- env: TEST_OFFICIAL=true
|
||||
|
||||
dist: trusty
|
||||
sudo: false
|
||||
@@ -36,17 +37,18 @@ before_cache:
|
||||
- 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; fi
|
||||
- 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; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then pip install ujson; else true; fi
|
||||
|
||||
script:
|
||||
- if [[ $TEST_OFFICIAL != 'true' ]]; then pytest -v -m nocoverage; fi
|
||||
- if [[ $TEST_OFFICIAL != 'true' ]]; then pytest -v -m "not nocoverage" --cov; fi
|
||||
- if [[ $TEST_OFFICIAL == 'true' ]]; then pytest -v tests/test_official.py; fi
|
||||
- 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
|
||||
|
||||
@@ -67,10 +67,12 @@ The following wonderful people contributed directly or indirectly to this projec
|
||||
- `Pieter Schutz <https://github.com/eldinnie>`_
|
||||
- `Poolitzer <https://github.com/Poolitzer>`_
|
||||
- `Rahiel Kasim <https://github.com/rahiel>`_
|
||||
- `Sahil Sharma <https://github.com/sahilsharma811>`_
|
||||
- `Sascha <https://github.com/saschalalala>`_
|
||||
- `Shelomentsev D <https://github.com/shelomentsevd>`_
|
||||
- `Simon Schürrle <https://github.com/SitiSchu>`_
|
||||
- `sooyhwang <https://github.com/sooyhwang>`_
|
||||
- `syntx <https://github.com/syntx>`_
|
||||
- `thodnev <https://github.com/thodnev>`_
|
||||
- `Trainer Jono <https://github.com/Tr-Jono>`_
|
||||
- `Valentijn <https://github.com/Faalentijn>`_
|
||||
@@ -78,5 +80,6 @@ The following wonderful people contributed directly or indirectly to this projec
|
||||
- `Vorobjev Simon <https://github.com/simonvorobjev>`_
|
||||
- `Wagner Macedo <https://github.com/wagnerluis1982>`_
|
||||
- `wjt <https://github.com/wjt>`_
|
||||
- `zeshuaro <https://github.com/zeshuaro>`_
|
||||
|
||||
Please add yourself here alphabetically when you submit your first pull request.
|
||||
|
||||
+114
-2
@@ -2,6 +2,118 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
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 improvments 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/1558
|
||||
.. _`#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*
|
||||
@@ -86,7 +198,7 @@ Bug fixes & improvements
|
||||
- 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`_)
|
||||
- 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`_)
|
||||
@@ -101,7 +213,7 @@ Buf fixes since v12.0.0b1
|
||||
- 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`)
|
||||
- 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
|
||||
|
||||
+7
-11
@@ -29,14 +29,9 @@ We have a vibrant community of developers helping each other in our `Telegram gr
|
||||
:target: https://www.gnu.org/licenses/lgpl-3.0.html
|
||||
:alt: LGPLv3 License
|
||||
|
||||
.. image:: https://travis-ci.org/python-telegram-bot/python-telegram-bot.svg?branch=master
|
||||
:target: https://travis-ci.org/python-telegram-bot/python-telegram-bot
|
||||
:alt: Travis CI Status
|
||||
|
||||
.. image:: https://img.shields.io/appveyor/ci/python-telegram-bot/python-telegram-bot/master.svg?logo=appveyor
|
||||
:target: https://ci.appveyor.com/project/python-telegram-bot/python-telegram-bot
|
||||
:alt: AppVeyor CI Status
|
||||
|
||||
.. image:: https://github.com/python-telegram-bot/python-telegram-bot/workflows/Testing%20your%20PR/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
|
||||
@@ -192,11 +187,12 @@ You can get help in several ways:
|
||||
|
||||
1. We have a vibrant community of developers helping each other in our `Telegram group <https://telegram.me/pythontelegrambotgroup>`_. Join us!
|
||||
|
||||
2. Our `Wiki pages <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ offer a growing amount of resources.
|
||||
2. Report bugs, request new features or ask questions by `creating an issue <https://github.com/python-telegram-bot/python-telegram-bot/issues/new/choose>`_.
|
||||
|
||||
3. You can ask for help on Stack Overflow using the `python-telegram-bot tag <https://stackoverflow.com/questions/tagged/python-telegram-bot>`_.
|
||||
3. Our `Wiki pages <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ offer a growing amount of resources.
|
||||
|
||||
4. You can even ask for help on Stack Overflow using the `python-telegram-bot tag <https://stackoverflow.com/questions/tagged/python-telegram-bot>`_.
|
||||
|
||||
4. As last resort, the developers are ready to help you with `serious issues <https://github.com/python-telegram-bot/python-telegram-bot/issues/new>`_.
|
||||
|
||||
|
||||
============
|
||||
|
||||
+5
-1
@@ -2,7 +2,7 @@ environment:
|
||||
|
||||
matrix:
|
||||
# For Python versions available on Appveyor, see
|
||||
# http://www.appveyor.com/docs/installed-software#python
|
||||
# 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.
|
||||
|
||||
@@ -10,6 +10,7 @@ environment:
|
||||
- PYTHON: "C:\\Python35"
|
||||
- PYTHON: "C:\\Python36"
|
||||
- PYTHON: "C:\\Python37"
|
||||
# - PYTHON: "C:\\Python38"
|
||||
|
||||
branches:
|
||||
only:
|
||||
@@ -26,6 +27,8 @@ install:
|
||||
# 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"
|
||||
@@ -33,6 +36,7 @@ install:
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- "pytest --version"
|
||||
- "pytest -m \"not nocoverage\" --cov --cov-report xml:coverage.xml"
|
||||
|
||||
after_test:
|
||||
|
||||
+2
-2
@@ -58,9 +58,9 @@ author = u'Leandro Toledo'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '12.0' # telegram.__version__[:3]
|
||||
version = '12.3' # telegram.__version__[:3]
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '12.0.0' # telegram.__version__
|
||||
release = '12.3.0' # telegram.__version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
||||
@@ -22,7 +22,7 @@ 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 <https://core.telegram.org/bots/api>`_
|
||||
Apart from the `telegram.ext` package the objects should reflect the types defined in the `official telegram bot api documentation <https://core.telegram.org/bots/api>`_.
|
||||
|
||||
.. toctree::
|
||||
telegram
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
telegram.ChatPermissions
|
||||
========================
|
||||
|
||||
.. autoclass:: telegram.ChatPermissions
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,6 @@
|
||||
telegram.LoginUrl
|
||||
=================
|
||||
|
||||
.. autoclass:: telegram.LoginUrl
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -13,6 +13,7 @@ telegram package
|
||||
telegram.chat
|
||||
telegram.chataction
|
||||
telegram.chatmember
|
||||
telegram.chatpermissions
|
||||
telegram.chatphoto
|
||||
telegram.constants
|
||||
telegram.contact
|
||||
@@ -31,6 +32,7 @@ telegram package
|
||||
telegram.inputmediavideo
|
||||
telegram.keyboardbutton
|
||||
telegram.location
|
||||
telegram.loginurl
|
||||
telegram.message
|
||||
telegram.messageentity
|
||||
telegram.parsemode
|
||||
@@ -140,8 +142,4 @@ Passport
|
||||
telegram.encryptedpassportelement
|
||||
telegram.encryptedcredentials
|
||||
|
||||
telegram.utils
|
||||
--------------
|
||||
|
||||
.. toctree::
|
||||
telegram.utils
|
||||
.. include:: telegram.utils.rst
|
||||
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
# Examples
|
||||
|
||||
In this folder there 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.
|
||||
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.
|
||||
|
||||
@@ -16,9 +16,15 @@ A common task for a bot is to ask information from the user. In v5.0 of this lib
|
||||
### [`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).
|
||||
|
||||
@@ -29,4 +35,4 @@ A basic example of a bot that can accept payments. Don't forget to enable and co
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
#
|
||||
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
|
||||
# If you're still using version 11.1.0, please see the examples at
|
||||
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
|
||||
|
||||
"""
|
||||
First, a few callback functions are defined. Then, those functions are passed to
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
#
|
||||
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
|
||||
# If you're still using version 11.1.0, please see the examples at
|
||||
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
|
||||
|
||||
"""
|
||||
First, a few callback functions are defined. Then, those functions are passed to
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Bot that explains Telegram's "Deep Linking Parameters" functionality.
|
||||
|
||||
This program is dedicated to the public domain under the CC0 license.
|
||||
|
||||
This Bot uses the Updater class to handle the bot.
|
||||
|
||||
First, a few handler functions are defined. Then, those functions are passed to
|
||||
the Dispatcher and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
|
||||
Usage:
|
||||
Deep Linking example. Send /start to get the link.
|
||||
Press Ctrl-C on the command line or send a signal to the process to stop the
|
||||
bot.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from telegram.ext import Updater, CommandHandler, Filters
|
||||
|
||||
# Enable logging
|
||||
from telegram.utils import helpers
|
||||
|
||||
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
level=logging.INFO)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Define constants that will allow us to reuse the deep-linking parameters.
|
||||
CHECK_THIS_OUT = 'check-this-out'
|
||||
USING_ENTITIES = 'using-entities-here'
|
||||
SO_COOL = 'so-cool'
|
||||
|
||||
|
||||
def start(update, context):
|
||||
"""Send a deep-linked URL when the command /start is issued."""
|
||||
bot = context.bot
|
||||
url = helpers.create_deep_linked_url(bot.get_me().username, CHECK_THIS_OUT, group=True)
|
||||
text = "Feel free to tell your friends about it:\n\n" + url
|
||||
update.message.reply_text(text)
|
||||
|
||||
|
||||
def deep_linked_level_1(update, context):
|
||||
"""Reached through the CHECK_THIS_OUT payload"""
|
||||
bot = context.bot
|
||||
url = helpers.create_deep_linked_url(bot.get_me().username, SO_COOL)
|
||||
text = "Awesome, you just accessed hidden functionality! " \
|
||||
" Now let's get back to the private chat."
|
||||
keyboard = InlineKeyboardMarkup.from_button(
|
||||
InlineKeyboardButton(text='Continue here!', url=url)
|
||||
)
|
||||
update.message.reply_text(text, reply_markup=keyboard)
|
||||
|
||||
|
||||
def deep_linked_level_2(update, context):
|
||||
"""Reached through the SO_COOL payload"""
|
||||
bot = context.bot
|
||||
url = helpers.create_deep_linked_url(bot.get_me().username, USING_ENTITIES)
|
||||
text = "You can also mask the deep-linked URLs as links: " \
|
||||
"[▶️ CLICK HERE]({0}).".format(url)
|
||||
update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True)
|
||||
|
||||
|
||||
def deep_linked_level_3(update, context):
|
||||
"""Reached through the USING_ENTITIES payload"""
|
||||
payload = context.args
|
||||
update.message.reply_text("Congratulations! This is as deep as it gets 👏🏻\n\n"
|
||||
"The payload was: {0}".format(payload))
|
||||
|
||||
|
||||
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 bot's token.
|
||||
updater = Updater("TOKEN", use_context=True)
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dp = updater.dispatcher
|
||||
|
||||
# More info on what deep linking actually is (read this first if it's unclear to you):
|
||||
# https://core.telegram.org/bots#deep-linking
|
||||
|
||||
# Register a deep-linking handler
|
||||
dp.add_handler(CommandHandler("start", deep_linked_level_1, Filters.regex(CHECK_THIS_OUT)))
|
||||
|
||||
# This one works with a textual link instead of an URL
|
||||
dp.add_handler(CommandHandler("start", deep_linked_level_2, Filters.regex(SO_COOL)))
|
||||
|
||||
# We can also pass on the deep-linking payload
|
||||
dp.add_handler(CommandHandler("start",
|
||||
deep_linked_level_3,
|
||||
Filters.regex(USING_ENTITIES),
|
||||
pass_args=True))
|
||||
|
||||
# Make sure the deep-linking handlers occur *before* the normal /start handler.
|
||||
dp.add_handler(CommandHandler("start", start))
|
||||
|
||||
# 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()
|
||||
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
#
|
||||
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
|
||||
# If you're still using version 11.1.0, please see the examples at
|
||||
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
|
||||
|
||||
"""
|
||||
Simple Bot to reply to Telegram messages.
|
||||
@@ -30,8 +26,8 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Define a few command handlers. These usually take the two arguments bot and
|
||||
# update. Error handlers also receive the raised TelegramError object in error.
|
||||
# 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):
|
||||
"""Send a message when the command /start is issued."""
|
||||
update.message.reply_text('Hi!')
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
#
|
||||
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
|
||||
# If you're still using version 11.1.0, please see the examples at
|
||||
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
|
||||
|
||||
"""
|
||||
First, a few handler functions are defined. Then, those functions are passed to
|
||||
@@ -31,8 +27,8 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Define a few command handlers. These usually take the two arguments bot and
|
||||
# update. Error handlers also receive the raised TelegramError object in error.
|
||||
# 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):
|
||||
"""Send a message when the command /start is issued."""
|
||||
update.message.reply_text('Hi!')
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
#
|
||||
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
|
||||
# If you're still using version 11.1.0, please see the examples at
|
||||
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
|
||||
|
||||
"""
|
||||
Basic example for a bot that uses inline keyboards.
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Simple inline keyboard bot with multiple CallbackQueryHandlers.
|
||||
|
||||
This Bot uses the Updater class to handle the bot.
|
||||
First, a few callback functions are defined as callback query handler. Then, those functions are
|
||||
passed to the Dispatcher and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
Usage:
|
||||
Example of a bot that uses inline keyboard that has multiple CallbackQueryHandlers arranged in a
|
||||
ConversationHandler.
|
||||
Send /start to initiate the conversation.
|
||||
Press Ctrl-C on the command line to stop the bot.
|
||||
"""
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, ConversationHandler
|
||||
import logging
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
level=logging.INFO)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Stages
|
||||
FIRST, SECOND = range(2)
|
||||
# Callback data
|
||||
ONE, TWO, THREE, FOUR = range(4)
|
||||
|
||||
|
||||
def start(update, context):
|
||||
"""Send message on `/start`."""
|
||||
# Get user that sent /start and log his name
|
||||
user = update.message.from_user
|
||||
logger.info("User %s started the conversation.", user.first_name)
|
||||
# Build InlineKeyboard where each button has a displayed text
|
||||
# and a string as callback_data
|
||||
# The keyboard is a list of button rows, where each row is in turn
|
||||
# a list (hence `[[...]]`).
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("1", callback_data=str(ONE)),
|
||||
InlineKeyboardButton("2", callback_data=str(TWO))]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
# Send message with text and appended InlineKeyboard
|
||||
update.message.reply_text(
|
||||
"Start handler, Choose a route",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
# Tell ConversationHandler that we're in state `FIRST` now
|
||||
return FIRST
|
||||
|
||||
|
||||
def start_over(update, context):
|
||||
"""Prompt same text & keyboard as `start` does but not as new message"""
|
||||
# Get CallbackQuery from Update
|
||||
query = update.callback_query
|
||||
# Get Bot from CallbackContext
|
||||
bot = context.bot
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("1", callback_data=str(ONE)),
|
||||
InlineKeyboardButton("2", callback_data=str(TWO))]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
# Instead of sending a new message, edit the message that
|
||||
# originated the CallbackQuery. This gives the feeling of an
|
||||
# interactive menu.
|
||||
bot.edit_message_text(
|
||||
chat_id=query.message.chat_id,
|
||||
message_id=query.message.message_id,
|
||||
text="Start handler, Choose a route",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
return FIRST
|
||||
|
||||
|
||||
def one(update, context):
|
||||
"""Show new choice of buttons"""
|
||||
query = update.callback_query
|
||||
bot = context.bot
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("3", callback_data=str(THREE)),
|
||||
InlineKeyboardButton("4", callback_data=str(FOUR))]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
bot.edit_message_text(
|
||||
chat_id=query.message.chat_id,
|
||||
message_id=query.message.message_id,
|
||||
text="First CallbackQueryHandler, Choose a route",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
return FIRST
|
||||
|
||||
|
||||
def two(update, context):
|
||||
"""Show new choice of buttons"""
|
||||
query = update.callback_query
|
||||
bot = context.bot
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("1", callback_data=str(ONE)),
|
||||
InlineKeyboardButton("3", callback_data=str(THREE))]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
bot.edit_message_text(
|
||||
chat_id=query.message.chat_id,
|
||||
message_id=query.message.message_id,
|
||||
text="Second CallbackQueryHandler, Choose a route",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
return FIRST
|
||||
|
||||
|
||||
def three(update, context):
|
||||
"""Show new choice of buttons"""
|
||||
query = update.callback_query
|
||||
bot = context.bot
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("Yes, let's do it again!", callback_data=str(ONE)),
|
||||
InlineKeyboardButton("Nah, I've had enough ...", callback_data=str(TWO))]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
bot.edit_message_text(
|
||||
chat_id=query.message.chat_id,
|
||||
message_id=query.message.message_id,
|
||||
text="Third CallbackQueryHandler. Do want to start over?",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
# Transfer to conversation state `SECOND`
|
||||
return SECOND
|
||||
|
||||
|
||||
def four(update, context):
|
||||
"""Show new choice of buttons"""
|
||||
query = update.callback_query
|
||||
bot = context.bot
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("2", callback_data=str(TWO)),
|
||||
InlineKeyboardButton("4", callback_data=str(FOUR))]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
bot.edit_message_text(
|
||||
chat_id=query.message.chat_id,
|
||||
message_id=query.message.message_id,
|
||||
text="Fourth CallbackQueryHandler, Choose a route",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
return FIRST
|
||||
|
||||
|
||||
def end(update, context):
|
||||
"""Returns `ConversationHandler.END`, which tells the
|
||||
ConversationHandler that the conversation is over"""
|
||||
query = update.callback_query
|
||||
bot = context.bot
|
||||
bot.edit_message_text(
|
||||
chat_id=query.message.chat_id,
|
||||
message_id=query.message.message_id,
|
||||
text="See you next time!"
|
||||
)
|
||||
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.
|
||||
updater = Updater("TOKEN", use_context=True)
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dp = updater.dispatcher
|
||||
|
||||
# Setup conversation handler with the states FIRST and SECOND
|
||||
# Use the pattern parameter to pass CallbackQueries with specific
|
||||
# data pattern to the corresponding handlers.
|
||||
# ^ means "start of line/string"
|
||||
# $ means "end of line/string"
|
||||
# So ^ABC$ will only allow 'ABC'
|
||||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('start', start)],
|
||||
states={
|
||||
FIRST: [CallbackQueryHandler(one, pattern='^' + str(ONE) + '$'),
|
||||
CallbackQueryHandler(two, pattern='^' + str(TWO) + '$'),
|
||||
CallbackQueryHandler(three, pattern='^' + str(THREE) + '$'),
|
||||
CallbackQueryHandler(four, pattern='^' + str(FOUR) + '$')],
|
||||
SECOND: [CallbackQueryHandler(start_over, pattern='^' + str(ONE) + '$'),
|
||||
CallbackQueryHandler(end, pattern='^' + str(TWO) + '$')]
|
||||
},
|
||||
fallbacks=[CommandHandler('start', start)]
|
||||
)
|
||||
|
||||
# Add ConversationHandler to dispatcher that will be used for handling
|
||||
# updates
|
||||
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()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 492 KiB |
@@ -0,0 +1,362 @@
|
||||
#!/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()
|
||||
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
#
|
||||
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
|
||||
# If you're still using version 11.1.0, please see the examples at
|
||||
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
|
||||
|
||||
"""
|
||||
Simple Bot to print/download all incoming passport data
|
||||
@@ -25,7 +21,7 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def msg(bot, update):
|
||||
def msg(update, context):
|
||||
# If we received any passport data
|
||||
passport_data = update.message.passport_data
|
||||
if passport_data:
|
||||
@@ -81,9 +77,9 @@ def msg(bot, update):
|
||||
actual_file.download()
|
||||
|
||||
|
||||
def error(bot, update, error):
|
||||
def error(update, context):
|
||||
"""Log Errors caused by Updates."""
|
||||
logger.warning('Update "%s" caused error "%s"', update, error)
|
||||
logger.warning('Update "%s" caused error "%s"', update, context.error)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
#
|
||||
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
|
||||
# If you're still using version 11.1.0, please see the examples at
|
||||
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
|
||||
|
||||
"""
|
||||
Basic example for a bot that can receive payment from user.
|
||||
@@ -46,7 +42,7 @@ def start_with_shipping_callback(update, context):
|
||||
currency = "USD"
|
||||
# price in dollars
|
||||
price = 1
|
||||
# price * 100 so as to include 2 d.p.
|
||||
# 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)]
|
||||
|
||||
@@ -70,7 +66,7 @@ def start_without_shipping_callback(update, context):
|
||||
currency = "USD"
|
||||
# price in dollars
|
||||
price = 1
|
||||
# price * 100 so as to include 2 d.p.
|
||||
# price * 100 so as to include 2 decimal points
|
||||
prices = [LabeledPrice("Test", price * 100)]
|
||||
|
||||
# optionally pass need_name=True, need_phone_number=True,
|
||||
@@ -107,9 +103,9 @@ def precheckout_callback(update, context):
|
||||
query.answer(ok=True)
|
||||
|
||||
|
||||
# finally, after contacting to the payment provider...
|
||||
# finally, after contacting the payment provider...
|
||||
def successful_payment_callback(update, context):
|
||||
# do something after successful receive of payment?
|
||||
# do something after successfully receiving payment?
|
||||
update.message.reply_text("Thank you for your payment!")
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
#
|
||||
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
|
||||
# If you're still using version 11.1.0, please see the examples at
|
||||
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
|
||||
|
||||
"""
|
||||
First, a few callback functions are defined. Then, those functions are passed to
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
#
|
||||
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
|
||||
# If you're still using version 11.1.0, please see the examples at
|
||||
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
|
||||
|
||||
"""
|
||||
Simple Bot to send timed Telegram messages.
|
||||
@@ -33,8 +29,8 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Define a few command handlers. These usually take the two arguments bot and
|
||||
# update. Error handlers also receive the raised TelegramError object in error.
|
||||
# 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 <seconds> to set a timer')
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ beautifulsoup4
|
||||
pytest==4.2.0
|
||||
pytest-timeout
|
||||
wheel
|
||||
attrs==19.1.0
|
||||
|
||||
@@ -14,7 +14,8 @@ upload-dir = docs/build/html
|
||||
|
||||
[flake8]
|
||||
max-line-length = 99
|
||||
ignore = W503
|
||||
ignore = W503, W605
|
||||
exclude = setup.py, docs/source/conf.py
|
||||
|
||||
[yapf]
|
||||
based_on_style = google
|
||||
|
||||
@@ -23,6 +23,7 @@ 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
|
||||
@@ -125,8 +126,8 @@ from .version import __version__ # noqa: F401
|
||||
__author__ = 'devs@python-telegram-bot.org'
|
||||
|
||||
__all__ = [
|
||||
'Audio', 'Bot', 'Chat', 'ChatMember', 'ChatAction', 'ChosenInlineResult', 'CallbackQuery',
|
||||
'Contact', 'Document', 'File', 'ForceReply', 'InlineKeyboardButton',
|
||||
'Audio', 'Bot', 'Chat', 'ChatMember', 'ChatPermissions', 'ChatAction', 'ChosenInlineResult',
|
||||
'CallbackQuery', 'Contact', 'Document', 'File', 'ForceReply', 'InlineKeyboardButton',
|
||||
'InlineKeyboardMarkup', 'InlineQuery', 'InlineQueryResult', 'InlineQueryResult',
|
||||
'InlineQueryResultArticle', 'InlineQueryResultAudio', 'InlineQueryResultCachedAudio',
|
||||
'InlineQueryResultCachedDocument', 'InlineQueryResultCachedGif',
|
||||
|
||||
+49
-26
@@ -1514,7 +1514,8 @@ class Bot(TelegramObject):
|
||||
calling get_file again.
|
||||
|
||||
Args:
|
||||
file_id (:obj:`str` | :class:`telegram.Audio` | :class:`telegram.Document` | \
|
||||
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`):
|
||||
@@ -2685,14 +2686,18 @@ class Bot(TelegramObject):
|
||||
return result
|
||||
|
||||
@log
|
||||
def restrict_chat_member(self, chat_id, user_id, until_date=None, can_send_messages=None,
|
||||
can_send_media_messages=None, can_send_other_messages=None,
|
||||
can_add_web_page_previews=None, timeout=None, **kwargs):
|
||||
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).
|
||||
@@ -2701,15 +2706,7 @@ class Bot(TelegramObject):
|
||||
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.
|
||||
can_send_messages (:obj:`bool`, optional): Pass True, if the user can send text
|
||||
messages, contacts, locations and venues.
|
||||
can_send_media_messages (:obj:`bool`, optional): Pass True, if the user can send
|
||||
audios, documents, photos, videos, video notes and voice notes, implies
|
||||
can_send_messages.
|
||||
can_send_other_messages (:obj:`bool`, optional): Pass 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): Pass True, if the user may add
|
||||
web page previews to their messages, implies can_send_media_messages.
|
||||
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).
|
||||
@@ -2720,24 +2717,15 @@ class Bot(TelegramObject):
|
||||
|
||||
Raises:
|
||||
:class:`telegram.TelegramError`
|
||||
|
||||
"""
|
||||
url = '{0}/restrictChatMember'.format(self.base_url)
|
||||
|
||||
data = {'chat_id': chat_id, 'user_id': user_id}
|
||||
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
|
||||
if can_send_messages is not None:
|
||||
data['can_send_messages'] = can_send_messages
|
||||
if can_send_media_messages is not None:
|
||||
data['can_send_media_messages'] = can_send_media_messages
|
||||
if can_send_other_messages is not None:
|
||||
data['can_send_other_messages'] = can_send_other_messages
|
||||
if can_add_web_page_previews is not None:
|
||||
data['can_add_web_page_previews'] = can_add_web_page_previews
|
||||
data.update(kwargs)
|
||||
|
||||
result = self._request.post(url, data, timeout=timeout)
|
||||
@@ -2815,6 +2803,38 @@ class Bot(TelegramObject):
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -2958,8 +2978,9 @@ class Bot(TelegramObject):
|
||||
@log
|
||||
def set_chat_description(self, chat_id, description, timeout=None, **kwargs):
|
||||
"""
|
||||
Use this method to change the description of 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.
|
||||
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
|
||||
@@ -3418,7 +3439,7 @@ class Bot(TelegramObject):
|
||||
return Poll.de_json(result, self)
|
||||
|
||||
def to_dict(self):
|
||||
data = {'id': self.id, 'username': self.username, 'first_name': self.username}
|
||||
data = {'id': self.id, 'username': self.username, 'first_name': self.first_name}
|
||||
|
||||
if self.last_name:
|
||||
data['last_name'] = self.last_name
|
||||
@@ -3526,6 +3547,8 @@ class Bot(TelegramObject):
|
||||
"""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
|
||||
|
||||
+23
-7
@@ -20,6 +20,7 @@
|
||||
"""This module contains an object that represents a Telegram Chat."""
|
||||
|
||||
from telegram import TelegramObject, ChatPhoto
|
||||
from .chatpermissions import ChatPermissions
|
||||
|
||||
|
||||
class Chat(TelegramObject):
|
||||
@@ -32,12 +33,13 @@ class Chat(TelegramObject):
|
||||
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.
|
||||
all_members_are_administrators (:obj:`bool`): Optional.
|
||||
photo (:class:`telegram.ChatPhoto`): Optional. Chat photo.
|
||||
description (:obj:`str`): Optional. Description, for supergroups and channel chats.
|
||||
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.
|
||||
@@ -54,15 +56,15 @@ class Chat(TelegramObject):
|
||||
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.
|
||||
all_members_are_administrators (:obj:`bool`, optional): True if a group has `All Members
|
||||
Are Admins` enabled.
|
||||
photo (:class:`telegram.ChatPhoto`, optional): Chat photo. Returned only in getChat.
|
||||
description (:obj:`str`, optional): Description, for supergroups and channel chats.
|
||||
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.
|
||||
@@ -88,12 +90,12 @@ class Chat(TelegramObject):
|
||||
username=None,
|
||||
first_name=None,
|
||||
last_name=None,
|
||||
all_members_are_administrators=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):
|
||||
@@ -105,11 +107,13 @@ class Chat(TelegramObject):
|
||||
self.username = username
|
||||
self.first_name = first_name
|
||||
self.last_name = last_name
|
||||
self.all_members_are_administrators = all_members_are_administrators
|
||||
# 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
|
||||
|
||||
@@ -132,6 +136,7 @@ class Chat(TelegramObject):
|
||||
data['photo'] = ChatPhoto.de_json(data.get('photo'), bot)
|
||||
from telegram import Message
|
||||
data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot)
|
||||
data['permissions'] = ChatPermissions.de_json(data.get('permissions'), bot)
|
||||
|
||||
return cls(bot=bot, **data)
|
||||
|
||||
@@ -221,6 +226,17 @@ class Chat(TelegramObject):
|
||||
"""
|
||||
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::
|
||||
|
||||
|
||||
+16
-12
@@ -32,18 +32,17 @@ class ChatMember(TelegramObject):
|
||||
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 administrator can change the chat title,
|
||||
photo and other settings.
|
||||
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 administrator can invite new users to the
|
||||
chat.
|
||||
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 administrator can pin messages.
|
||||
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
|
||||
@@ -52,6 +51,8 @@ class ChatMember(TelegramObject):
|
||||
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
|
||||
@@ -65,20 +66,20 @@ class ChatMember(TelegramObject):
|
||||
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 only. True, if the administrator
|
||||
can change the chat title, photo and other settings.
|
||||
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 only. True, if the administrator
|
||||
can invite new users to the chat.
|
||||
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 only. True, if the administrator
|
||||
can pin messages, supergroups only.
|
||||
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
|
||||
@@ -90,6 +91,8 @@ class ChatMember(TelegramObject):
|
||||
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
|
||||
@@ -114,7 +117,7 @@ class ChatMember(TelegramObject):
|
||||
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_other_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
|
||||
@@ -131,6 +134,7 @@ class ChatMember(TelegramObject):
|
||||
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
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2018
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have 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)
|
||||
@@ -17,7 +17,8 @@
|
||||
"""Constants in the Telegram network.
|
||||
|
||||
The following constants were extracted from the
|
||||
`Telegram Bots FAQ <https://core.telegram.org/bots/faq>`_.
|
||||
`Telegram Bots FAQ <https://core.telegram.org/bots/faq>`_ and
|
||||
`Telegram Bots API <https://core.telegram.org/bots/api>`_.
|
||||
|
||||
Attributes:
|
||||
MAX_MESSAGE_LENGTH (:obj:`int`): 4096
|
||||
@@ -25,6 +26,7 @@ Attributes:
|
||||
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
|
||||
@@ -47,6 +49,7 @@ MAX_CAPTION_LENGTH = 1024
|
||||
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
|
||||
|
||||
@@ -73,13 +73,31 @@ class CallbackContext(object):
|
||||
raise ValueError('CallbackContext should not be used with a non context aware '
|
||||
'dispatcher!')
|
||||
self._dispatcher = dispatcher
|
||||
self.chat_data = None
|
||||
self.user_data = None
|
||||
self._chat_data = None
|
||||
self._user_data = None
|
||||
self.args = None
|
||||
self.matches = None
|
||||
self.error = None
|
||||
self.job = None
|
||||
|
||||
@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)
|
||||
@@ -94,9 +112,9 @@ class CallbackContext(object):
|
||||
user = update.effective_user
|
||||
|
||||
if chat:
|
||||
self.chat_data = dispatcher.chat_data[chat.id]
|
||||
self._chat_data = dispatcher.chat_data[chat.id]
|
||||
if user:
|
||||
self.user_data = dispatcher.user_data[user.id]
|
||||
self._user_data = dispatcher.user_data[user.id]
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
from threading import Lock
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler,
|
||||
@@ -37,8 +38,7 @@ class _ConversationTimeoutContext(object):
|
||||
class ConversationHandler(Handler):
|
||||
"""
|
||||
A handler to hold a conversation with a single user by managing four collections of other
|
||||
handlers. Note that neither posts in Telegram Channels, nor group interactions with multiple
|
||||
users are managed by instances of this class.
|
||||
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
|
||||
@@ -57,11 +57,6 @@ class ConversationHandler(Handler):
|
||||
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.
|
||||
|
||||
The fourth, optional collection of handlers, a ``list`` named :attr:`timed_out_behavior` is
|
||||
used if the wait for ``run_async`` takes longer than defined in :attr:`run_async_timeout`.
|
||||
For example, you can let the user know that they should wait for a bit before they can
|
||||
continue.
|
||||
|
||||
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,
|
||||
@@ -69,6 +64,20 @@ class ConversationHandler(Handler):
|
||||
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.
|
||||
@@ -86,13 +95,16 @@ class ConversationHandler(Handler):
|
||||
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
|
||||
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
|
||||
@@ -124,6 +136,9 @@ class ConversationHandler(Handler):
|
||||
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
|
||||
@@ -147,7 +162,8 @@ class ConversationHandler(Handler):
|
||||
per_message=False,
|
||||
conversation_timeout=None,
|
||||
name=None,
|
||||
persistent=False):
|
||||
persistent=False,
|
||||
map_to_parent=None):
|
||||
|
||||
self.entry_points = entry_points
|
||||
self.states = states
|
||||
@@ -165,9 +181,12 @@ class ConversationHandler(Handler):
|
||||
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__)
|
||||
|
||||
@@ -245,7 +264,8 @@ class ConversationHandler(Handler):
|
||||
return None
|
||||
|
||||
key = self._get_key(update)
|
||||
state = self.conversations.get(key)
|
||||
with self._conversations_lock:
|
||||
state = self.conversations.get(key)
|
||||
|
||||
# Resolve promises
|
||||
if isinstance(state, tuple) and len(state) == 2 and isinstance(state[1], Promise):
|
||||
@@ -264,7 +284,8 @@ class ConversationHandler(Handler):
|
||||
if res is None and old_state is None:
|
||||
res = self.END
|
||||
self.update_state(res, key)
|
||||
state = self.conversations.get(key)
|
||||
with self._conversations_lock:
|
||||
state = self.conversations.get(key)
|
||||
else:
|
||||
handlers = self.states.get(self.WAITING, [])
|
||||
for handler in handlers:
|
||||
@@ -323,50 +344,72 @@ class ConversationHandler(Handler):
|
||||
|
||||
"""
|
||||
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)
|
||||
timeout_job = self.timeout_jobs.pop(conversation_key, None)
|
||||
|
||||
if timeout_job is not None:
|
||||
timeout_job.schedule_removal()
|
||||
if self.conversation_timeout and new_state != self.END:
|
||||
self.timeout_jobs[conversation_key] = dispatcher.job_queue.run_once(
|
||||
self._trigger_timeout, self.conversation_timeout,
|
||||
context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
|
||||
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))
|
||||
|
||||
self.update_state(new_state, conversation_key)
|
||||
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:
|
||||
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)
|
||||
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):
|
||||
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))
|
||||
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:
|
||||
self.conversations[key] = new_state
|
||||
if self.persistent:
|
||||
self.persistence.update_conversation(self.name, key, new_state)
|
||||
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):
|
||||
context = context.job.context
|
||||
else:
|
||||
context = job.context
|
||||
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]
|
||||
|
||||
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)
|
||||
handler.handle_update(context.update, context.dispatcher, check, callback_context)
|
||||
self.update_state(self.END, context.conversation_key)
|
||||
|
||||
+78
-2
@@ -22,7 +22,7 @@ import re
|
||||
|
||||
from future.utils import string_types
|
||||
|
||||
from telegram import Chat
|
||||
from telegram import Chat, Update
|
||||
|
||||
__all__ = ['Filters', 'BaseFilter', 'InvertedFilter', 'MergedFilter']
|
||||
|
||||
@@ -236,11 +236,87 @@ class Filters(object):
|
||||
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 and not message.text.startswith('/'):
|
||||
return message.text in self.iterable
|
||||
return False
|
||||
|
||||
def __call__(self, update):
|
||||
if isinstance(update, Update):
|
||||
if self.update_filter:
|
||||
return self.filter(update)
|
||||
else:
|
||||
return self.filter(update.effective_message)
|
||||
else:
|
||||
return self._TextIterable(update)
|
||||
|
||||
def filter(self, message):
|
||||
return bool(message.text and not message.text.startswith('/'))
|
||||
|
||||
text = _Text()
|
||||
"""Text Messages."""
|
||||
"""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):
|
||||
if self.update_filter:
|
||||
return self.filter(update)
|
||||
else:
|
||||
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'
|
||||
|
||||
@@ -151,14 +151,11 @@ class Handler(object):
|
||||
optional_args['update_queue'] = dispatcher.update_queue
|
||||
if self.pass_job_queue:
|
||||
optional_args['job_queue'] = dispatcher.job_queue
|
||||
if self.pass_user_data or self.pass_chat_data:
|
||||
chat = update.effective_chat
|
||||
if self.pass_user_data:
|
||||
user = update.effective_user
|
||||
|
||||
if self.pass_user_data:
|
||||
optional_args['user_data'] = dispatcher.user_data[user.id if user else None]
|
||||
|
||||
if self.pass_chat_data:
|
||||
optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None]
|
||||
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
|
||||
|
||||
+50
-35
@@ -29,6 +29,7 @@ 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):
|
||||
@@ -42,7 +43,8 @@ class JobQueue(object):
|
||||
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 set_dispatcher instead.
|
||||
DEPRECATED: Use :attr:`set_dispatcher` instead.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, bot=None):
|
||||
@@ -68,32 +70,38 @@ class JobQueue(object):
|
||||
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, next_t=None, last_t=None):
|
||||
if next_t is None:
|
||||
next_t = job.interval
|
||||
if next_t is None:
|
||||
raise ValueError('next_t is None')
|
||||
def _put(self, job, time_spec=None, previous_t=None):
|
||||
"""
|
||||
Enqueues the job, scheduling its next run at the correct time.
|
||||
|
||||
if isinstance(next_t, datetime.datetime):
|
||||
next_t = (next_t - datetime.datetime.now()).total_seconds()
|
||||
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).
|
||||
|
||||
elif isinstance(next_t, datetime.time):
|
||||
next_datetime = datetime.datetime.combine(datetime.date.today(), next_t)
|
||||
|
||||
if datetime.datetime.now().time() > next_t:
|
||||
next_datetime += datetime.timedelta(days=1)
|
||||
|
||||
next_t = (next_datetime - datetime.datetime.now()).total_seconds()
|
||||
|
||||
elif isinstance(next_t, datetime.timedelta):
|
||||
next_t = next_t.total_seconds()
|
||||
|
||||
next_t += last_t or time.time()
|
||||
|
||||
self.logger.debug('Putting job %s with t=%f', job.name, next_t)
|
||||
"""
|
||||
# get time at which to run:
|
||||
time_spec = time_spec or 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=%f', job.name, time_spec)
|
||||
self._queue.put((next_t, job))
|
||||
|
||||
# Wake up the loop if this job should be executed next
|
||||
@@ -133,7 +141,7 @@ class JobQueue(object):
|
||||
|
||||
"""
|
||||
job = Job(callback, repeat=False, context=context, name=name, job_queue=self)
|
||||
self._put(job, next_t=when)
|
||||
self._put(job, time_spec=when)
|
||||
return job
|
||||
|
||||
def run_repeating(self, callback, interval, first=None, context=None, name=None):
|
||||
@@ -184,7 +192,7 @@ class JobQueue(object):
|
||||
context=context,
|
||||
name=name,
|
||||
job_queue=self)
|
||||
self._put(job, next_t=first)
|
||||
self._put(job, time_spec=first)
|
||||
return job
|
||||
|
||||
def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None):
|
||||
@@ -195,7 +203,8 @@ class JobQueue(object):
|
||||
job. It should take ``bot, job`` as parameters, where ``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.
|
||||
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.
|
||||
@@ -217,10 +226,11 @@ class JobQueue(object):
|
||||
interval=datetime.timedelta(days=1),
|
||||
repeat=True,
|
||||
days=days,
|
||||
tzinfo=time.tzinfo,
|
||||
context=context,
|
||||
name=name,
|
||||
job_queue=self)
|
||||
self._put(job, next_t=time)
|
||||
self._put(job, time_spec=time)
|
||||
return job
|
||||
|
||||
def _set_next_peek(self, t):
|
||||
@@ -264,7 +274,7 @@ class JobQueue(object):
|
||||
|
||||
if job.enabled:
|
||||
try:
|
||||
current_week_day = datetime.datetime.now().weekday()
|
||||
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)
|
||||
@@ -276,7 +286,7 @@ class JobQueue(object):
|
||||
self.logger.debug('Skipping disabled job %s', job.name)
|
||||
|
||||
if job.repeat and not job.removed:
|
||||
self._put(job, last_t=t)
|
||||
self._put(job, previous_t=t)
|
||||
else:
|
||||
self.logger.debug('Dropping non-repeating or removed job %s', job.name)
|
||||
|
||||
@@ -350,10 +360,11 @@ class Job(object):
|
||||
It should take ``bot, job`` as parameters, where ``job`` is the
|
||||
:class:`telegram.ext.Job` instance. It can be used to access it's :attr:`context`
|
||||
or change it to a repeating job.
|
||||
interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`, optional): The interval in
|
||||
which the job will run. 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:`next_t` when you put the job into the job queue.
|
||||
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
|
||||
@@ -363,7 +374,9 @@ class Job(object):
|
||||
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,
|
||||
@@ -373,19 +386,21 @@ class Job(object):
|
||||
context=None,
|
||||
days=Days.EVERY_DAY,
|
||||
name=None,
|
||||
job_queue=None):
|
||||
job_queue=None,
|
||||
tzinfo=_UTC):
|
||||
|
||||
self.callback = callback
|
||||
self.context = context
|
||||
self.name = name or callback.__name__
|
||||
|
||||
self._repeat = repeat
|
||||
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
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class Animation(TelegramObject):
|
||||
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.
|
||||
@@ -44,6 +45,8 @@ class Animation(TelegramObject):
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
@@ -56,16 +59,19 @@ class Animation(TelegramObject):
|
||||
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,)
|
||||
|
||||
@@ -78,4 +84,22 @@ class Animation(TelegramObject):
|
||||
|
||||
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
|
||||
|
||||
return cls(**data)
|
||||
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)
|
||||
|
||||
@@ -17,9 +17,6 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an object that represents a Telegram ChatPhoto."""
|
||||
|
||||
# TODO: add direct download shortcuts.
|
||||
|
||||
from telegram import TelegramObject
|
||||
|
||||
|
||||
@@ -27,14 +24,14 @@ class ChatPhoto(TelegramObject):
|
||||
"""This object represents a chat photo.
|
||||
|
||||
Attributes:
|
||||
small_file_id (:obj:`str`): Unique file identifier of small (160x160) chat photo.
|
||||
big_file_id (:obj:`str`): Unique file identifier of big (640x640) chat photo.
|
||||
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`): Unique file identifier of small (160x160) chat photo. This
|
||||
file_id can be used only for photo download.
|
||||
big_file_id (:obj:`str`): Unique file identifier of big (640x640) chat photo. This file_id
|
||||
can be used only for photo download.
|
||||
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.
|
||||
|
||||
@@ -43,6 +40,9 @@ class ChatPhoto(TelegramObject):
|
||||
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):
|
||||
@@ -50,3 +50,41 @@ class ChatPhoto(TelegramObject):
|
||||
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)
|
||||
|
||||
@@ -28,6 +28,7 @@ class Sticker(TelegramObject):
|
||||
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.
|
||||
@@ -41,6 +42,7 @@ class Sticker(TelegramObject):
|
||||
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
|
||||
@@ -58,6 +60,7 @@ class Sticker(TelegramObject):
|
||||
file_id,
|
||||
width,
|
||||
height,
|
||||
is_animated,
|
||||
thumb=None,
|
||||
emoji=None,
|
||||
file_size=None,
|
||||
@@ -69,6 +72,7 @@ class Sticker(TelegramObject):
|
||||
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
|
||||
@@ -123,20 +127,23 @@ class StickerSet(TelegramObject):
|
||||
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, contains_masks, stickers, bot=None, **kwargs):
|
||||
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
|
||||
|
||||
|
||||
@@ -86,20 +86,13 @@ class InlineKeyboardButton(TelegramObject):
|
||||
self.text = text
|
||||
|
||||
# Optionals
|
||||
if url:
|
||||
self.url = url
|
||||
if login_url:
|
||||
self.login_url = login_url
|
||||
if callback_data:
|
||||
self.callback_data = callback_data
|
||||
if switch_inline_query:
|
||||
self.switch_inline_query = switch_inline_query
|
||||
if switch_inline_query_current_chat:
|
||||
self.switch_inline_query_current_chat = switch_inline_query_current_chat
|
||||
if callback_game:
|
||||
self.callback_game = callback_game
|
||||
if pay:
|
||||
self.pay = pay
|
||||
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):
|
||||
|
||||
@@ -77,17 +77,10 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
self.input_message_content = input_message_content
|
||||
|
||||
# Optional
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if url:
|
||||
self.url = url
|
||||
if hide_url:
|
||||
self.hide_url = hide_url
|
||||
if description:
|
||||
self.description = description
|
||||
if thumb_url:
|
||||
self.thumb_url = thumb_url
|
||||
if thumb_width:
|
||||
self.thumb_width = thumb_width
|
||||
if thumb_height:
|
||||
self.thumb_height = thumb_height
|
||||
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
|
||||
|
||||
@@ -79,15 +79,9 @@ class InlineQueryResultAudio(InlineQueryResult):
|
||||
self.title = title
|
||||
|
||||
# Optionals
|
||||
if performer:
|
||||
self.performer = performer
|
||||
if audio_duration:
|
||||
self.audio_duration = audio_duration
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
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
|
||||
|
||||
@@ -68,11 +68,7 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
|
||||
self.audio_file_id = audio_file_id
|
||||
|
||||
# Optionals
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
self.caption = caption
|
||||
self.parse_mode = parse_mode
|
||||
self.reply_markup = reply_markup
|
||||
self.input_message_content = input_message_content
|
||||
|
||||
@@ -75,13 +75,8 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
|
||||
self.document_file_id = document_file_id
|
||||
|
||||
# Optionals
|
||||
if description:
|
||||
self.description = description
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
self.description = description
|
||||
self.caption = caption
|
||||
self.parse_mode = parse_mode
|
||||
self.reply_markup = reply_markup
|
||||
self.input_message_content = input_message_content
|
||||
|
||||
@@ -72,13 +72,8 @@ class InlineQueryResultCachedGif(InlineQueryResult):
|
||||
self.gif_file_id = gif_file_id
|
||||
|
||||
# Optionals
|
||||
if title:
|
||||
self.title = title
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
self.title = title
|
||||
self.caption = caption
|
||||
self.parse_mode = parse_mode
|
||||
self.reply_markup = reply_markup
|
||||
self.input_message_content = input_message_content
|
||||
|
||||
@@ -72,13 +72,8 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
|
||||
self.mpeg4_file_id = mpeg4_file_id
|
||||
|
||||
# Optionals
|
||||
if title:
|
||||
self.title = title
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
self.title = title
|
||||
self.caption = caption
|
||||
self.parse_mode = parse_mode
|
||||
self.reply_markup = reply_markup
|
||||
self.input_message_content = input_message_content
|
||||
|
||||
@@ -75,15 +75,9 @@ class InlineQueryResultCachedPhoto(InlineQueryResult):
|
||||
self.photo_file_id = photo_file_id
|
||||
|
||||
# Optionals
|
||||
if title:
|
||||
self.title = title
|
||||
if description:
|
||||
self.description = description
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
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
|
||||
|
||||
@@ -58,7 +58,5 @@ class InlineQueryResultCachedSticker(InlineQueryResult):
|
||||
self.sticker_file_id = sticker_file_id
|
||||
|
||||
# Optionals
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
self.reply_markup = reply_markup
|
||||
self.input_message_content = input_message_content
|
||||
|
||||
@@ -76,13 +76,8 @@ class InlineQueryResultCachedVideo(InlineQueryResult):
|
||||
self.title = title
|
||||
|
||||
# Optionals
|
||||
if description:
|
||||
self.description = description
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
self.description = description
|
||||
self.caption = caption
|
||||
self.parse_mode = parse_mode
|
||||
self.reply_markup = reply_markup
|
||||
self.input_message_content = input_message_content
|
||||
|
||||
@@ -72,11 +72,7 @@ class InlineQueryResultCachedVoice(InlineQueryResult):
|
||||
self.title = title
|
||||
|
||||
# Optionals
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
self.caption = caption
|
||||
self.parse_mode = parse_mode
|
||||
self.reply_markup = reply_markup
|
||||
self.input_message_content = input_message_content
|
||||
|
||||
@@ -79,17 +79,10 @@ class InlineQueryResultContact(InlineQueryResult):
|
||||
self.first_name = first_name
|
||||
|
||||
# Optionals
|
||||
if last_name:
|
||||
self.last_name = last_name
|
||||
if vcard:
|
||||
self.vcard = vcard
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
if thumb_url:
|
||||
self.thumb_url = thumb_url
|
||||
if thumb_width:
|
||||
self.thumb_width = thumb_width
|
||||
if thumb_height:
|
||||
self.thumb_height = thumb_height
|
||||
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
|
||||
|
||||
@@ -91,19 +91,11 @@ class InlineQueryResultDocument(InlineQueryResult):
|
||||
self.mime_type = mime_type
|
||||
|
||||
# Optionals
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if description:
|
||||
self.description = description
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
if thumb_url:
|
||||
self.thumb_url = thumb_url
|
||||
if thumb_width:
|
||||
self.thumb_width = thumb_width
|
||||
if thumb_height:
|
||||
self.thumb_height = thumb_height
|
||||
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
|
||||
|
||||
@@ -46,5 +46,4 @@ class InlineQueryResultGame(InlineQueryResult):
|
||||
self.id = id
|
||||
self.game_short_name = game_short_name
|
||||
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
self.reply_markup = reply_markup
|
||||
|
||||
@@ -85,19 +85,11 @@ class InlineQueryResultGif(InlineQueryResult):
|
||||
self.thumb_url = thumb_url
|
||||
|
||||
# Optionals
|
||||
if gif_width:
|
||||
self.gif_width = gif_width
|
||||
if gif_height:
|
||||
self.gif_height = gif_height
|
||||
if gif_duration:
|
||||
self.gif_duration = gif_duration
|
||||
if title:
|
||||
self.title = title
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
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
|
||||
|
||||
@@ -80,15 +80,9 @@ class InlineQueryResultLocation(InlineQueryResult):
|
||||
self.title = title
|
||||
|
||||
# Optionals
|
||||
if live_period:
|
||||
self.live_period = live_period
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
if thumb_url:
|
||||
self.thumb_url = thumb_url
|
||||
if thumb_width:
|
||||
self.thumb_width = thumb_width
|
||||
if thumb_height:
|
||||
self.thumb_height = thumb_height
|
||||
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
|
||||
|
||||
@@ -86,19 +86,11 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
|
||||
self.thumb_url = thumb_url
|
||||
|
||||
# Optional
|
||||
if mpeg4_width:
|
||||
self.mpeg4_width = mpeg4_width
|
||||
if mpeg4_height:
|
||||
self.mpeg4_height = mpeg4_height
|
||||
if mpeg4_duration:
|
||||
self.mpeg4_duration = mpeg4_duration
|
||||
if title:
|
||||
self.title = title
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
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
|
||||
|
||||
@@ -86,19 +86,11 @@ class InlineQueryResultPhoto(InlineQueryResult):
|
||||
self.thumb_url = thumb_url
|
||||
|
||||
# Optionals
|
||||
if photo_width:
|
||||
self.photo_width = int(photo_width)
|
||||
if photo_height:
|
||||
self.photo_height = int(photo_height)
|
||||
if title:
|
||||
self.title = title
|
||||
if description:
|
||||
self.description = description
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
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
|
||||
|
||||
@@ -90,17 +90,10 @@ class InlineQueryResultVenue(InlineQueryResult):
|
||||
self.address = address
|
||||
|
||||
# Optional
|
||||
if foursquare_id:
|
||||
self.foursquare_id = foursquare_id
|
||||
if foursquare_type:
|
||||
self.foursquare_type = foursquare_type
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
if thumb_url:
|
||||
self.thumb_url = thumb_url
|
||||
if thumb_width:
|
||||
self.thumb_width = thumb_width
|
||||
if thumb_height:
|
||||
self.thumb_height = thumb_height
|
||||
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
|
||||
|
||||
@@ -94,19 +94,11 @@ class InlineQueryResultVideo(InlineQueryResult):
|
||||
self.title = title
|
||||
|
||||
# Optional
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if video_width:
|
||||
self.video_width = video_width
|
||||
if video_height:
|
||||
self.video_height = video_height
|
||||
if video_duration:
|
||||
self.video_duration = video_duration
|
||||
if description:
|
||||
self.description = description
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
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
|
||||
|
||||
@@ -77,13 +77,8 @@ class InlineQueryResultVoice(InlineQueryResult):
|
||||
self.title = title
|
||||
|
||||
# Optional
|
||||
if voice_duration:
|
||||
self.voice_duration = voice_duration
|
||||
if caption:
|
||||
self.caption = caption
|
||||
if parse_mode:
|
||||
self.parse_mode = parse_mode
|
||||
if reply_markup:
|
||||
self.reply_markup = reply_markup
|
||||
if input_message_content:
|
||||
self.input_message_content = input_message_content
|
||||
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
|
||||
|
||||
@@ -26,7 +26,8 @@ class LoginUrl(TelegramObject):
|
||||
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
|
||||
|
||||
Sample bot: `@discussbot <https://t.me/dicussbot>`_
|
||||
|
||||
Attributes:
|
||||
url (:obj:`str`): An HTTP URL to be opened with user authorization data.
|
||||
@@ -53,13 +54,11 @@ class LoginUrl(TelegramObject):
|
||||
"""
|
||||
|
||||
def __init__(self, url, forward_text=None, bot_username=None, request_write_access=None):
|
||||
# Required
|
||||
self.url = url
|
||||
|
||||
if forward_text:
|
||||
self.forward_text = forward_text
|
||||
if bot_username:
|
||||
self.bot_username = bot_username
|
||||
if request_write_access:
|
||||
self.request_write_access = request_write_access
|
||||
# Optional
|
||||
self.forward_text = forward_text
|
||||
self.bot_username = bot_username
|
||||
self.request_write_access = request_write_access
|
||||
|
||||
self._id_attrs = (self.url,)
|
||||
|
||||
+32
-11
@@ -338,10 +338,20 @@ class Message(TelegramObject):
|
||||
|
||||
@property
|
||||
def link(self):
|
||||
""":obj:`str`: Convenience property. If the chat of the message is a supergroup or a
|
||||
channel and has a :attr:`Chat.username`, returns a t.me link of the message."""
|
||||
if self.chat.type in (Chat.SUPERGROUP, Chat.CHANNEL) and self.chat.username:
|
||||
return "https://t.me/{}/{}".format(self.chat.username, self.message_id)
|
||||
""":obj:`str`: Convenience property. If the chat of the message is not
|
||||
a private chat, returns a t.me link of the message."""
|
||||
if self.chat.type != Chat.PRIVATE:
|
||||
if self.chat.username:
|
||||
to_link = self.chat.username
|
||||
else:
|
||||
if self.chat.type != Chat.GROUP:
|
||||
# Get rid of leading -100 for supergroups
|
||||
id_to_link = str(self.chat.id)[4:]
|
||||
else:
|
||||
# Get rid of leading minus for regular groups
|
||||
id_to_link = str(self.chat.id)[1:]
|
||||
to_link = "c/{}".format(id_to_link)
|
||||
return "https://t.me/{}/{}".format(to_link, self.message_id)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
@@ -473,6 +483,9 @@ class Message(TelegramObject):
|
||||
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)
|
||||
@@ -490,6 +503,9 @@ class Message(TelegramObject):
|
||||
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
|
||||
@@ -510,6 +526,9 @@ class Message(TelegramObject):
|
||||
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
|
||||
@@ -742,13 +761,14 @@ class Message(TelegramObject):
|
||||
self._quote(kwargs)
|
||||
return self.bot.send_poll(self.chat_id, *args, **kwargs)
|
||||
|
||||
def forward(self, chat_id, disable_notification=False):
|
||||
def forward(self, chat_id, *args, **kwargs):
|
||||
"""Shortcut for::
|
||||
|
||||
bot.forward_message(chat_id=chat_id,
|
||||
from_chat_id=update.message.chat_id,
|
||||
disable_notification=disable_notification,
|
||||
message_id=update.message.message_id)
|
||||
message_id=update.message.message_id,
|
||||
*args,
|
||||
**kwargs)
|
||||
|
||||
Returns:
|
||||
:class:`telegram.Message`: On success, instance representing the message forwarded.
|
||||
@@ -757,8 +777,9 @@ class Message(TelegramObject):
|
||||
return self.bot.forward_message(
|
||||
chat_id=chat_id,
|
||||
from_chat_id=self.chat_id,
|
||||
disable_notification=disable_notification,
|
||||
message_id=self.message_id)
|
||||
message_id=self.message_id,
|
||||
*args,
|
||||
**kwargs)
|
||||
|
||||
def edit_text(self, *args, **kwargs):
|
||||
"""Shortcut for::
|
||||
@@ -865,7 +886,7 @@ class Message(TelegramObject):
|
||||
|
||||
Args:
|
||||
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
|
||||
be an entity that belongs to this message.
|
||||
be an entity that belongs to this message.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: The text of the given entity
|
||||
@@ -890,7 +911,7 @@ class Message(TelegramObject):
|
||||
|
||||
Args:
|
||||
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
|
||||
be an entity that belongs to this message.
|
||||
be an entity that belongs to this message.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: The text of the given entity
|
||||
|
||||
+179
-26
@@ -17,7 +17,12 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains helper functions."""
|
||||
|
||||
import datetime as dtm # dtm = "DateTime Module"
|
||||
import time
|
||||
|
||||
from collections import defaultdict
|
||||
from numbers import Number
|
||||
|
||||
try:
|
||||
import ujson as json
|
||||
@@ -27,7 +32,6 @@ from html import escape
|
||||
|
||||
import re
|
||||
import signal
|
||||
from datetime import datetime
|
||||
|
||||
# From https://stackoverflow.com/questions/2549939/get-signal-names-from-numbers-in-python
|
||||
_signames = {v: k
|
||||
@@ -40,54 +44,154 @@ def get_signal_name(signum):
|
||||
return _signames[signum]
|
||||
|
||||
|
||||
# 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.
|
||||
if hasattr(datetime, 'timestamp'):
|
||||
# Python 3.3+
|
||||
def _timestamp(dt_obj):
|
||||
return dt_obj.timestamp()
|
||||
else:
|
||||
# Python < 3.3 (incl 2.7)
|
||||
from time import mktime
|
||||
|
||||
def _timestamp(dt_obj):
|
||||
return mktime(dt_obj.timetuple())
|
||||
|
||||
|
||||
def escape_markdown(text):
|
||||
"""Helper function to escape telegram markup symbols."""
|
||||
escape_chars = '\*_`\['
|
||||
return re.sub(r'([%s])' % escape_chars, r'\\\1', text)
|
||||
|
||||
|
||||
def to_timestamp(dt_obj):
|
||||
# -------- 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 :module:`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:
|
||||
dt_obj (:class:`datetime.datetime`):
|
||||
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:
|
||||
int:
|
||||
(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 not dt_obj:
|
||||
return None
|
||||
|
||||
return int(_timestamp(dt_obj))
|
||||
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):
|
||||
unixtime (int): integer POSIX timestamp
|
||||
|
||||
Returns:
|
||||
datetime.datetime:
|
||||
|
||||
equivalent :obj:`datetime.datetime` value in naive UTC if ``timestamp`` is not
|
||||
``None``; else ``None``
|
||||
"""
|
||||
if not unixtime:
|
||||
if unixtime is None:
|
||||
return None
|
||||
|
||||
return datetime.utcfromtimestamp(unixtime)
|
||||
return dtm.datetime.utcfromtimestamp(unixtime)
|
||||
|
||||
# -------- end --------
|
||||
|
||||
|
||||
def mention_html(user_id, name):
|
||||
@@ -147,6 +251,55 @@ def effective_message_type(entity):
|
||||
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 enocde_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.
|
||||
|
||||
Vendored
+1
-1
Submodule telegram/vendor/ptb_urllib3 updated: d2403a79fc...1954df0395
+1
-1
@@ -17,4 +17,4 @@
|
||||
# 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.0.0'
|
||||
__version__ = '12.3.0'
|
||||
|
||||
+25
-16
@@ -17,11 +17,10 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""Provide a bot to tests"""
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
|
||||
from platform import python_implementation
|
||||
|
||||
# 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
|
||||
@@ -32,32 +31,42 @@ FALLBACKS = [
|
||||
'payment_provider_token': '284685063:TEST:NjQ0NjZlNzI5YjJi',
|
||||
'chat_id': '675666224',
|
||||
'super_group_id': '-1001493296829',
|
||||
'channel_id': '@pythontelegrambottests'
|
||||
'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'
|
||||
'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):
|
||||
full_name = '{0}_{1}_{2[0]}{2[1]}'.format(name, python_implementation(),
|
||||
sys.version_info).upper()
|
||||
# First try full_names such as
|
||||
# TOKEN_CPYTHON_33
|
||||
# CHAT_ID_PYPY_27
|
||||
val = os.getenv(full_name)
|
||||
if val:
|
||||
return val
|
||||
# Then try short names
|
||||
# TOKEN
|
||||
# CHAT_ID
|
||||
# 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
|
||||
|
||||
|
||||
+127
-3
@@ -16,8 +16,10 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from queue import Queue
|
||||
from threading import Thread, Event
|
||||
@@ -25,8 +27,11 @@ from time import sleep
|
||||
|
||||
import pytest
|
||||
|
||||
from telegram import Bot
|
||||
from telegram.ext import Dispatcher, JobQueue, Updater
|
||||
from telegram import (Bot, Message, User, Chat, MessageEntity, Update,
|
||||
InlineQuery, CallbackQuery, ShippingQuery, PreCheckoutQuery,
|
||||
ChosenInlineResult)
|
||||
from telegram.ext import Dispatcher, JobQueue, Updater, BaseFilter
|
||||
from telegram.utils.helpers import _UtcOffsetTimezone
|
||||
from tests.bots import get_bot
|
||||
|
||||
TRAVIS = os.getenv('TRAVIS', False)
|
||||
@@ -34,6 +39,11 @@ 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
|
||||
@@ -46,7 +56,7 @@ def bot_info():
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def bot(bot_info):
|
||||
return Bot(bot_info['token'], private_key=PRIVATE_KEY)
|
||||
return make_bot(bot_info)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@@ -146,3 +156,117 @@ 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):
|
||||
return Bot(bot_info['token'], private_key=PRIVATE_KEY)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2018
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should 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]')
|
||||
+68
-7
@@ -17,10 +17,11 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from flaky import flaky
|
||||
|
||||
from telegram import PhotoSize, Animation, Voice
|
||||
from telegram import PhotoSize, Animation, Voice, TelegramError
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
@@ -42,6 +43,9 @@ class TestAnimation(object):
|
||||
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
|
||||
@@ -72,20 +76,52 @@ class TestAnimation(object):
|
||||
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 == 320
|
||||
assert message.animation.thumb.height == 180
|
||||
assert message.animation.thumb.width == self.width
|
||||
assert message.animation.thumb.height == self.height
|
||||
|
||||
@flaky(3, 1)
|
||||
def test_resend(self, bot, chat_id, animation):
|
||||
message = bot.send_animation(chat_id, animation.file_id)
|
||||
@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 != ''
|
||||
assert message.animation.file_name == animation.file_name
|
||||
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)
|
||||
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,
|
||||
@@ -117,6 +153,31 @@ class TestAnimation(object):
|
||||
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)
|
||||
|
||||
+35
-10
@@ -19,7 +19,7 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
import datetime as dtm
|
||||
from platform import python_implementation
|
||||
|
||||
import pytest
|
||||
@@ -28,7 +28,7 @@ from future.utils import string_types
|
||||
|
||||
from telegram import (Bot, Update, ChatAction, TelegramError, User, InlineKeyboardMarkup,
|
||||
InlineKeyboardButton, InlineQueryResultArticle, InputTextMessageContent,
|
||||
ShippingOption, LabeledPrice, Poll)
|
||||
ShippingOption, LabeledPrice, ChatPermissions, Poll)
|
||||
from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter
|
||||
from telegram.utils.helpers import from_timestamp
|
||||
|
||||
@@ -50,6 +50,11 @@ def media_message(bot, chat_id):
|
||||
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',
|
||||
@@ -84,6 +89,18 @@ class TestBot(object):
|
||||
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):
|
||||
@@ -91,19 +108,20 @@ class TestBot(object):
|
||||
|
||||
assert message.text == message.text
|
||||
assert message.forward_from.username == message.from_user.username
|
||||
assert isinstance(message.forward_date, datetime)
|
||||
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(TelegramError, match='Message to delete not found'):
|
||||
with pytest.raises(BadRequest):
|
||||
# Considering that the first message is old enough
|
||||
bot.delete_message(chat_id=chat_id, message_id=1)
|
||||
|
||||
@@ -260,6 +278,16 @@ class TestBot(object):
|
||||
|
||||
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
|
||||
@@ -581,16 +609,13 @@ class TestBot(object):
|
||||
|
||||
@flaky(3, 1)
|
||||
@pytest.mark.timeout(10)
|
||||
def test_restrict_chat_member(self, bot, channel_id):
|
||||
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,
|
||||
until_date=datetime.now(),
|
||||
can_send_messages=False,
|
||||
can_send_media_messages=False,
|
||||
can_send_other_messages=False,
|
||||
can_add_web_page_previews=False)
|
||||
chat_permissions,
|
||||
until_date=dtm.datetime.utcnow())
|
||||
|
||||
@flaky(3, 1)
|
||||
@pytest.mark.timeout(10)
|
||||
|
||||
@@ -105,3 +105,15 @@ class TestCallbackContext(object):
|
||||
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"
|
||||
|
||||
+21
-3
@@ -19,7 +19,7 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from telegram import Chat, ChatAction
|
||||
from telegram import Chat, ChatAction, ChatPermissions
|
||||
from telegram import User
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ 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)
|
||||
can_set_sticker_set=TestChat.can_set_sticker_set,
|
||||
permissions=TestChat.permissions)
|
||||
|
||||
|
||||
class TestChat(object):
|
||||
@@ -39,6 +40,11 @@ class TestChat(object):
|
||||
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 = {
|
||||
@@ -48,7 +54,8 @@ class TestChat(object):
|
||||
'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
|
||||
'can_set_sticker_set': self.can_set_sticker_set,
|
||||
'permissions': self.permissions.to_dict()
|
||||
}
|
||||
chat = Chat.de_json(json_dict, bot)
|
||||
|
||||
@@ -59,6 +66,7 @@ class TestChat(object):
|
||||
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_to_dict(self, chat):
|
||||
chat_dict = chat.to_dict()
|
||||
@@ -69,6 +77,7 @@ class TestChat(object):
|
||||
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)
|
||||
@@ -133,6 +142,15 @@ class TestChat(object):
|
||||
monkeypatch.setattr('telegram.Bot.unban_chat_member', test)
|
||||
assert chat.unban_member(42)
|
||||
|
||||
def test_set_permissions(self, monkeypatch, chat):
|
||||
def test(*args, **kwargs):
|
||||
chat_id = args[1] == chat.id
|
||||
permissions = args[2] == self.permissions
|
||||
return chat_id and permissions
|
||||
|
||||
monkeypatch.setattr('telegram.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[1] == chat.id and args[2] == 'test'
|
||||
|
||||
@@ -46,7 +46,7 @@ class TestChatMember(object):
|
||||
assert chat_member.status == self.status
|
||||
|
||||
def test_de_json_all_args(self, bot, user):
|
||||
time = datetime.datetime.now()
|
||||
time = datetime.datetime.utcnow()
|
||||
json_dict = {'user': user.to_dict(),
|
||||
'status': self.status,
|
||||
'until_date': to_timestamp(time),
|
||||
@@ -61,8 +61,9 @@ class TestChatMember(object):
|
||||
'can_promote_members': True,
|
||||
'can_send_messages': False,
|
||||
'can_send_media_messages': True,
|
||||
'can_send_other_messages': False,
|
||||
'can_add_web_page_previews': True}
|
||||
'can_send_polls': False,
|
||||
'can_send_other_messages': True,
|
||||
'can_add_web_page_previews': False}
|
||||
|
||||
chat_member = ChatMember.de_json(json_dict, bot)
|
||||
|
||||
@@ -79,8 +80,9 @@ class TestChatMember(object):
|
||||
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_other_messages is False
|
||||
assert chat_member.can_add_web_page_previews 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()
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2018
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have 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
|
||||
+263
-535
@@ -21,50 +21,30 @@ import re
|
||||
from queue import Queue
|
||||
|
||||
import pytest
|
||||
import itertools
|
||||
from telegram.utils.deprecate import TelegramDeprecationWarning
|
||||
|
||||
from telegram import (Message, Update, Chat, Bot, User, CallbackQuery, InlineQuery,
|
||||
ChosenInlineResult, ShippingQuery, PreCheckoutQuery, MessageEntity)
|
||||
from telegram.ext import CommandHandler, Filters, BaseFilter, CallbackContext, JobQueue, \
|
||||
PrefixHandler
|
||||
|
||||
message = Message(1, User(1, '', False), None, 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 = ('callback_query', 'channel_post', 'edited_channel_post', 'inline_query',
|
||||
'chosen_inline_result', 'shipping_query', 'pre_checkout_query',
|
||||
'callback_query_without_message',)
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture(scope='class', params=params, ids=ids)
|
||||
def false_update(request):
|
||||
return Update(update_id=1, **request.param)
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def message(bot):
|
||||
return Message(message_id=1,
|
||||
from_user=User(id=1, first_name='', is_bot=False),
|
||||
date=None,
|
||||
chat=Chat(id=1, type=''),
|
||||
message='/test',
|
||||
bot=bot,
|
||||
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
|
||||
offset=0,
|
||||
length=len('/test'))])
|
||||
|
||||
|
||||
class TestCommandHandler(object):
|
||||
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("", ""))
|
||||
|
||||
@@ -72,30 +52,33 @@ class TestCommandHandler(object):
|
||||
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 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 make_callback_for(self, pass_keyword):
|
||||
def callback(bot, update, **kwargs):
|
||||
self.test_flag = kwargs.get(keyword, None) 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 ch_callback_args(self, bot, update, args):
|
||||
if update.message.text == '/test':
|
||||
self.test_flag = len(args) == 0
|
||||
elif update.message.text == '/test@{}'.format(bot.username):
|
||||
self.test_flag = len(args) == 0
|
||||
else:
|
||||
self.test_flag = args == ['one', 'two']
|
||||
keyword = pass_keyword[5:]
|
||||
return callback
|
||||
|
||||
def callback_context(self, update, context):
|
||||
self.test_flag = (isinstance(context, CallbackContext)
|
||||
@@ -122,550 +105,295 @@ class TestCommandHandler(object):
|
||||
num = len(context.matches) == 2
|
||||
self.test_flag = types and num
|
||||
|
||||
def test_basic(self, dp, message):
|
||||
handler = CommandHandler('test', self.callback_basic)
|
||||
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)
|
||||
|
||||
message.text = '/test'
|
||||
dp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
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)))
|
||||
|
||||
message.text = '/nottest'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
message.text = 'test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
message.text = 'not /test at start'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
message.entities = []
|
||||
message.text = '/test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
@pytest.mark.parametrize('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, command):
|
||||
def test_invalid_commands(self, cmd):
|
||||
with pytest.raises(ValueError, match='not a valid bot command'):
|
||||
CommandHandler(command, self.callback_basic)
|
||||
CommandHandler(cmd, self.callback_basic)
|
||||
|
||||
def test_command_list(self, message):
|
||||
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)
|
||||
|
||||
message.text = '/test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
|
||||
message.text = '/star'
|
||||
check = handler.check_update(Update(0, message))
|
||||
|
||||
message.text = '/stop'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
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'):
|
||||
CommandHandler('test', self.callback_basic, allow_edited=True)
|
||||
self.make_default_handler(allow_edited=True)
|
||||
|
||||
def test_no_edited(self, message):
|
||||
handler = CommandHandler('test', self.callback_basic)
|
||||
message.text = '/test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
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)
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=message))
|
||||
assert check is not None and check is not False
|
||||
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)
|
||||
|
||||
handler = CommandHandler('test', self.callback_basic,
|
||||
filters=~Filters.update.edited_message)
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
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))
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=message))
|
||||
assert check is None or check is False
|
||||
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_edited_deprecated(self, message):
|
||||
handler = CommandHandler('test', self.callback_basic,
|
||||
allow_edited=False)
|
||||
message.text = '/test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=message))
|
||||
assert check is None or check is False
|
||||
|
||||
handler = CommandHandler('test', self.callback_basic,
|
||||
allow_edited=True)
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
def test_directed_commands(self, message):
|
||||
handler = CommandHandler('test', self.callback_basic)
|
||||
|
||||
message.text = '/test@{}'.format(message.bot.username)
|
||||
message.entities[0].length = len(message.text)
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
message.text = '/test@otherbot'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
def test_with_filter(self, message):
|
||||
handler = CommandHandler('test', self.callback_basic, Filters.group)
|
||||
|
||||
message.chat = Chat(-23, 'group')
|
||||
message.text = '/test'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
message.chat = Chat(23, 'private')
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
|
||||
def test_pass_args(self, dp, message):
|
||||
handler = CommandHandler('test', self.ch_callback_args, pass_args=True)
|
||||
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))
|
||||
|
||||
message.text = '/test'
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
self.test_flag = False
|
||||
message.text = '/test@{}'.format(message.bot.username)
|
||||
message.entities[0].length = len(message.text)
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
self.test_flag = False
|
||||
message.text = '/test@{} one two'.format(message.bot.username)
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
self.test_flag = False
|
||||
message.text = '/test one two'
|
||||
message.entities[0].length = len('/test')
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
def test_newline(self, dp, message):
|
||||
handler = CommandHandler('test', self.callback_basic)
|
||||
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)
|
||||
|
||||
message.text = '/test\nfoobar'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is not None and check is not False
|
||||
|
||||
dp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
|
||||
def test_pass_user_or_chat_data(self, dp, message):
|
||||
handler = CommandHandler('test', self.callback_data_1,
|
||||
pass_user_data=True)
|
||||
@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)
|
||||
|
||||
message.text = '/test'
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
handler = CommandHandler('test', 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 = CommandHandler('test', 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 = CommandHandler('test', self.callback_queue_1,
|
||||
pass_job_queue=True)
|
||||
dp.add_handler(handler)
|
||||
|
||||
message.text = '/test'
|
||||
dp.process_update(Update(0, message=message))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
handler = CommandHandler('test', 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 = CommandHandler('test', 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
|
||||
assert self.response(dp, command_update) == pass_combination.get(pass_keyword, False)
|
||||
|
||||
def test_other_update_types(self, false_update):
|
||||
handler = CommandHandler('test', self.callback_basic)
|
||||
check = handler.check_update(false_update)
|
||||
assert check is None or check is False
|
||||
"""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, message):
|
||||
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
|
||||
|
||||
class TestFilter(BaseFilter):
|
||||
def __init__(self):
|
||||
self.tested = False
|
||||
|
||||
def filter(self, message):
|
||||
self.tested = True
|
||||
|
||||
test_filter = TestFilter()
|
||||
|
||||
handler = CommandHandler('test', self.callback_basic,
|
||||
filters=test_filter)
|
||||
message.text = '/star'
|
||||
|
||||
check = handler.check_update(Update(0, message=message))
|
||||
assert check is None or check is False
|
||||
|
||||
assert not test_filter.tested
|
||||
|
||||
def test_context(self, cdp, message):
|
||||
handler = CommandHandler('test', self.callback_context)
|
||||
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)
|
||||
|
||||
message.text = '/test'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
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_args(self, cdp, message):
|
||||
handler = CommandHandler('test', self.callback_context_args)
|
||||
cdp.add_handler(handler)
|
||||
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)
|
||||
|
||||
message.text = '/test'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert not self.test_flag
|
||||
|
||||
message.text = '/test one two'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
|
||||
def test_context_regex(self, cdp, message):
|
||||
handler = CommandHandler('test', self.callback_context_regex1, Filters.regex('one two'))
|
||||
cdp.add_handler(handler)
|
||||
|
||||
message.text = '/test'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert not self.test_flag
|
||||
|
||||
message.text += ' one two'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
|
||||
def test_context_multiple_regex(self, cdp, message):
|
||||
handler = CommandHandler('test', self.callback_context_regex2,
|
||||
Filters.regex('one') & Filters.regex('two'))
|
||||
cdp.add_handler(handler)
|
||||
|
||||
message.text = '/test'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert not self.test_flag
|
||||
|
||||
message.text += ' one two'
|
||||
cdp.process_update(Update(0, message))
|
||||
assert self.test_flag
|
||||
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)
|
||||
|
||||
|
||||
par = ['!help', '!test', '#help', '#test', 'mytrig-help', 'mytrig-test']
|
||||
# ----------------------------- PrefixHandler -----------------------------
|
||||
|
||||
def combinations(prefixes, commands):
|
||||
return (prefix + command for prefix in prefixes for command in commands)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function', params=par)
|
||||
def prefixmessage(bot, request):
|
||||
return Message(message_id=1,
|
||||
from_user=User(id=1, first_name='', is_bot=False),
|
||||
date=None,
|
||||
chat=Chat(id=1, type=''),
|
||||
text=request.param,
|
||||
bot=bot)
|
||||
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
|
||||
|
||||
class TestPrefixHandler(object):
|
||||
test_flag = False
|
||||
SRE_TYPE = type(re.match("", ""))
|
||||
@pytest.fixture(scope='class', params=[1, 2], ids=['single prefix', 'multiple prefixes'])
|
||||
def prefixes(self, request):
|
||||
return TestPrefixHandler.PREFIXES[:request.param]
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset(self):
|
||||
self.test_flag = False
|
||||
@pytest.fixture(scope='class', params=COMMANDS)
|
||||
def command(self, request):
|
||||
return request.param
|
||||
|
||||
def callback_basic(self, bot, update):
|
||||
test_bot = isinstance(bot, Bot)
|
||||
test_update = isinstance(update, Update)
|
||||
self.test_flag = test_bot and test_update
|
||||
@pytest.fixture(scope='class', params=[1, 2], ids=['single command', 'multiple commands'])
|
||||
def commands(self, request):
|
||||
return TestPrefixHandler.COMMANDS[:request.param]
|
||||
|
||||
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)
|
||||
@pytest.fixture(scope='class')
|
||||
def prefix_message_text(self, prefix, command):
|
||||
return prefix + command
|
||||
|
||||
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)
|
||||
@pytest.fixture(scope='class')
|
||||
def prefix_message(self, prefix_message_text):
|
||||
return make_message(prefix_message_text)
|
||||
|
||||
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)
|
||||
@pytest.fixture(scope='class')
|
||||
def prefix_message_update(self, prefix_message):
|
||||
return make_message_update(prefix_message)
|
||||
|
||||
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 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 par:
|
||||
if update.message.text in TestPrefixHandler.COMBINATIONS:
|
||||
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, 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(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_basic(self, dp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
|
||||
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
|
||||
|
||||
dp.process_update(Update(0, prefixmessage))
|
||||
assert self.test_flag
|
||||
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)))
|
||||
|
||||
prefixmessage.text = 'test'
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is None or check is False
|
||||
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
|
||||
|
||||
prefixmessage.text = '#nocom'
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is None or check is False
|
||||
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)
|
||||
|
||||
message.text = 'not !test at start'
|
||||
check = handler.check_update(Update(0, message))
|
||||
assert check is None or check is False
|
||||
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_single_prefix_single_command(self, prefixmessage):
|
||||
handler = PrefixHandler('!', 'test', self.callback_basic)
|
||||
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
if prefixmessage.text in ['!test']:
|
||||
assert check is not None and check is not False
|
||||
else:
|
||||
assert check is None or check is False
|
||||
|
||||
def test_single_prefix_multi_command(self, prefixmessage):
|
||||
handler = PrefixHandler('!', ['test', 'help'], self.callback_basic)
|
||||
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
if prefixmessage.text in ['!test', '!help']:
|
||||
assert check is not None and check is not False
|
||||
else:
|
||||
assert check is None or check is False
|
||||
|
||||
def test_multi_prefix_single_command(self, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#'], 'test', self.callback_basic)
|
||||
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
if prefixmessage.text in ['!test', '#test']:
|
||||
assert check is not None and check is not False
|
||||
else:
|
||||
assert check is None or check is False
|
||||
|
||||
def test_no_edited(self, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is not None and check is not False
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=prefixmessage))
|
||||
assert check is not None and check is not False
|
||||
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
|
||||
filters=~Filters.update.edited_message)
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is not None and check is not False
|
||||
|
||||
check = handler.check_update(Update(0, edited_message=prefixmessage))
|
||||
assert check is None or check is False
|
||||
|
||||
def test_with_filter(self, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
|
||||
filters=Filters.group)
|
||||
|
||||
prefixmessage.chat = Chat(-23, 'group')
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is not None and check is not False
|
||||
|
||||
prefixmessage.chat = Chat(23, 'private')
|
||||
check = handler.check_update(Update(0, prefixmessage))
|
||||
assert check is None or check is False
|
||||
|
||||
def test_pass_args(self, dp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.ch_callback_args,
|
||||
pass_args=True)
|
||||
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))
|
||||
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
update_with_args = make_message_update(prefix_message.text + ' one two')
|
||||
assert self.response(dp, update_with_args)
|
||||
|
||||
self.test_flag = False
|
||||
prefixmessage.text += ' one two'
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
def test_pass_user_or_chat_data(self, dp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_1,
|
||||
pass_user_data=True)
|
||||
@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)
|
||||
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
self.test_flag = False
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_1,
|
||||
pass_chat_data=True)
|
||||
dp.add_handler(handler)
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
self.test_flag = False
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_2,
|
||||
pass_chat_data=True, pass_user_data=True)
|
||||
dp.add_handler(handler)
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
def test_pass_job_or_update_queue(self, dp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_1,
|
||||
pass_job_queue=True)
|
||||
dp.add_handler(handler)
|
||||
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
self.test_flag = False
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_1,
|
||||
pass_update_queue=True)
|
||||
dp.add_handler(handler)
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
dp.remove_handler(handler)
|
||||
self.test_flag = False
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_2,
|
||||
pass_job_queue=True, pass_update_queue=True)
|
||||
dp.add_handler(handler)
|
||||
dp.process_update(Update(0, message=prefixmessage))
|
||||
assert self.test_flag
|
||||
assert self.response(dp, prefix_message_update) \
|
||||
== pass_combination.get(pass_keyword, False)
|
||||
|
||||
def test_other_update_types(self, false_update):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
|
||||
check = handler.check_update(false_update)
|
||||
assert check is None or check is False
|
||||
handler = self.make_default_handler()
|
||||
assert not is_match(handler, false_update)
|
||||
|
||||
def test_filters_for_wrong_command(self, prefixmessage):
|
||||
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
|
||||
|
||||
class TestFilter(BaseFilter):
|
||||
def __init__(self):
|
||||
self.tested = False
|
||||
|
||||
def filter(self, message):
|
||||
self.tested = True
|
||||
|
||||
test_filter = TestFilter()
|
||||
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
|
||||
filters=test_filter)
|
||||
|
||||
prefixmessage.text = '/star'
|
||||
|
||||
check = handler.check_update(Update(0, message=prefixmessage))
|
||||
assert check is None or check is False
|
||||
|
||||
assert not test_filter.tested
|
||||
|
||||
def test_context(self, cdp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_context)
|
||||
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)
|
||||
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert self.test_flag
|
||||
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_args(self, cdp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
|
||||
self.callback_context_args)
|
||||
cdp.add_handler(handler)
|
||||
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)
|
||||
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert not self.test_flag
|
||||
|
||||
prefixmessage.text += ' one two'
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
def test_context_regex(self, cdp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
|
||||
self.callback_context_regex1, Filters.regex('one two'))
|
||||
cdp.add_handler(handler)
|
||||
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert not self.test_flag
|
||||
|
||||
prefixmessage.text += ' one two'
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert self.test_flag
|
||||
|
||||
def test_context_multiple_regex(self, cdp, prefixmessage):
|
||||
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
|
||||
self.callback_context_regex2,
|
||||
Filters.regex('one') & Filters.regex('two'))
|
||||
cdp.add_handler(handler)
|
||||
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert not self.test_flag
|
||||
|
||||
prefixmessage.text += ' one two'
|
||||
cdp.process_update(Update(0, prefixmessage))
|
||||
assert self.test_flag
|
||||
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)
|
||||
|
||||
@@ -24,7 +24,7 @@ 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)
|
||||
MessageHandler, Filters, InlineQueryHandler, CallbackContext)
|
||||
|
||||
|
||||
@pytest.fixture(scope='class')
|
||||
@@ -43,6 +43,10 @@ class TestConversationHandler(object):
|
||||
# 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)
|
||||
@@ -69,6 +73,43 @@ class TestConversationHandler(object):
|
||||
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
|
||||
@@ -76,7 +117,10 @@ class TestConversationHandler(object):
|
||||
|
||||
# Actions
|
||||
def start(self, bot, update):
|
||||
return self._set_state(update, self.THIRSTY)
|
||||
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)
|
||||
@@ -88,7 +132,10 @@ class TestConversationHandler(object):
|
||||
return self._set_state(update, None)
|
||||
|
||||
def brew(self, bot, update):
|
||||
return self._set_state(update, self.BREWING)
|
||||
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)
|
||||
@@ -98,11 +145,39 @@ class TestConversationHandler(object):
|
||||
|
||||
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'"):
|
||||
@@ -555,6 +630,101 @@ class TestConversationHandler(object):
|
||||
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,
|
||||
@@ -609,3 +779,108 @@ class TestConversationHandler(object):
|
||||
"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
|
||||
|
||||
+25
-9
@@ -27,7 +27,7 @@ import re
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def update():
|
||||
return Update(0, Message(0, User(0, 'Testuser', False), datetime.datetime.now(),
|
||||
return Update(0, Message(0, User(0, 'Testuser', False), datetime.datetime.utcnow(),
|
||||
Chat(0, 'private')))
|
||||
|
||||
|
||||
@@ -43,9 +43,25 @@ class TestFilters(object):
|
||||
|
||||
def test_filters_text(self, update):
|
||||
update.message.text = 'test'
|
||||
assert Filters.text(update)
|
||||
assert (Filters.text)(update)
|
||||
update.message.text = '/test'
|
||||
assert not Filters.text(update)
|
||||
assert not (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(self, update):
|
||||
update.message.text = 'test'
|
||||
@@ -138,7 +154,7 @@ class TestFilters(object):
|
||||
assert isinstance(matches, list)
|
||||
assert len(matches) == 2
|
||||
assert all([type(res) == SRE_TYPE for res in matches])
|
||||
update.message.forward_date = datetime.datetime.now()
|
||||
update.message.forward_date = datetime.datetime.utcnow()
|
||||
result = filter(update)
|
||||
assert result
|
||||
assert isinstance(result, dict)
|
||||
@@ -248,7 +264,7 @@ class TestFilters(object):
|
||||
assert result
|
||||
|
||||
def test_filters_reply(self, update):
|
||||
another_message = Message(1, User(1, 'TestOther', False), datetime.datetime.now(),
|
||||
another_message = Message(1, User(1, 'TestOther', False), datetime.datetime.utcnow(),
|
||||
Chat(0, 'private'))
|
||||
update.message.text = 'test'
|
||||
assert not Filters.reply(update)
|
||||
@@ -475,7 +491,7 @@ class TestFilters(object):
|
||||
|
||||
def test_filters_forwarded(self, update):
|
||||
assert not Filters.forwarded(update)
|
||||
update.message.forward_date = datetime.datetime.now()
|
||||
update.message.forward_date = datetime.datetime.utcnow()
|
||||
assert Filters.forwarded(update)
|
||||
|
||||
def test_filters_game(self, update):
|
||||
@@ -606,7 +622,7 @@ class TestFilters(object):
|
||||
|
||||
def test_and_filters(self, update):
|
||||
update.message.text = 'test'
|
||||
update.message.forward_date = datetime.datetime.now()
|
||||
update.message.forward_date = datetime.datetime.utcnow()
|
||||
assert (Filters.text & Filters.forwarded)(update)
|
||||
update.message.text = '/test'
|
||||
assert not (Filters.text & Filters.forwarded)(update)
|
||||
@@ -615,7 +631,7 @@ class TestFilters(object):
|
||||
assert not (Filters.text & Filters.forwarded)(update)
|
||||
|
||||
update.message.text = 'test'
|
||||
update.message.forward_date = datetime.datetime.now()
|
||||
update.message.forward_date = datetime.datetime.utcnow()
|
||||
assert (Filters.text & Filters.forwarded & Filters.private)(update)
|
||||
|
||||
def test_or_filters(self, update):
|
||||
@@ -630,7 +646,7 @@ class TestFilters(object):
|
||||
|
||||
def test_and_or_filters(self, update):
|
||||
update.message.text = 'test'
|
||||
update.message.forward_date = datetime.datetime.now()
|
||||
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)
|
||||
|
||||
+116
-2
@@ -16,12 +16,27 @@
|
||||
#
|
||||
# You 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):
|
||||
@@ -31,8 +46,107 @@ class TestHelpers(object):
|
||||
|
||||
assert expected_str == helpers.escape_markdown(test_str)
|
||||
|
||||
def test_effective_message_type(self):
|
||||
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,
|
||||
@@ -47,7 +161,7 @@ class TestHelpers(object):
|
||||
assert helpers.effective_message_type(test_message) == 'text'
|
||||
test_message.text = None
|
||||
|
||||
test_message = build_test_message(sticker=Sticker('sticker_id', 50, 50))
|
||||
test_message = build_test_message(sticker=Sticker('sticker_id', 50, 50, False))
|
||||
assert helpers.effective_message_type(test_message) == 'sticker'
|
||||
test_message.sticker = None
|
||||
|
||||
|
||||
+75
-20
@@ -16,7 +16,7 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import datetime
|
||||
import datetime as dtm
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
@@ -28,6 +28,7 @@ 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')
|
||||
@@ -40,6 +41,8 @@ def job_queue(bot, _dp):
|
||||
|
||||
|
||||
@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
|
||||
@@ -81,6 +84,24 @@ class TestJobQueue(object):
|
||||
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)
|
||||
@@ -98,6 +119,13 @@ class TestJobQueue(object):
|
||||
sleep(0.07)
|
||||
assert self.result == 1
|
||||
|
||||
def test_run_repeating_first_timezone(self, job_queue, timezone):
|
||||
"""Test correct scheduling of job when passing a timezone-aware datetime as ``first``"""
|
||||
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)
|
||||
@@ -181,7 +209,7 @@ class TestJobQueue(object):
|
||||
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 = datetime.timedelta(seconds=0.05)
|
||||
interval = dtm.timedelta(seconds=0.05)
|
||||
expected_time = time.time() + interval.total_seconds()
|
||||
|
||||
job_queue.run_once(self.job_datetime_tests, interval)
|
||||
@@ -190,43 +218,70 @@ class TestJobQueue(object):
|
||||
|
||||
def test_time_unit_dt_datetime(self, job_queue):
|
||||
# Testing running at a specific datetime
|
||||
delta = datetime.timedelta(seconds=0.05)
|
||||
when = datetime.datetime.now() + delta
|
||||
expected_time = time.time() + delta.total_seconds()
|
||||
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 pytest.approx(self.job_time) == expected_time
|
||||
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 = 0.05
|
||||
when = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time()
|
||||
expected_time = time.time() + delta
|
||||
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 pytest.approx(self.job_time) == expected_time
|
||||
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 jobs next_t has been calculated correctly
|
||||
delta = -2
|
||||
when = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time()
|
||||
expected_time = time.time() + delta + 60 * 60 * 24
|
||||
# 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 pytest.approx(job_queue._queue.get(False)[0]) == expected_time
|
||||
assert job_queue._queue.get(False)[0] == pytest.approx(expected_time)
|
||||
|
||||
def test_run_daily(self, job_queue):
|
||||
delta = 0.5
|
||||
time_of_day = (datetime.datetime.now() + datetime.timedelta(seconds=delta)).time()
|
||||
expected_time = time.time() + 60 * 60 * 24 + delta
|
||||
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.6)
|
||||
sleep(0.2)
|
||||
assert self.result == 1
|
||||
assert pytest.approx(job_queue._queue.get(False)[0]) == expected_time
|
||||
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)
|
||||
|
||||
+26
-12
@@ -35,12 +35,12 @@ def message(bot):
|
||||
@pytest.fixture(scope='function',
|
||||
params=[
|
||||
{'forward_from': User(99, 'forward_user', False),
|
||||
'forward_date': datetime.now()},
|
||||
'forward_date': datetime.utcnow()},
|
||||
{'forward_from_chat': Chat(-23, 'channel'),
|
||||
'forward_from_message_id': 101,
|
||||
'forward_date': datetime.now()},
|
||||
'forward_date': datetime.utcnow()},
|
||||
{'reply_to_message': Message(50, None, None, None)},
|
||||
{'edit_date': datetime.now()},
|
||||
{'edit_date': datetime.utcnow()},
|
||||
{'text': 'a text message',
|
||||
'enitites': [MessageEntity('bold', 10, 4),
|
||||
MessageEntity('italic', 16, 7)]},
|
||||
@@ -57,7 +57,7 @@ def message(bot):
|
||||
[PhotoSize('game_photo_id', 30, 30), ])},
|
||||
{'photo': [PhotoSize('photo_id', 50, 50)],
|
||||
'caption': 'photo_file'},
|
||||
{'sticker': Sticker('sticker_id', 50, 50)},
|
||||
{'sticker': Sticker('sticker_id', 50, 50, True)},
|
||||
{'video': Video('video_id', 12, 12, 12),
|
||||
'caption': 'video_file'},
|
||||
{'voice': Voice('voice_id', 5)},
|
||||
@@ -114,7 +114,7 @@ def message_params(bot, request):
|
||||
class TestMessage(object):
|
||||
id = 1
|
||||
from_user = User(2, 'testuser', False)
|
||||
date = datetime.now()
|
||||
date = datetime.utcnow()
|
||||
chat = Chat(3, 'private')
|
||||
test_entities = [{'length': 4, 'offset': 10, 'type': 'bold'},
|
||||
{'length': 7, 'offset': 16, 'type': 'italic'},
|
||||
@@ -303,16 +303,30 @@ class TestMessage(object):
|
||||
def test_chat_id(self, message):
|
||||
assert message.chat_id == message.chat.id
|
||||
|
||||
def test_link(self, message):
|
||||
assert message.link is None
|
||||
@pytest.mark.parametrize('type', argvalues=[Chat.SUPERGROUP, Chat.CHANNEL])
|
||||
def test_link_with_username(self, message, type):
|
||||
message.chat.username = 'username'
|
||||
message.chat.type = 'supergroup'
|
||||
message.chat.type = type
|
||||
assert message.link == 'https://t.me/{}/{}'.format(message.chat.username,
|
||||
message.message_id)
|
||||
message.chat.type = 'channel'
|
||||
assert message.link == 'https://t.me/{}/{}'.format(message.chat.username,
|
||||
message.message_id)
|
||||
message.chat.type = 'private'
|
||||
|
||||
@pytest.mark.parametrize('type, id', argvalues=[
|
||||
(Chat.CHANNEL, -1003), (Chat.SUPERGROUP, -1003), (Chat.GROUP, -3)
|
||||
])
|
||||
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
|
||||
|
||||
def test_effective_attachment(self, message_params):
|
||||
|
||||
@@ -26,6 +26,8 @@ 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
|
||||
|
||||
+2
-9
@@ -17,8 +17,6 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import os
|
||||
import sys
|
||||
from platform import python_implementation
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -30,17 +28,12 @@ def call_pre_commit_hook(hook_id):
|
||||
|
||||
@pytest.mark.nocoverage
|
||||
@pytest.mark.parametrize('hook_id', argvalues=('yapf', 'flake8', 'pylint'))
|
||||
@pytest.mark.skipif(not (os.getenv('TRAVIS') or os.getenv('APPVEYOR')), reason='Not running in CI')
|
||||
@pytest.mark.skipif(not sys.version_info[:2] == (3, 6) or python_implementation() != 'CPython',
|
||||
reason='Only running pre-commit-hooks on newest tested python version, '
|
||||
'as they are slow and consistent across platforms.')
|
||||
@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 sys.version_info[:2] in ((3, 6), (2, 7)) or python_implementation() != 'CPython',
|
||||
reason='Only testing build on 2.7 and 3.6')
|
||||
@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
|
||||
|
||||
+1
-1
@@ -317,7 +317,7 @@ class TestPhoto(object):
|
||||
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)
|
||||
e = Sticker(photo.file_id, self.width, self.height, False)
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
+21
-10
@@ -49,6 +49,7 @@ class TestSticker(object):
|
||||
emoji = '💪'
|
||||
width = 510
|
||||
height = 512
|
||||
is_animated = False
|
||||
file_size = 39518
|
||||
thumb_width = 319
|
||||
thumb_height = 320
|
||||
@@ -66,6 +67,7 @@ class TestSticker(object):
|
||||
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
|
||||
@@ -81,6 +83,7 @@ class TestSticker(object):
|
||||
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)
|
||||
@@ -132,6 +135,7 @@ class TestSticker(object):
|
||||
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)
|
||||
@@ -146,6 +150,7 @@ class TestSticker(object):
|
||||
'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
|
||||
@@ -155,6 +160,7 @@ class TestSticker(object):
|
||||
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
|
||||
@@ -174,6 +180,7 @@ class TestSticker(object):
|
||||
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()
|
||||
|
||||
@@ -194,11 +201,11 @@ class TestSticker(object):
|
||||
bot.send_sticker(chat_id)
|
||||
|
||||
def test_equality(self, sticker):
|
||||
a = Sticker(sticker.file_id, self.width, self.height)
|
||||
b = Sticker(sticker.file_id, self.width, self.height)
|
||||
c = Sticker(sticker.file_id, 0, 0)
|
||||
d = Sticker('', self.width, self.height)
|
||||
e = PhotoSize(sticker.file_id, self.width, self.height)
|
||||
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)
|
||||
@@ -224,8 +231,9 @@ def sticker_set(bot):
|
||||
|
||||
class TestStickerSet(object):
|
||||
title = 'Test stickers'
|
||||
is_animated = True
|
||||
contains_masks = False
|
||||
stickers = [Sticker('file_id', 512, 512)]
|
||||
stickers = [Sticker('file_id', 512, 512, True)]
|
||||
name = 'NOTAREALNAME'
|
||||
|
||||
def test_de_json(self, bot):
|
||||
@@ -233,6 +241,7 @@ class TestStickerSet(object):
|
||||
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]
|
||||
}
|
||||
@@ -240,6 +249,7 @@ class TestStickerSet(object):
|
||||
|
||||
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
|
||||
|
||||
@@ -258,6 +268,7 @@ class TestStickerSet(object):
|
||||
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()
|
||||
|
||||
@@ -282,10 +293,10 @@ class TestStickerSet(object):
|
||||
assert sticker.get_file()
|
||||
|
||||
def test_equality(self):
|
||||
a = StickerSet(self.name, self.title, self.contains_masks, self.stickers)
|
||||
b = StickerSet(self.name, self.title, self.contains_masks, self.stickers)
|
||||
c = StickerSet(self.name, None, None, None)
|
||||
d = StickerSet('blah', self.title, self.contains_masks, self.stickers)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user