mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2026-06-25 10:44:38 +00:00
Compare commits
30 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 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
+51
@@ -2,8 +2,59 @@
|
||||
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:**
|
||||
|
||||
|
||||
+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>`_.
|
||||
|
||||
|
||||
============
|
||||
|
||||
+2
-2
@@ -58,9 +58,9 @@ author = u'Leandro Toledo'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '12.1' # telegram.__version__[:3]
|
||||
version = '12.3' # telegram.__version__[:3]
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '12.2.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.
|
||||
|
||||
+2
-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.
|
||||
|
||||
@@ -35,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.
|
||||
|
||||
@@ -21,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:
|
||||
@@ -77,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():
|
||||
|
||||
@@ -42,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)]
|
||||
|
||||
@@ -66,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -184,7 +184,9 @@ class ConversationHandler(Handler):
|
||||
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__)
|
||||
|
||||
@@ -262,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):
|
||||
@@ -281,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:
|
||||
@@ -340,15 +344,22 @@ class ConversationHandler(Handler):
|
||||
|
||||
"""
|
||||
conversation_key, handler, check_result = check_result
|
||||
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:
|
||||
# Remove the old timeout job (if present)
|
||||
timeout_job = self.timeout_jobs.pop(conversation_key, None)
|
||||
|
||||
if timeout_job is not None:
|
||||
timeout_job.schedule_removal()
|
||||
|
||||
new_state = handler.handle_update(update, dispatcher, check_result, context)
|
||||
|
||||
with self._timeout_jobs_lock:
|
||||
if self.conversation_timeout and new_state != self.END:
|
||||
# Add the new timeout job
|
||||
self.timeout_jobs[conversation_key] = dispatcher.job_queue.run_once(
|
||||
self._trigger_timeout, self.conversation_timeout,
|
||||
context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
|
||||
|
||||
if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent:
|
||||
self.update_state(self.END, conversation_key)
|
||||
@@ -358,36 +369,47 @@ class ConversationHandler(Handler):
|
||||
|
||||
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'
|
||||
|
||||
+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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,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,)
|
||||
|
||||
+30
-9
@@ -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::
|
||||
|
||||
+130
-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):
|
||||
|
||||
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.2.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
|
||||
|
||||
|
||||
+19
-2
@@ -27,9 +27,11 @@ from time import sleep
|
||||
|
||||
import pytest
|
||||
|
||||
from telegram import Bot, Message, User, Chat, MessageEntity, Update, \
|
||||
InlineQuery, CallbackQuery, ShippingQuery, PreCheckoutQuery, ChosenInlineResult
|
||||
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)
|
||||
@@ -37,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
|
||||
@@ -253,3 +260,13 @@ def get_false_update_fixture_decorator_params():
|
||||
@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]')
|
||||
+5
-4
@@ -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
|
||||
@@ -108,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)
|
||||
|
||||
@@ -614,7 +615,7 @@ class TestBot(object):
|
||||
assert bot.restrict_chat_member(channel_id,
|
||||
95205500,
|
||||
chat_permissions,
|
||||
until_date=datetime.now())
|
||||
until_date=dtm.datetime.utcnow())
|
||||
|
||||
@flaky(3, 1)
|
||||
@pytest.mark.timeout(10)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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')
|
||||
@@ -117,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)
|
||||
@@ -129,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)
|
||||
@@ -139,9 +145,20 @@ 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)
|
||||
@@ -613,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,
|
||||
@@ -670,10 +782,10 @@ class TestConversationHandler(object):
|
||||
|
||||
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)]
|
||||
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)
|
||||
|
||||
+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)
|
||||
|
||||
+84
-1
@@ -16,6 +16,9 @@
|
||||
#
|
||||
# You 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
|
||||
@@ -23,6 +26,17 @@ 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):
|
||||
@@ -32,6 +46,76 @@ class TestHelpers(object):
|
||||
|
||||
assert expected_str == helpers.escape_markdown(test_str)
|
||||
|
||||
def test_to_float_timestamp_absolute_naive(self):
|
||||
"""Conversion from timezone-naive datetime to timestamp.
|
||||
Naive datetimes should be assumed to be in UTC.
|
||||
"""
|
||||
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5)
|
||||
assert helpers.to_float_timestamp(datetime) == 1573431976.1
|
||||
|
||||
def test_to_float_timestamp_absolute_aware(self, timezone):
|
||||
"""Conversion from timezone-aware datetime to timestamp"""
|
||||
# we're parametrizing this with two different UTC offsets to exclude the possibility
|
||||
# of an xpass when the test is run in a timezone with the same UTC offset
|
||||
datetime = dtm.datetime(2019, 11, 11, 0, 26, 16, 10**5, tzinfo=timezone)
|
||||
assert (helpers.to_float_timestamp(datetime)
|
||||
== 1573431976.1 - timezone.utcoffset(None).total_seconds())
|
||||
|
||||
def test_to_float_timestamp_absolute_no_reference(self):
|
||||
"""A reference timestamp is only relevant for relative time specifications"""
|
||||
with pytest.raises(ValueError):
|
||||
helpers.to_float_timestamp(dtm.datetime(2019, 11, 11), reference_timestamp=123)
|
||||
|
||||
@pytest.mark.parametrize('time_spec', DELTA_TIME_SPECS, ids=str)
|
||||
def test_to_float_timestamp_delta(self, time_spec):
|
||||
"""Conversion from a 'delta' time specification to timestamp"""
|
||||
reference_t = 0
|
||||
delta = time_spec.total_seconds() if hasattr(time_spec, 'total_seconds') else time_spec
|
||||
assert helpers.to_float_timestamp(time_spec, reference_t) == reference_t + delta
|
||||
|
||||
def test_to_float_timestamp_time_of_day(self):
|
||||
"""Conversion from time-of-day specification to timestamp"""
|
||||
hour, hour_delta = 12, 1
|
||||
ref_t = _datetime_to_float_timestamp(dtm.datetime(1970, 1, 1, hour=hour))
|
||||
|
||||
# test for a time of day that is still to come, and one in the past
|
||||
time_future, time_past = dtm.time(hour + hour_delta), dtm.time(hour - hour_delta)
|
||||
assert helpers.to_float_timestamp(time_future, ref_t) == ref_t + 60 * 60 * hour_delta
|
||||
assert helpers.to_float_timestamp(time_past, ref_t) == ref_t + 60 * 60 * (24 - hour_delta)
|
||||
|
||||
def test_to_float_timestamp_time_of_day_timezone(self, timezone):
|
||||
"""Conversion from timezone-aware time-of-day specification to timestamp"""
|
||||
# we're parametrizing this with two different UTC offsets to exclude the possibility
|
||||
# of an xpass when the test is run in a timezone with the same UTC offset
|
||||
utc_offset = timezone.utcoffset(None)
|
||||
ref_datetime = dtm.datetime(1970, 1, 1, 12)
|
||||
ref_t, time_of_day = _datetime_to_float_timestamp(ref_datetime), ref_datetime.time()
|
||||
|
||||
# first test that naive time is assumed to be utc:
|
||||
assert helpers.to_float_timestamp(time_of_day, ref_t) == pytest.approx(ref_t)
|
||||
# test that by setting the timezone the timestamp changes accordingly:
|
||||
assert (helpers.to_float_timestamp(time_of_day.replace(tzinfo=timezone), ref_t)
|
||||
== pytest.approx(ref_t + (-utc_offset.total_seconds() % (24 * 60 * 60))))
|
||||
|
||||
@pytest.mark.parametrize('time_spec', RELATIVE_TIME_SPECS, ids=str)
|
||||
def test_to_float_timestamp_default_reference(self, time_spec):
|
||||
"""The reference timestamp for relative time specifications should default to now"""
|
||||
now = time.time()
|
||||
assert (helpers.to_float_timestamp(time_spec)
|
||||
== pytest.approx(helpers.to_float_timestamp(time_spec, reference_timestamp=now)))
|
||||
|
||||
@pytest.mark.parametrize('time_spec', TIME_SPECS, ids=str)
|
||||
def test_to_timestamp(self, time_spec):
|
||||
# delegate tests to `to_float_timestamp`
|
||||
assert helpers.to_timestamp(time_spec) == int(helpers.to_float_timestamp(time_spec))
|
||||
|
||||
def test_to_timestamp_none(self):
|
||||
# this 'convenience' behaviour has been left left for backwards compatibility
|
||||
assert helpers.to_timestamp(None) is None
|
||||
|
||||
def test_from_timestamp(self):
|
||||
assert helpers.from_timestamp(1573431976) == dtm.datetime(2019, 11, 11, 0, 26, 16)
|
||||
|
||||
def test_create_deep_linked_url(self):
|
||||
username = 'JamesTheMock'
|
||||
|
||||
@@ -63,7 +147,6 @@ class TestHelpers(object):
|
||||
helpers.create_deep_linked_url("abc", None)
|
||||
|
||||
def test_effective_message_type(self):
|
||||
|
||||
def build_test_message(**kwargs):
|
||||
config = dict(
|
||||
message_id=1,
|
||||
|
||||
+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)
|
||||
|
||||
+25
-11
@@ -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)]},
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user